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 { 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<Note[]>(() => {
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();

View File

@ -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);

View File

@ -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;
}
/**

View File

@ -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
}