From f50b03a099137017a23db50579639dee1e6d3f41 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 5 Oct 2025 20:39:05 -0400 Subject: [PATCH] refactor: replace basic search with SearchOrchestratorService for improved tag filtering and search index --- src/app.component.ts | 40 +++++++--- src/core/search/search-parser.spec.ts | 6 ++ src/core/search/search-parser.ts | 105 +++----------------------- vault/.obsidian/graph.json.bak | 20 ++--- 4 files changed, 56 insertions(+), 115 deletions(-) diff --git a/src/app.component.ts b/src/app.component.ts index 202eea8..0a7415d 100644 --- a/src/app.component.ts +++ b/src/app.component.ts @@ -23,6 +23,8 @@ import { BookmarksService } from './core/bookmarks/bookmarks.service'; import { SearchInputWithAssistantComponent } from './components/search-input-with-assistant/search-input-with-assistant.component'; import { SearchHistoryService } from './core/search/search-history.service'; import { GraphIndexService } from './core/graph/graph-index.service'; +import { SearchIndexService } from './core/search/search-index.service'; +import { SearchOrchestratorService } from './core/search/search-orchestrator.service'; // Types import { FileMetadata, Note, TagInfo, VaultNode } from './types'; @@ -62,6 +64,8 @@ export class AppComponent implements OnInit, OnDestroy { private readonly bookmarksService = inject(BookmarksService); private readonly searchHistoryService = inject(SearchHistoryService); private readonly graphIndexService = inject(GraphIndexService); + private readonly searchIndex = inject(SearchIndexService); + private readonly searchOrchestrator = inject(SearchOrchestratorService); private readonly logService = inject(LogService); private elementRef = inject(ElementRef); @@ -227,22 +231,29 @@ export class AppComponent implements OnInit, OnDestroy { searchResults = computed(() => { const notes = this.vaultService.allNotes(); - const tagFilter = this.activeTagFilter(); - - if (tagFilter) { - return notes.filter(note => note.tags.some(tag => tag.toLowerCase() === tagFilter)); + const rawQuery = this.sidebarSearchTerm().trim(); + if (!rawQuery) { + return []; } - const term = this.sidebarSearchTerm().trim().toLowerCase(); - if (!term) return []; - const cleanedTerm = term.startsWith('#') ? term.slice(1) : term; - return notes.filter(note => - note.title.toLowerCase().includes(cleanedTerm) || - note.content.toLowerCase().includes(cleanedTerm) || - note.tags.some(tag => tag.toLowerCase().includes(cleanedTerm)) - ); + const tagFilter = this.activeTagFilter(); + const effectiveQuery = tagFilter ? `tag:${tagFilter}` : rawQuery; + + const results = this.searchOrchestrator.execute(effectiveQuery); + if (!results.length) { + return []; + } + + const noteById = new Map(notes.map(note => [note.id, note])); + return results + .map(result => noteById.get(result.noteId)) + .filter((note): note is Note => Boolean(note)); }); + clearTagFilter(): void { + this.sidebarSearchTerm.set(''); + } + constructor() { this.themeService.initFromStorage(); @@ -295,6 +306,11 @@ export class AppComponent implements OnInit, OnDestroy { this.graphIndexService.rebuildIndex(notes); }); + effect(() => { + const notes = this.vaultService.allNotes(); + this.searchIndex.rebuildIndex(notes); + }, { allowSignalWrites: true }); + // Persist outline tab effect(() => { const tab = this.outlineTab(); diff --git a/src/core/search/search-parser.spec.ts b/src/core/search/search-parser.spec.ts index 054a7b5..84814e4 100644 --- a/src/core/search/search-parser.spec.ts +++ b/src/core/search/search-parser.spec.ts @@ -37,6 +37,12 @@ describe('Search Parser', () => { expect(parsed.isEmpty).toBe(false); }); + it('queryToPredicate matches tag operator', () => { + const predicate = queryToPredicate(parseSearchQuery('tag:test')); + expect(predicate(createContext({ tags: ['#test', '#other'] }))).toBe(true); + expect(predicate(createContext({ tags: ['#other'] }))).toBe(false); + }); + it('queryToPredicate matches simple text', () => { const predicate = queryToPredicate(parseSearchQuery('hello')); expect(predicate(createContext())).toBe(true); diff --git a/src/core/search/search-parser.ts b/src/core/search/search-parser.ts index c475252..0662009 100644 --- a/src/core/search/search-parser.ts +++ b/src/core/search/search-parser.ts @@ -68,101 +68,20 @@ export function queryToPredicate(parsed: ParsedQuery, options?: SearchOptions): */ function tokenize(query: string): string[] { const tokens: string[] = []; - let current = ''; - let i = 0; - - while (i < query.length) { - const char = query[i]; - - // Handle quoted strings - if (char === '"') { - // If we are inside a prefix token like file: or path:, attach the quoted part to current - if (current.includes(':') && !current.includes(' ')) { - let quoted = '"'; - i++; - while (i < query.length && query[i] !== '"') { - quoted += query[i]; - i++; - } - if (i < query.length && query[i] === '"') { - quoted += '"'; - i++; - } - current += quoted; - continue; - } else { - if (current) { - tokens.push(current); - current = ''; - } - let quoted = ''; - i++; - while (i < query.length && query[i] !== '"') { - quoted += query[i]; - i++; - } - if (i < query.length && query[i] === '"') { - i++; - } - if (quoted) { - tokens.push(`"${quoted}"`); - } - continue; - } + // This regex handles: + // - quoted strings (double and single) + // - regex patterns /.../ + // - parentheses + // - property searches like [prop]:"value" + // - operators and words + const regex = /\s*("([^"]*)"|'([^']*)'|\/([^\/]*)\/|\(|\)|-?\[[^\]]*\]:?"[^"]*"|-?\[[^\]]*\]|-?[^\s\(\)]+)/g; + let match; + while ((match = regex.exec(query)) !== null) { + if (match[1]) { + tokens.push(match[1]); } - - // Handle regex patterns /.../ - if (char === '/' && (current === '' || current.endsWith(' '))) { - if (current) { - tokens.push(current); - current = ''; - } - let regex = '/'; - i++; - while (i < query.length && query[i] !== '/') { - regex += query[i]; - i++; - } - if (i < query.length) { - regex += '/'; - i++; - } - if (regex.length > 2) { - tokens.push(regex); - } - continue; - } - - // Handle parentheses - if (char === '(' || char === ')') { - if (current) { - tokens.push(current); - current = ''; - } - tokens.push(char); - i++; - continue; - } - - // Handle spaces - if (char === ' ') { - if (current) { - tokens.push(current); - current = ''; - } - i++; - continue; - } - - current += char; - i++; } - - if (current) { - tokens.push(current); - } - - return tokens.filter(t => t.length > 0); + return tokens; } /** diff --git a/vault/.obsidian/graph.json.bak b/vault/.obsidian/graph.json.bak index 8bc5a09..339d3ce 100644 --- a/vault/.obsidian/graph.json.bak +++ b/vault/.obsidian/graph.json.bak @@ -1,22 +1,22 @@ { "collapse-filter": false, "search": "", - "showTags": true, + "showTags": false, "showAttachments": false, "hideUnresolved": false, - "showOrphans": false, + "showOrphans": true, "collapse-color-groups": false, "colorGroups": [], "collapse-display": false, "showArrow": false, - "textFadeMultiplier": -3, - "nodeSizeMultiplier": 0.25, - "lineSizeMultiplier": 1.45, + "textFadeMultiplier": 0, + "nodeSizeMultiplier": 1, + "lineSizeMultiplier": 1, "collapse-forces": false, - "centerStrength": 0.27, - "repelStrength": 10, - "linkStrength": 0.15, - "linkDistance": 102, - "scale": 1.4019828977761002, + "centerStrength": 0.3, + "repelStrength": 17, + "linkStrength": 0.5, + "linkDistance": 200, + "scale": 1, "close": false } \ No newline at end of file