Skip to content

support multi-dot partial search via index-side edge-grams - #157

Closed
mj-palanker wants to merge 2 commits into
mainfrom
mjp/fts-symbol-prefix-fix
Closed

support multi-dot partial search via index-side edge-grams#157
mj-palanker wants to merge 2 commits into
mainfrom
mjp/fts-symbol-prefix-fix

Conversation

@mj-palanker

Copy link
Copy Markdown
Contributor

Context

#139 made progressive typing of dotted identifiers work (ad.ph02t1.admin.se → ad.ph02t1.admin.securitygroup) by appending a :* prefix operator to the last search term. On a prefix-dense index like ours (edge-grams mean every word contributes all its prefixes as lexemes), :* forces GIN to range-scan and union the posting lists of every lexeme starting with the term — a trailing common token like prod turned a sub-second search into a 43s query on a 2.1M-row tenant (OPS-2310). #139 is reverted in the PR below this one; this PR restores the multi-dot matching it provided, keeping all queries as exact lexeme lookups.

Root cause of the original miss

jargon's tokenizer merges dotted segments (admin.securitygroup stays one token), so edge-grams were computed on the merged string and a bare sub-token gram like se (prefix of securitygroup) was never indexed. #139 papered over that at query time; this fixes it at write time.

Changes

  • Index side (normalizeVectorDocs): additionally emit edge-grams for each symbol-split sub-token (securitygroup → s, se, sec, …) at lowered weight. Purely additive — new vectors are a strict superset of old ones.
  • Query side (FullTextSearchQuery): build symbol-aware variants of the input — original, symbols→spaces (ad ph02t1 admin se), and symbols-deleted (adph02t1adminse) — each with its stemmed twin, deduped and OR'd as exact websearch_to_tsquery('simple', …) terms. No :* anywhere. Inputs without symbols produce byte-identical queries to pre-fix: prefix match last search term for multi-dot queries #139 behavior.

Deployment properties

  • The joined variant matches joined-token prefix lexemes already present in existing indexes, so from-the-start dotted typing works immediately on old rows — no backfill required.
  • The split variant matches the new sub-token edge-grams on rows written after this change.
  • Known gap: mid-path partials (admin.se) won't match rows indexed before this change until they're rewritten or backfilled. (fix: prefix match last search term for multi-dot queries #139 covered this case at query time; accepted trade-off.)

Testing

mj-palanker and others added 2 commits July 27, 2026 10:10
This reverts commit cf492ec.

The query-time :* prefix operator added by #139 causes pathological GIN
scans: because our fts_data vectors are prefix-dense by design (edge-grams
index every prefix of every word), a trailing prefix term like 'prod':*
must range-scan and union the posting lists of every lexeme starting with
that string. On a large tenant this turned a sub-second search into a 43s
query. Prefix expansion belongs at write time in this schema; a follow-up
restores multi-dot partial matching via index-side sub-token edge-grams
and query-side symbol normalization, without :*.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…:* operator

Replaces the query-time :* prefix matching from #139 (reverted in the
previous commit) with prefix expansion at write time, keeping search
queries as cheap exact lexeme lookups:

- normalizeVectorDocs now emits edge-grams for each symbol-split
  sub-token (at lowered weight). jargon's tokenizer merges dotted
  segments ("admin.securitygroup" stays one token), so grams of bare
  sub-tokens like "se" were never indexed - the actual root cause of
  the multi-dot search misses #139 worked around.

- FullTextSearchQuery builds symbol-aware variants of the input: the
  original text, a symbol->space split form ("ad ph02t1 admin se"),
  and a symbol-deleted joined form ("adph02t1adminse"), each with its
  stemmed counterpart, deduped and OR'd as exact websearch_to_tsquery
  terms. The joined form matches the joined-token prefix lexemes
  already present in existing indexes (no backfill needed); the split
  form matches the new sub-token edge-grams on rows written after
  this change. Inputs without symbols produce byte-identical queries
  to the pre-#139 behavior.

Restores TestSearchMultiDotPrefix from #139 unchanged, and adds unit
tests for the new lexeme emission, the query variants, and a guard
asserting generated queries never contain :*.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mj-palanker

Copy link
Copy Markdown
Contributor Author

We are not moving forward with this, while we believe it could be correct, it's not cleat that is it. There are significant costs needed to re-index the world for this to properly work, and we are likely going to attempt to re-architect this system in some way instead of continue to put out small fires.
if you are a future person looking at this, and you feel so inspired as to pick up this approach and merge it, feel free to do so given that you have audited it and strongly believe it to be correct.

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