diff --git a/src/main/ai-vault-search/session-search-engine.test.ts b/src/main/ai-vault-search/session-search-engine.test.ts index 2334f217969..f52213857c2 100644 --- a/src/main/ai-vault-search/session-search-engine.test.ts +++ b/src/main/ai-vault-search/session-search-engine.test.ts @@ -66,6 +66,18 @@ describe('the route ladder tries phrase, then AND, then repair, then OR', () => expect(ids(result).sort()).toEqual(['1', '2']) }) + it('keeps the stop words a repaired prose phrase was typed with', async () => { + const { db, engine } = await open('ss-engine-typo-phrase') + // Two copies, so the repair only suggests a term the index really holds. + addSyntheticSession(db, { id: 1, text: 'relay is dropping frames' }) + addSyntheticSession(db, { id: 2, text: 'dropping frames again here' }) + // Repairing the body alone would re-plan `relay dropping frames`, which no + // phrase in the index can match, and the answer would fall to AND. + const result = engine.search({ query: 'relay is droppng frames' }) + expect(result.planner.route).toBe('typo+phrase') + expect(ids(result)).toEqual(['1']) + }) + it('keeps every term a repaired literal was typed with', async () => { const { db, engine } = await open('ss-engine-typo-literal') addSyntheticSession(db, { id: 1, text: 'parseJson the data' }) diff --git a/src/main/ai-vault-search/session-search-retrieval.ts b/src/main/ai-vault-search/session-search-retrieval.ts index 8c6f2c183a9..f4a963d37b3 100644 --- a/src/main/ai-vault-search/session-search-retrieval.ts +++ b/src/main/ai-vault-search/session-search-retrieval.ts @@ -180,20 +180,28 @@ export class SessionSearchRetrieval { ): SessionSearchQueryPlan | null { const typoRepair = this.typoRepair let changed = false - const body = plan.body.map((term) => { + // Only the body is a candidate for a correction, but the re-plan is fed the + // tokens as typed: re-planning the body alone would hand the phrase rung a + // sentence with its stop words already gone, and `relay dropping frames` + // cannot match the `relay is dropping frames` that is in the transcript. + const repairable = new Set(plan.body.map((term) => term.toLowerCase())) + const phrase = plan.phrase.map((token) => { + if (!repairable.has(token.toLowerCase())) { + return token + } // Repaired inside the scope the search will run in, so a spelling only // tool output carries neither suppresses a repair nor becomes one. - const fix = typoRepair.correct(term, scope) - if (fix && fix !== term.toLowerCase()) { + const fix = typoRepair.correct(token, scope) + if (fix && fix !== token.toLowerCase()) { changed = true return fix } - return term + return token }) // The repair changes spellings, not the query's character: the re-plan is // told what the original decided so a corrected literal keeps every term it // was typed with. - return changed ? planSessionSearchQuery(body.join(' '), plan.literal) : null + return changed ? planSessionSearchQuery(phrase.join(' '), plan.literal) : null } /**