refactor: replace basic search with SearchOrchestratorService for improved tag filtering and search index

This commit is contained in:
Bruno Charest 2025-10-05 20:39:05 -04:00
parent 989f3ee25a
commit f50b03a099
4 changed files with 56 additions and 115 deletions

View File

@ -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 { SearchInputWithAssistantComponent } from './components/search-input-with-assistant/search-input-with-assistant.component';
import { SearchHistoryService } from './core/search/search-history.service'; import { SearchHistoryService } from './core/search/search-history.service';
import { GraphIndexService } from './core/graph/graph-index.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 // Types
import { FileMetadata, Note, TagInfo, VaultNode } from './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 bookmarksService = inject(BookmarksService);
private readonly searchHistoryService = inject(SearchHistoryService); private readonly searchHistoryService = inject(SearchHistoryService);
private readonly graphIndexService = inject(GraphIndexService); private readonly graphIndexService = inject(GraphIndexService);
private readonly searchIndex = inject(SearchIndexService);
private readonly searchOrchestrator = inject(SearchOrchestratorService);
private readonly logService = inject(LogService); private readonly logService = inject(LogService);
private elementRef = inject(ElementRef); private elementRef = inject(ElementRef);
@ -227,22 +231,29 @@ export class AppComponent implements OnInit, OnDestroy {
searchResults = computed<Note[]>(() => { searchResults = computed<Note[]>(() => {
const notes = this.vaultService.allNotes(); const notes = this.vaultService.allNotes();
const tagFilter = this.activeTagFilter(); const rawQuery = this.sidebarSearchTerm().trim();
if (!rawQuery) {
if (tagFilter) { return [];
return notes.filter(note => note.tags.some(tag => tag.toLowerCase() === tagFilter));
} }
const term = this.sidebarSearchTerm().trim().toLowerCase(); const tagFilter = this.activeTagFilter();
if (!term) return []; const effectiveQuery = tagFilter ? `tag:${tagFilter}` : rawQuery;
const cleanedTerm = term.startsWith('#') ? term.slice(1) : term;
return notes.filter(note => const results = this.searchOrchestrator.execute(effectiveQuery);
note.title.toLowerCase().includes(cleanedTerm) || if (!results.length) {
note.content.toLowerCase().includes(cleanedTerm) || return [];
note.tags.some(tag => tag.toLowerCase().includes(cleanedTerm)) }
);
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() { constructor() {
this.themeService.initFromStorage(); this.themeService.initFromStorage();
@ -295,6 +306,11 @@ export class AppComponent implements OnInit, OnDestroy {
this.graphIndexService.rebuildIndex(notes); this.graphIndexService.rebuildIndex(notes);
}); });
effect(() => {
const notes = this.vaultService.allNotes();
this.searchIndex.rebuildIndex(notes);
}, { allowSignalWrites: true });
// Persist outline tab // Persist outline tab
effect(() => { effect(() => {
const tab = this.outlineTab(); const tab = this.outlineTab();

View File

@ -37,6 +37,12 @@ describe('Search Parser', () => {
expect(parsed.isEmpty).toBe(false); 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', () => { it('queryToPredicate matches simple text', () => {
const predicate = queryToPredicate(parseSearchQuery('hello')); const predicate = queryToPredicate(parseSearchQuery('hello'));
expect(predicate(createContext())).toBe(true); expect(predicate(createContext())).toBe(true);

View File

@ -68,101 +68,20 @@ export function queryToPredicate(parsed: ParsedQuery, options?: SearchOptions):
*/ */
function tokenize(query: string): string[] { function tokenize(query: string): string[] {
const tokens: string[] = []; const tokens: string[] = [];
let current = ''; // This regex handles:
let i = 0; // - quoted strings (double and single)
// - regex patterns /.../
while (i < query.length) { // - parentheses
const char = query[i]; // - property searches like [prop]:"value"
// - operators and words
// Handle quoted strings const regex = /\s*("([^"]*)"|'([^']*)'|\/([^\/]*)\/|\(|\)|-?\[[^\]]*\]:?"[^"]*"|-?\[[^\]]*\]|-?[^\s\(\)]+)/g;
if (char === '"') { let match;
// If we are inside a prefix token like file: or path:, attach the quoted part to current while ((match = regex.exec(query)) !== null) {
if (current.includes(':') && !current.includes(' ')) { if (match[1]) {
let quoted = '"'; tokens.push(match[1]);
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;
} }
} }
return tokens;
// 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);
} }
/** /**

View File

@ -1,22 +1,22 @@
{ {
"collapse-filter": false, "collapse-filter": false,
"search": "", "search": "",
"showTags": true, "showTags": false,
"showAttachments": false, "showAttachments": false,
"hideUnresolved": false, "hideUnresolved": false,
"showOrphans": false, "showOrphans": true,
"collapse-color-groups": false, "collapse-color-groups": false,
"colorGroups": [], "colorGroups": [],
"collapse-display": false, "collapse-display": false,
"showArrow": false, "showArrow": false,
"textFadeMultiplier": -3, "textFadeMultiplier": 0,
"nodeSizeMultiplier": 0.25, "nodeSizeMultiplier": 1,
"lineSizeMultiplier": 1.45, "lineSizeMultiplier": 1,
"collapse-forces": false, "collapse-forces": false,
"centerStrength": 0.27, "centerStrength": 0.3,
"repelStrength": 10, "repelStrength": 17,
"linkStrength": 0.15, "linkStrength": 0.5,
"linkDistance": 102, "linkDistance": 200,
"scale": 1.4019828977761002, "scale": 1,
"close": false "close": false
} }