fix(fts): index-time prefix ladder + drop query-time :* (OPS-2314) - #158
Draft
arreyder wants to merge 1 commit into
Draft
fix(fts): index-time prefix ladder + drop query-time :* (OPS-2314)#158arreyder wants to merge 1 commit into
arreyder wants to merge 1 commit into
Conversation
Index the prefix ladder of each bare sub-token in normalizeVectorDocs so a trailing partial search term matches as an exact single-posting-list lexeme, then drop the `|| ':*'` prefix operator from buildSearchQuery. This removes the unbounded GIN posting-list union that PR #139's query-time `:*` introduced (~53s -> 102ms on prod ClickHouse data) while preserving IGA-1440 recall. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
DEPLOYMENT ORDERING IS LOAD-BEARING — DO NOT MERGE-AND-SHIP AS ONE STEP.
The tsvector is generated at row-write time by this generated code and stored in the row. Existing rows keep their old (
:*-dependent) vectors until they are re-marshaled. The index-side prefix ladder must ship AND all FTS tsvector columns must be backfilled / re-indexed BEFORE the query-side:*removal reaches prod. Removing:*against un-backfilled rows silently drops trailing-partial recall (typeahead stops matching) with no error.Recommended: split this into two mergeable PRs.
normalizeVectorDocsprefix ladder, release it, then backfill / re-index every FTS tsvector column.buildSearchQuery:*removal.This draft is the combined proof that the two-sided change is correct and green end-to-end. It is intentionally NOT for merge as-is.
Summary
Fixes the FTS trailing-term prefix explosion (OPS-2314) by moving prefix expansion from query time to index time:
normalizeVectorDocs,pgdb/v1/fts.go): emit the prefix ladder of each bare sub-token (securitygroup→s, se, sec, …), weightedlowerWeight(doc.Weight)to match existing gram weighting.buildSearchQuery,pgdb/v1/fts.go): drop|| ':*'from both the non-stemmed and stemmed branches. The trailing term is now an exact single-posting-list lexeme lookup.TestSearchTrailingTermNoPrefixExplosion): asserts the emitted tsquery contains no:*.Root cause
to_tsquery('simple', ? || ':*')prefix match on the trailing search term.:*forces GIN to union the posting lists of every lexeme sharing the prefix (prod→ prod, production, product, prod-xyz, …). For a common prefix this is an unbounded union → GBs scanned.Why the naive fix does NOT work
The obvious one-liner — just drop
:*from the query — was tested against real Postgres and REGRESSES IGA-1440. The premise that the index already stores a bare per-word prefix ladder is false: the index emits full sub-token lexemes + 3-char sliding-window grams + compound-prefix ladders, but not the bares, se, sec, …ladder. So with:*removed and no index change,TestSearchMultiDotPrefixfails on exactly the partial-trailing cases:The two-sided change (this PR) makes the bare ladder exist at index time, so exact-match recall is preserved and the posting-list union is eliminated.
Caveats (from the validated prototype writeup)
tsvectorMaxMegabytescap truncates lexemes when exceeded, which could drop tail lexemes on very long values. Measure vector-size delta on representative large tenants before shipping the backfill; consider capping ladder length if bloat is significant. Exact-match query cost is unaffected by ladder length.ENGLISH_LONG/EXACTbehavior change. These types intentionally get no grams.buildSearchQueryis field-type-agnostic, so after:*removal a trailing partial on anENGLISH_LONG/EXACTfield matches only full words (no partial typeahead). Confirm no product surface relies on trailing-prefix typeahead over long-text / exact fields.v.value[0:i]byte slicing (matching the existingsymbolsFullTokensSplitDocprefix block). On multibyte sub-tokens this can split a rune (harmless byte-fragment lexemes; fuzz seeds pass, Postgres tolerates). Sub-tokens are lowercased identifier-ish text so risk is low; a rune-aware slice is cheap hardening for the real PR.Test results
Links
:*🤖 Generated with Claude Code