fix: deduplicate related notes and normalize scores per section

- Deduplicate MemPalace results by source (keep highest score)
- Filter cross-section duplicates (vault removes notes already in MemPalace)
- Add per-section max-normalization so both sections display on 0–100% scale

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-04-14 14:51:18 +02:00
parent 2822e8f954
commit 207c4428f8
2 changed files with 54 additions and 4 deletions
+26 -2
View File
@@ -33427,7 +33427,14 @@ var RelatedNotesView = class extends import_obsidian4.ItemView {
useMempalace ? this.queryMempalace(mpQuery, topK, file.basename) : Promise.resolve([]), useMempalace ? this.queryMempalace(mpQuery, topK, file.basename) : Promise.resolve([]),
embedReady ? es.searchSimilarToFile(file) : Promise.resolve([]) embedReady ? es.searchSimilarToFile(file) : Promise.resolve([])
]); ]);
this.render(mpResults, nativeResults, file.basename, embedReady ? modelShort : null); const mpNames = new Set(mpResults.map((r) => r.source));
const dedupedNative = nativeResults.filter((r) => !mpNames.has(r.file.basename));
this.render(
this.normalizeScores(mpResults),
this.normalizeScores(dedupedNative),
file.basename,
embedReady ? modelShort : null
);
} }
// ─── MemPalace ──────────────────────────────────────────────────────────── // ─── MemPalace ────────────────────────────────────────────────────────────
/** Build a semantic query from the note: title + stripped body (first ~300 chars). */ /** Build a semantic query from the note: title + stripped body (first ~300 chars). */
@@ -33492,7 +33499,24 @@ var RelatedNotesView = class extends import_obsidian4.ItemView {
const excerpt = afterScore.replace(/\n{3,}/g, "\n\n").trim().slice(0, 240); const excerpt = afterScore.replace(/\n{3,}/g, "\n\n").trim().slice(0, 240);
results.push({ source, location, score, excerpt }); results.push({ source, location, score, excerpt });
} }
return results; const seen = /* @__PURE__ */ new Map();
for (const r of results) {
const existing = seen.get(r.source);
if (!existing || r.score > existing.score)
seen.set(r.source, r);
}
return [...seen.values()].sort((a, b) => b.score - a.score);
}
// ─── Score normalization ──────────────────────────────────────────────────
/** Max-normalize scores within a result set so the best item = 1.0.
* Preserves relative proportions; each section independently scaled. */
normalizeScores(items) {
if (items.length === 0)
return items;
const max2 = Math.max(...items.map((i) => i.score));
if (max2 === 0)
return items;
return items.map((i) => ({ ...i, score: i.score / max2 }));
} }
// ─── Rendering ──────────────────────────────────────────────────────────── // ─── Rendering ────────────────────────────────────────────────────────────
renderStatus(msg) { renderStatus(msg) {
+28 -2
View File
@@ -64,7 +64,15 @@ export class RelatedNotesView extends ItemView {
embedReady ? es!.searchSimilarToFile(file) : Promise.resolve([]), embedReady ? es!.searchSimilarToFile(file) : Promise.resolve([]),
]); ]);
this.render(mpResults, nativeResults, file.basename, embedReady ? modelShort : null); const mpNames = new Set(mpResults.map((r) => r.source));
const dedupedNative = nativeResults.filter((r) => !mpNames.has(r.file.basename));
this.render(
this.normalizeScores(mpResults),
this.normalizeScores(dedupedNative),
file.basename,
embedReady ? modelShort : null,
);
} }
// ─── MemPalace ──────────────────────────────────────────────────────────── // ─── MemPalace ────────────────────────────────────────────────────────────
@@ -136,7 +144,25 @@ export class RelatedNotesView extends ItemView {
results.push({ source, location, score, excerpt }); results.push({ source, location, score, excerpt });
} }
return results;
// Deduplicate by source, keeping highest-scoring entry
const seen = new Map<string, MpResult>();
for (const r of results) {
const existing = seen.get(r.source);
if (!existing || r.score > existing.score) seen.set(r.source, r);
}
return [...seen.values()].sort((a, b) => b.score - a.score);
}
// ─── Score normalization ──────────────────────────────────────────────────
/** Max-normalize scores within a result set so the best item = 1.0.
* Preserves relative proportions; each section independently scaled. */
private normalizeScores<T extends { score: number }>(items: T[]): T[] {
if (items.length === 0) return items;
const max = Math.max(...items.map((i) => i.score));
if (max === 0) return items;
return items.map((i) => ({ ...i, score: i.score / max }));
} }
// ─── Rendering ──────────────────────────────────────────────────────────── // ─── Rendering ────────────────────────────────────────────────────────────