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:
@@ -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
@@ -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
@@ -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
@@ -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) };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user