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 9c178526a..d727c64f5 100644 --- a/apps/server/src/services/search/services/progressive_search.spec.ts +++ b/apps/server/src/services/search/services/progressive_search.spec.ts @@ -81,7 +81,7 @@ describe("Progressive Search Strategy", () => { expect(findNoteByTitle(searchResults, "Anaylsis Three")).toBeTruthy(); }); - it("should merge exact and fuzzy results with exact matches ranked higher", () => { + it("should merge exact and fuzzy results with exact matches always ranked higher", () => { rootNote .child(note("Analysis Report")) // Exact match .child(note("Data Analysis")) // Exact match @@ -93,26 +93,26 @@ describe("Progressive Search Strategy", () => { expect(searchResults.length).toBe(4); - // First two results should be exact matches with higher scores - const exactMatches = ["Analysis Report", "Data Analysis"]; - const fuzzyMatches = ["Anaylsis Doc", "Statistical Anlaysis"]; - - // Find exact and fuzzy match results - const exactResults = searchResults.filter(result => - exactMatches.includes(becca.notes[result.noteId].title) - ); - const fuzzyResults = searchResults.filter(result => - fuzzyMatches.includes(becca.notes[result.noteId].title) - ); - - expect(exactResults.length).toBe(2); - expect(fuzzyResults.length).toBe(2); - - // Exact matches should have higher scores than fuzzy matches - const lowestExactScore = Math.min(...exactResults.map(r => r.score)); - const highestFuzzyScore = Math.max(...fuzzyResults.map(r => r.score)); + // Get the note titles in result order + const resultTitles = searchResults.map(r => becca.notes[r.noteId].title); - expect(lowestExactScore).toBeGreaterThan(highestFuzzyScore); + // Find positions of exact and fuzzy matches + const exactPositions = resultTitles.map((title, index) => + title.toLowerCase().includes("analysis") ? index : -1 + ).filter(pos => pos !== -1); + + const fuzzyPositions = resultTitles.map((title, index) => + (title.includes("Anaylsis") || title.includes("Anlaysis")) ? index : -1 + ).filter(pos => pos !== -1); + + expect(exactPositions.length).toBe(2); + expect(fuzzyPositions.length).toBe(2); + + // CRITICAL: All exact matches must come before all fuzzy matches + const lastExactPosition = Math.max(...exactPositions); + const firstFuzzyPosition = Math.min(...fuzzyPositions); + + expect(lastExactPosition).toBeLessThan(firstFuzzyPosition); }); it("should not duplicate results between phases", () => { diff --git a/apps/server/src/services/search/services/search.spec.ts b/apps/server/src/services/search/services/search.spec.ts index 18b793faf..c8dcc4d8d 100644 --- a/apps/server/src/services/search/services/search.spec.ts +++ b/apps/server/src/services/search/services/search.spec.ts @@ -578,7 +578,7 @@ describe("Search", () => { expect(searchResults.length).toEqual(10); }); - it("progressive search prioritizes exact matches over fuzzy matches", () => { + it("progressive search always puts exact matches before fuzzy matches", () => { rootNote .child(note("Analysis Report")) // Exact match .child(note("Data Analysis")) // Exact match @@ -591,27 +591,30 @@ describe("Search", () => { const searchContext = new SearchContext(); const searchResults = searchService.findResultsWithQuery("analysis", searchContext); - // Should find all matches but exact ones should rank higher + // Should find all matches but exact ones should come first expect(searchResults.length).toEqual(7); - // First 5 results should be exact matches with higher scores - const topResults = searchResults.slice(0, 5); - const bottomResults = searchResults.slice(5); - - const topTitles = topResults.map(r => becca.notes[r.noteId].title); - const bottomTitles = bottomResults.map(r => becca.notes[r.noteId].title); - - // All top results should be exact matches - expect(topTitles.every(title => title.toLowerCase().includes("analysis"))).toBeTruthy(); + // Get note titles in result order + const resultTitles = searchResults.map(r => becca.notes[r.noteId].title); - // Bottom results should be fuzzy matches - expect(bottomTitles.some(title => title.includes("Anaylsis") || title.includes("Anlaysis"))).toBeTruthy(); - - // Verify score ordering - const lowestExactScore = Math.min(...topResults.map(r => r.score)); - const highestFuzzyScore = Math.max(...bottomResults.map(r => r.score)); + // Find all exact matches (contain "analysis") + const exactMatchIndices = resultTitles.map((title, index) => + title.toLowerCase().includes("analysis") ? index : -1 + ).filter(index => index !== -1); - expect(lowestExactScore).toBeGreaterThan(highestFuzzyScore); + // Find all fuzzy matches (contain typos) + const fuzzyMatchIndices = resultTitles.map((title, index) => + (title.includes("Anaylsis") || title.includes("Anlaysis")) ? index : -1 + ).filter(index => index !== -1); + + expect(exactMatchIndices.length).toEqual(5); + expect(fuzzyMatchIndices.length).toEqual(2); + + // CRITICAL: All exact matches must appear before all fuzzy matches + const lastExactIndex = Math.max(...exactMatchIndices); + const firstFuzzyIndex = Math.min(...fuzzyMatchIndices); + + expect(lastExactIndex).toBeLessThan(firstFuzzyIndex); }); diff --git a/apps/server/src/services/search/services/search.ts b/apps/server/src/services/search/services/search.ts index fbc4c3ae4..da16ea48f 100644 --- a/apps/server/src/services/search/services/search.ts +++ b/apps/server/src/services/search/services/search.ts @@ -317,11 +317,8 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S // Add fuzzy results that aren't already in exact results const additionalFuzzyResults = fuzzyResults.filter(result => !exactNoteIds.has(result.noteId)); - // Combine results with exact matches first, then fuzzy matches - const combinedResults = [...exactResults, ...additionalFuzzyResults]; - - // Sort combined results by score - combinedResults.sort((a, b) => { + // Sort exact results by score (best exact matches first) + exactResults.sort((a, b) => { if (a.score > b.score) { return -1; } else if (a.score < b.score) { @@ -336,7 +333,24 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S return a.notePathArray.length < b.notePathArray.length ? -1 : 1; }); - return combinedResults; + // Sort fuzzy results by score (best fuzzy matches first) + additionalFuzzyResults.sort((a, b) => { + if (a.score > b.score) { + return -1; + } else if (a.score < b.score) { + return 1; + } + + // if score does not decide then sort results by depth of the note. + if (a.notePathArray.length === b.notePathArray.length) { + return a.notePathTitle < b.notePathTitle ? -1 : 1; + } + + return a.notePathArray.length < b.notePathArray.length ? -1 : 1; + }); + + // CRITICAL: Always put exact matches before fuzzy matches, regardless of scores + return [...exactResults, ...additionalFuzzyResults]; } function parseQueryToExpression(query: string, searchContext: SearchContext) {