feat(quick_search): make sure that we rank exact matches higher when merging results with fuzzy search

This commit is contained in:
perf3ct 2025-08-03 21:29:18 +00:00
parent db1619af31
commit 583ab8dc92
No known key found for this signature in database
GPG Key ID: 569C4EEC436F5232
3 changed files with 61 additions and 44 deletions

View File

@ -81,7 +81,7 @@ describe("Progressive Search Strategy", () => {
expect(findNoteByTitle(searchResults, "Anaylsis Three")).toBeTruthy(); 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 rootNote
.child(note("Analysis Report")) // Exact match .child(note("Analysis Report")) // Exact match
.child(note("Data Analysis")) // Exact match .child(note("Data Analysis")) // Exact match
@ -93,26 +93,26 @@ describe("Progressive Search Strategy", () => {
expect(searchResults.length).toBe(4); expect(searchResults.length).toBe(4);
// First two results should be exact matches with higher scores // Get the note titles in result order
const exactMatches = ["Analysis Report", "Data Analysis"]; const resultTitles = searchResults.map(r => becca.notes[r.noteId].title);
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));
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", () => { it("should not duplicate results between phases", () => {

View File

@ -578,7 +578,7 @@ describe("Search", () => {
expect(searchResults.length).toEqual(10); 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 rootNote
.child(note("Analysis Report")) // Exact match .child(note("Analysis Report")) // Exact match
.child(note("Data Analysis")) // Exact match .child(note("Data Analysis")) // Exact match
@ -591,27 +591,30 @@ describe("Search", () => {
const searchContext = new SearchContext(); const searchContext = new SearchContext();
const searchResults = searchService.findResultsWithQuery("analysis", 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); expect(searchResults.length).toEqual(7);
// First 5 results should be exact matches with higher scores // Get note titles in result order
const topResults = searchResults.slice(0, 5); const resultTitles = searchResults.map(r => becca.notes[r.noteId].title);
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();
// Bottom results should be fuzzy matches // Find all exact matches (contain "analysis")
expect(bottomTitles.some(title => title.includes("Anaylsis") || title.includes("Anlaysis"))).toBeTruthy(); const exactMatchIndices = resultTitles.map((title, index) =>
title.toLowerCase().includes("analysis") ? index : -1
// Verify score ordering ).filter(index => index !== -1);
const lowestExactScore = Math.min(...topResults.map(r => r.score));
const highestFuzzyScore = Math.max(...bottomResults.map(r => r.score));
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);
}); });

View File

@ -317,11 +317,8 @@ function mergeExactAndFuzzyResults(exactResults: SearchResult[], fuzzyResults: S
// Add fuzzy results that aren't already in exact results // Add fuzzy results that aren't already in exact results
const additionalFuzzyResults = fuzzyResults.filter(result => !exactNoteIds.has(result.noteId)); const additionalFuzzyResults = fuzzyResults.filter(result => !exactNoteIds.has(result.noteId));
// Combine results with exact matches first, then fuzzy matches // Sort exact results by score (best exact matches first)
const combinedResults = [...exactResults, ...additionalFuzzyResults]; exactResults.sort((a, b) => {
// Sort combined results by score
combinedResults.sort((a, b) => {
if (a.score > b.score) { if (a.score > b.score) {
return -1; return -1;
} else if (a.score < b.score) { } 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 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) { function parseQueryToExpression(query: string, searchContext: SearchContext) {