diff --git a/src/core/pglite-engine.ts b/src/core/pglite-engine.ts index 3b6e422c9..c29b82887 100644 --- a/src/core/pglite-engine.ts +++ b/src/core/pglite-engine.ts @@ -4835,11 +4835,11 @@ export class PGLiteEngine implements BrainEngine { const { rows } = await this.db.query( `SELECT t.id AS take_id, t.page_id, p.slug AS page_slug, t.row_num, t.claim, t.kind, t.holder, t.weight, - similarity(t.claim, $1)::real AS score + word_similarity($1, t.claim)::real AS score FROM takes t JOIN pages p ON p.id = t.page_id WHERE t.active - AND t.claim % $1 + AND $1 <% t.claim AND ($2::text[] IS NULL OR t.holder = ANY($2::text[])) AND ($4::text[] IS NULL OR p.source_id = ANY($4::text[])) AND ($5::text IS NULL OR p.source_id = $5::text) diff --git a/src/core/postgres-engine.ts b/src/core/postgres-engine.ts index b86292b2a..55dbf9aea 100644 --- a/src/core/postgres-engine.ts +++ b/src/core/postgres-engine.ts @@ -4962,11 +4962,11 @@ export class PostgresEngine implements BrainEngine { const rows = await sql` SELECT t.id AS take_id, t.page_id, p.slug AS page_slug, t.row_num, t.claim, t.kind, t.holder, t.weight, - similarity(t.claim, ${query})::real AS score + word_similarity(${query}, t.claim)::real AS score FROM takes t JOIN pages p ON p.id = t.page_id WHERE t.active - AND t.claim % ${query} + AND ${query} <% t.claim AND ( ${opts.takesHoldersAllowList ?? null}::text[] IS NULL OR t.holder = ANY(${opts.takesHoldersAllowList ?? null}::text[]) diff --git a/test/takes-engine.test.ts b/test/takes-engine.test.ts index 826cf41ae..8bf1bbe8b 100644 --- a/test/takes-engine.test.ts +++ b/test/takes-engine.test.ts @@ -100,6 +100,22 @@ describe('searchTakes', () => { const worldHits = await engine.searchTakes('founder', { takesHoldersAllowList: ['world'] }); expect(worldHits.every(h => h.holder === 'world')).toBe(true); }); + + // #3267: whole-string trigram % structurally can't match a short keyword + // against a long claim (similarity between the full strings stays under the + // 0.3 threshold). word_similarity (<%) matches the keyword against the + // best-matching word span instead. + test('single-word keyword matches a long claim containing it (#3267)', async () => { + await engine.addTakesBatch([ + { + page_id: acmePageId, row_num: 50, + claim: 'Acme will consolidate the mid-market vertical SaaS landscape through disciplined acquisitions and a shared billing platform over the next five years', + kind: 'bet', holder: 'garry', weight: 0.6, + }, + ]); + const hits = await engine.searchTakes('consolidate'); + expect(hits.some(h => h.claim.includes('consolidate the mid-market'))).toBe(true); + }); }); describe('updateTake', () => {