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 { 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();
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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++;
|
|
||||||
}
|
}
|
||||||
|
return tokens;
|
||||||
if (current) {
|
|
||||||
tokens.push(current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokens.filter(t => t.length > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
20
vault/.obsidian/graph.json.bak
vendored
20
vault/.obsidian/graph.json.bak
vendored
@ -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
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user