v1.0.1: Boost related notes by frontmatter properties and shared tags

- EmbedSearch.searchSimilarToFile: +0.15 for notes linked via contextProperties
  frontmatter fields (related, collection, up, tags), +0.05 per shared tag (max 3)
- SearchResult: add optional `linked` field
- RelatedNotesView: show "verknüpft" badge for property-linked notes
- main.ts: pass contextProperties to EmbedSearch on init

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-03-05 02:32:43 +01:00
parent 32006149e7
commit ba8d4a1da1
8 changed files with 114 additions and 11 deletions
+44 -5
View File
@@ -32293,6 +32293,8 @@ var EmbedSearch = class {
constructor(app, modelId) {
this.excludeFolders = [];
// vault folder prefixes to skip
this.contextProperties = [];
// frontmatter keys whose links get a score boost
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.pipe = null;
this.cache = /* @__PURE__ */ new Map();
@@ -32508,18 +32510,51 @@ var EmbedSearch = class {
return [];
}
}
const linkedPaths = /* @__PURE__ */ new Set();
if (this.contextProperties.length > 0) {
const meta = this.app.metadataCache.getFileCache(file);
const links = meta?.frontmatterLinks ?? [];
for (const link of links) {
if (this.contextProperties.includes(link.key.split(".")[0])) {
const resolved = this.app.metadataCache.getFirstLinkpathDest(link.link, file.path);
if (resolved)
linkedPaths.add(resolved.path);
}
}
}
const fileMeta = this.app.metadataCache.getFileCache(file);
const fileTags = new Set(
(fileMeta?.tags ?? []).map((t) => t.tag.toLowerCase())
);
const scores = [];
for (const [path3, { vec }] of this.vecs) {
if (path3 === file.path)
continue;
const s = this.cosine(qvec, vec);
if (s > 0.2)
scores.push([path3, s]);
let s = this.cosine(qvec, vec);
if (s < 0.15)
continue;
if (linkedPaths.has(path3)) {
s = Math.min(1, s + 0.15);
}
if (fileTags.size > 0) {
const otherMeta = this.app.metadataCache.getFileCache(this.vecs.get(path3).file);
const otherTags = (otherMeta?.tags ?? []).map((t) => t.tag.toLowerCase());
let sharedTags = 0;
for (const tag of otherTags) {
if (fileTags.has(tag))
sharedTags++;
if (sharedTags >= 3)
break;
}
if (sharedTags > 0)
s = Math.min(1, s + sharedTags * 0.05);
}
scores.push([path3, s]);
}
scores.sort((a, b) => b[1] - a[1]);
return scores.slice(0, topK).map(([path3, score]) => {
const { file: f } = this.vecs.get(path3);
return { file: f, score, excerpt: "", title: f.basename };
return { file: f, score, excerpt: "", title: f.basename, linked: linkedPaths.has(path3) };
});
}
async search(query, topK = 8) {
@@ -33199,7 +33234,10 @@ var RelatedNotesView = class extends import_obsidian4.ItemView {
for (const r of results) {
const item = list.createDiv("vc-related-item");
const info = item.createDiv("vc-related-info");
info.createDiv({ cls: "vc-related-name", text: r.title });
const nameRow = info.createDiv("vc-related-name-row");
nameRow.createSpan({ cls: "vc-related-name", text: r.title });
if (r.linked)
nameRow.createSpan({ cls: "vc-related-linked", text: "verkn\xFCpft" });
const folder = r.file.parent?.path;
if (folder && folder !== "/") {
info.createDiv({ cls: "vc-related-folder", text: folder });
@@ -33334,6 +33372,7 @@ var MemexChatPlugin = class extends import_obsidian5.Plugin {
}
this.embedSearch = new EmbedSearch(this.app, this.settings.embeddingModel);
this.embedSearch.excludeFolders = this.settings.embedExcludeFolders ?? [];
this.embedSearch.contextProperties = this.settings.contextProperties ?? [];
this.registerEvent(
this.app.vault.on("modify", (file) => {
if (this.embedSearch && file instanceof import_obsidian5.TFile && file.extension === "md")
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "memex-chat",
"name": "Memex Chat",
"version": "1.0.0",
"version": "1.0.1",
"minAppVersion": "1.4.0",
"description": "Chat with your Obsidian vault using Claude AI — semantic context retrieval, @ mentions, thread history.",
"author": "Sven",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "memex-chat",
"version": "1.0.0",
"version": "1.0.1",
"description": "Obsidian plugin: Chat with your vault using Claude AI",
"main": "main.js",
"scripts": {
+41 -3
View File
@@ -27,6 +27,7 @@ export class EmbedSearch {
private app: App;
private modelId: string;
excludeFolders: string[] = []; // vault folder prefixes to skip
contextProperties: string[] = []; // frontmatter keys whose links get a score boost
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private pipe: ((text: string, opts: object) => Promise<{ data: Float32Array }>) | null = null;
private cache: Map<string, EmbedCacheEntry> = new Map(); // vaultPath → entry
@@ -278,16 +279,53 @@ export class EmbedSearch {
qvec = await this.embedWithTimeout(text);
} catch { return []; }
}
// Collect paths explicitly linked via contextProperty frontmatter fields
const linkedPaths = new Set<string>();
if (this.contextProperties.length > 0) {
const meta = this.app.metadataCache.getFileCache(file);
const links = meta?.frontmatterLinks ?? [];
for (const link of links) {
if (this.contextProperties.includes(link.key.split(".")[0])) {
const resolved = this.app.metadataCache.getFirstLinkpathDest(link.link, file.path);
if (resolved) linkedPaths.add(resolved.path);
}
}
}
// Collect tags of the current file
const fileMeta = this.app.metadataCache.getFileCache(file);
const fileTags = new Set<string>(
(fileMeta?.tags ?? []).map((t) => t.tag.toLowerCase())
);
const scores: Array<[string, number]> = [];
for (const [path, { vec }] of this.vecs) {
if (path === file.path) continue;
const s = this.cosine(qvec, vec);
if (s > 0.2) scores.push([path, s]);
let s = this.cosine(qvec, vec);
if (s < 0.15) continue; // broader pre-filter to allow boosted notes through
if (linkedPaths.has(path)) {
s = Math.min(1.0, s + 0.15);
}
if (fileTags.size > 0) {
const otherMeta = this.app.metadataCache.getFileCache(this.vecs.get(path)!.file);
const otherTags = (otherMeta?.tags ?? []).map((t) => t.tag.toLowerCase());
let sharedTags = 0;
for (const tag of otherTags) {
if (fileTags.has(tag)) sharedTags++;
if (sharedTags >= 3) break;
}
if (sharedTags > 0) s = Math.min(1.0, s + sharedTags * 0.05);
}
scores.push([path, s]);
}
scores.sort((a, b) => b[1] - a[1]);
return scores.slice(0, topK).map(([path, score]) => {
const { file: f } = this.vecs.get(path)!;
return { file: f, score, excerpt: "", title: f.basename };
return { file: f, score, excerpt: "", title: f.basename, linked: linkedPaths.has(path) };
});
}
+3 -1
View File
@@ -68,7 +68,9 @@ export class RelatedNotesView extends ItemView {
const item = list.createDiv("vc-related-item");
const info = item.createDiv("vc-related-info");
info.createDiv({ cls: "vc-related-name", text: r.title });
const nameRow = info.createDiv("vc-related-name-row");
nameRow.createSpan({ cls: "vc-related-name", text: r.title });
if (r.linked) nameRow.createSpan({ cls: "vc-related-linked", text: "verknüpft" });
// Folder path (dimmed)
const folder = r.file.parent?.path;
+2
View File
@@ -5,6 +5,8 @@ export interface SearchResult {
score: number;
excerpt: string;
title: string;
/** True when the note is explicitly linked via a contextProperty frontmatter field */
linked?: boolean;
}
/** Minimal TF-IDF search engine over the Obsidian vault */
+1
View File
@@ -146,6 +146,7 @@ export default class MemexChatPlugin extends Plugin {
}
this.embedSearch = new EmbedSearch(this.app, this.settings.embeddingModel);
this.embedSearch.excludeFolders = this.settings.embedExcludeFolders ?? [];
this.embedSearch.contextProperties = this.settings.contextProperties ?? [];
// Re-embed modified notes as they change
this.registerEvent(
+21
View File
@@ -51,6 +51,13 @@
min-width: 0;
}
.vc-related-name-row {
display: flex;
align-items: baseline;
gap: 5px;
min-width: 0;
}
.vc-related-name {
font-size: 12px;
font-weight: 500;
@@ -58,6 +65,20 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.vc-related-linked {
font-size: 9px;
font-weight: 600;
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 15%, transparent);
border-radius: 3px;
padding: 0 4px;
white-space: nowrap;
flex-shrink: 0;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.vc-related-folder {