refactor: replace basic search with SearchOrchestratorService for improved tag filtering and search index
This commit is contained in:
parent
989f3ee25a
commit
f50b03a099
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
20
vault/.obsidian/graph.json.bak
vendored
20
vault/.obsidian/graph.json.bak
vendored
@ -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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user