From cd18081f4ae984acabf28a3444f98436eb460145 Mon Sep 17 00:00:00 2001 From: Time Attakc <89218912+time-attack@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:28:33 -0700 Subject: [PATCH] fix(takes): keyword search matches words in long claims via word_similarity (#3267) (#3333) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both engines' searchTakes used whole-string trigram similarity (claim % query), which structurally cannot pass the 0.3 threshold for a short keyword against a 100-200 char claim — keyword search returned zero results on real brains. Switch the predicate to word similarity (query <% claim) and rank by word_similarity(query, claim), in both postgres-engine and pglite-engine per the engine-parity invariant. Holder allow-list and source-scope filters unchanged. Regression test: single-word query must match a long claim containing it (fails under the old predicate). Fixes #3267 Co-authored-by: Garry Tan Co-authored-by: Claude Fable 5 --- src/core/pglite-engine.ts | 4 ++-- src/core/postgres-engine.ts | 4 ++-- test/takes-engine.test.ts | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) 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', () => {