Skip to content

search overhaul stage 2: Postgres full-text parity (cloud 'returns nothing' fix) - #30

Closed
andrei-hasna wants to merge 3 commits into
feat/search-stage-1-sqlite-ftsfrom
feat/search-stage-2-pg-fts-parity
Closed

search overhaul stage 2: Postgres full-text parity (cloud 'returns nothing' fix)#30
andrei-hasna wants to merge 3 commits into
feat/search-stage-1-sqlite-ftsfrom
feat/search-stage-2-pg-fts-parity

Conversation

@andrei-hasna

Copy link
Copy Markdown
Contributor

Stage 2 — the top-priority correctness fix. Stacked on #29 (which is on #28).

Problem

Cloud NoteRepo.list matched with title/content ILIKE '%q%' and ordered by created_at DESC. That substring path misses multi-term / word-order-varying queries (returning near-empty results) and never ranks by relevance — so hosted search diverges materially from local.

Fix (src/serve.ts, src/db/pg-migrations.ts)

  • Weighted tsvector generated column on knowledge_items (title = A, content = B) + GIN index.
  • NoteRepo.list now filters with search_vector @@ websearch_to_tsquery('english', $n) and orders by ts_rank_cd(...) (created_at tiebreak). websearch_to_tsquery gives users implicit-AND, phrases, and OR/negation.
  • Migrations are appended to PG_MIGRATIONS (ids are index-derived — never insert mid-array) and are idempotent.

Tests

tests/search-pg-parity.test.ts runs the real NoteRepo against an in-process Postgres (@electric-sql/pglite, devDependency) with the real migrations applied. It asserts:

  • word-order independence ("beta alpha" finds alpha beta ... — empty under the old ILIKE),
  • relevance ranking beats recency (title match outranks a newer body match),
  • quoted-phrase adjacency,
  • total reflects the FTS predicate,
  • sqlite-vs-pg equivalence over the shared corpus.

These fail on the pre-Stage-2 implementation and pass now.

🤖 Generated with Claude Code

…ion reconcile

Add a shared backend-agnostic search corpus and characterization tests that
pin the current OR-of-prefixes / no-phrase SQLite keyword behavior, so the
follow-up stages produce a reviewable behavior diff. Reconcile the package
version to 0.2.87 (ahead of npm latest 0.2.86; main had drifted to 0.2.81)
and add a CHANGELOG documenting the staged plan. No runtime behavior change.
…tics, pagination

Replace the OR-of-prefixes FTS builder with a real query parser: AND by
default, "quoted phrases", prefix*, explicit OR, and NOT/leading-dash
negation. Add an AND-first / OR-fallback strategy so precise multi-term
queries stay precise while natural-language questions (ask/context.pack)
retain recall instead of returning nothing.

Weight bm25 columns so title/source_uri outrank body. Rebuild chunks_fts
under a new schema v9 migration with the diacritic-folding tokenizer
(porter unicode61 remove_diacritics 2), backfilled losslessly from the
existing stored FTS rows. Add offset pagination to hybridSearch with a
widened fetch window for stable, non-overlapping pages. Malformed MATCH
expressions now degrade to zero keyword hits rather than throwing.

Tests: flip the Stage 0 characterization suite to assert AND/phrase/NOT/
prefix/bm25/diacritic/pagination behavior (fail on the old builder, pass
now); bump schema-version expectations 8 -> 9 across affected suites.
Replace the cloud NoteRepo.list ILIKE-substring + created_at-recency path
with weighted full-text search: a tsvector generated column (title=A,
content=B) plus a GIN index, queried via websearch_to_tsquery and ranked by
ts_rank_cd (created_at tiebreak). This fixes the 'cloud returns nothing'
divergence where multi-term / word-order-varying queries matched no
substring and results were ordered by recency instead of relevance.

Postgres migrations are appended to PG_MIGRATIONS (ids are index-derived, so
never inserted mid-array) and are idempotent.

Add an in-process Postgres parity suite (@electric-sql/pglite devDependency)
that runs the real NoteRepo against the real migrations and asserts
word-order independence, relevance-over-recency, phrase adjacency, correct
totals, and sqlite-vs-pg equivalence over the shared corpus.
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

Resolved via #32 (merged to main, commit efb615d).

This PR was auto-closed by GitHub when its base branch feat/search-stage-1-sqlite-fts was deleted on the merge of Stage 1 (#29 → main, 15:11). GitHub refuses to reopen a PR whose base branch is gone. Stage 2 (the Postgres "cloud returns nothing" parity fix) had not landed on main.

I rebased the identical Stage-2 change fresh onto current main (which now carries Stages 0+1 via #29), verified it locally (15/15 search + PG-parity tests pass against real pglite; full suite green apart from pre-existing 5s-timeout flakes that also fail on origin/main and pass at --timeout 30000; tsc clean; secrets scan clean), and merged it as #32. No behavior change vs this PR's diff — same tsvector generated column + GIN index + websearch_to_tsquery/ts_rank_cd path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant