From 5b669ca28774c98503fe0d0b84d2144cdbbdae0b Mon Sep 17 00:00:00 2001 From: perf3ct Date: Sun, 10 Aug 2025 21:59:20 +0000 Subject: [PATCH] feat(search): also implement defensive checks for undefined notes --- .../search/expressions/note_flat_text.ts | 12 +++++++-- .../services/progressive_search.spec.ts | 22 ++++++++-------- .../services/search/services/search.spec.ts | 9 +++---- .../src/services/search/services/search.ts | 25 ++++++++++++++++++- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/apps/server/src/services/search/expressions/note_flat_text.ts b/apps/server/src/services/search/expressions/note_flat_text.ts index 5bd48a0ef..b9ad19c36 100644 --- a/apps/server/src/services/search/expressions/note_flat_text.ts +++ b/apps/server/src/services/search/expressions/note_flat_text.ts @@ -57,7 +57,11 @@ class NoteFlatTextExp extends Expression { const foundAttrTokens: string[] = []; for (const token of remainingTokens) { - if (note.type.includes(token) || note.mime.includes(token)) { + // Add defensive checks for undefined properties + const typeMatches = note.type && note.type.includes(token); + const mimeMatches = note.mime && note.mime.includes(token); + + if (typeMatches || mimeMatches) { foundAttrTokens.push(token); } } @@ -105,7 +109,11 @@ class NoteFlatTextExp extends Expression { const foundAttrTokens: string[] = []; for (const token of this.tokens) { - if (note.type.includes(token) || note.mime.includes(token)) { + // Add defensive checks for undefined properties + const typeMatches = note.type && note.type.includes(token); + const mimeMatches = note.mime && note.mime.includes(token); + + if (typeMatches || mimeMatches) { foundAttrTokens.push(token); } diff --git a/apps/server/src/services/search/services/progressive_search.spec.ts b/apps/server/src/services/search/services/progressive_search.spec.ts index d727c64f5..6bf6c2379 100644 --- a/apps/server/src/services/search/services/progressive_search.spec.ts +++ b/apps/server/src/services/search/services/progressive_search.spec.ts @@ -25,25 +25,25 @@ describe("Progressive Search Strategy", () => { it("should complete search with exact matches when sufficient results found", () => { // Create notes with exact matches rootNote - .child(note("Test Document One")) - .child(note("Test Report Two")) - .child(note("Test Analysis Three")) - .child(note("Test Summary Four")) - .child(note("Test Review Five")) - .child(note("Typo Test Documnt")); // This has a typo + .child(note("Document Analysis One")) + .child(note("Document Report Two")) + .child(note("Document Review Three")) + .child(note("Document Summary Four")) + .child(note("Document Overview Five")) + .child(note("Documnt Analysis Six")); // This has a typo that should require fuzzy matching const searchContext = new SearchContext(); - const searchResults = searchService.findResultsWithQuery("test", searchContext); + const searchResults = searchService.findResultsWithQuery("document", searchContext); - // Should find 5+ exact matches and not process the typo - expect(searchResults.length).toBeGreaterThanOrEqual(5); + // Should find 5 exact matches and not need fuzzy matching + expect(searchResults.length).toEqual(5); // Verify all results have high scores (exact matches) const highQualityResults = searchResults.filter(result => result.score >= 10); - expect(highQualityResults.length).toBeGreaterThanOrEqual(5); + expect(highQualityResults.length).toEqual(5); // The typo document should not be in results since we have enough exact matches - expect(findNoteByTitle(searchResults, "Typo Test Documnt")).toBeFalsy(); + expect(findNoteByTitle(searchResults, "Documnt Analysis Six")).toBeFalsy(); }); it("should use exact match scoring only in Phase 1", () => { diff --git a/apps/server/src/services/search/services/search.spec.ts b/apps/server/src/services/search/services/search.spec.ts index c8dcc4d8d..773525caa 100644 --- a/apps/server/src/services/search/services/search.spec.ts +++ b/apps/server/src/services/search/services/search.spec.ts @@ -583,16 +583,15 @@ describe("Search", () => { .child(note("Analysis Report")) // Exact match .child(note("Data Analysis")) // Exact match .child(note("Test Analysis")) // Exact match - .child(note("Statistical Analysis")) // Exact match - .child(note("Business Analysis")) // Exact match .child(note("Advanced Anaylsis")) // Fuzzy match (typo) .child(note("Quick Anlaysis")); // Fuzzy match (typo) const searchContext = new SearchContext(); const searchResults = searchService.findResultsWithQuery("analysis", searchContext); - // Should find all matches but exact ones should come first - expect(searchResults.length).toEqual(7); + // With only 3 exact matches (below threshold), fuzzy should be triggered + // Should find all 5 matches but exact ones should come first + expect(searchResults.length).toEqual(5); // Get note titles in result order const resultTitles = searchResults.map(r => becca.notes[r.noteId].title); @@ -607,7 +606,7 @@ describe("Search", () => { (title.includes("Anaylsis") || title.includes("Anlaysis")) ? index : -1 ).filter(index => index !== -1); - expect(exactMatchIndices.length).toEqual(5); + expect(exactMatchIndices.length).toEqual(3); expect(fuzzyMatchIndices.length).toEqual(2); // CRITICAL: All exact matches must appear before all fuzzy matches diff --git a/apps/server/src/services/search/services/search.ts b/apps/server/src/services/search/services/search.ts index da16ea48f..3236b6edb 100644 --- a/apps/server/src/services/search/services/search.ts +++ b/apps/server/src/services/search/services/search.ts @@ -237,6 +237,19 @@ function findResultsWithExpression(expression: Expression, searchContext: Search loadNeededInfoFromDatabase(); } + // If there's an explicit orderBy clause, skip progressive search + // as it would interfere with the ordering + if (searchContext.orderBy) { + // For ordered queries, don't use progressive search but respect + // the original fuzzy matching setting + return performSearch(expression, searchContext, searchContext.enableFuzzyMatching); + } + + // If fuzzy matching is explicitly disabled, skip progressive search + if (!searchContext.enableFuzzyMatching) { + return performSearch(expression, searchContext, false); + } + // Phase 1: Try exact matches first (without fuzzy matching) const exactResults = performSearch(expression, searchContext, false); @@ -251,7 +264,7 @@ function findResultsWithExpression(expression: Expression, searchContext: Search return exactResults; } - // Phase 2: Add fuzzy matching as fallback + // Phase 2: Add fuzzy matching as fallback when exact matches are insufficient const fuzzyResults = performSearch(expression, searchContext, true); // Merge results, ensuring exact matches always rank higher than fuzzy matches @@ -402,6 +415,16 @@ function findResultsWithQuery(query: string, searchContext: SearchContext): Sear return []; } + // If the query starts with '#', it's a pure expression query. + // Don't use progressive search for these as they may have complex + // ordering or other logic that shouldn't be interfered with. + const isPureExpressionQuery = query.trim().startsWith('#'); + + if (isPureExpressionQuery) { + // For pure expression queries, use standard search without progressive phases + return performSearch(expression, searchContext, searchContext.enableFuzzyMatching); + } + return findResultsWithExpression(expression, searchContext); }