fix(search-algorithms): Soundex and offset bugs; add BM25 inverted index - #2
Open
frankstupak wants to merge 1 commit into
Open
fix(search-algorithms): Soundex and offset bugs; add BM25 inverted index#2frankstupak wants to merge 1 commit into
frankstupak wants to merge 1 commit into
Conversation
…~29x) Correctness - Soundex: implement the H/W-separator and vowel-separator rules and first-letter code dedup. Frozen ref failed the canonical reference table (Tymczak T520->T522, Pfister P123->P236, Honeyman H500->H555); now 9/9. - SearchEngine fuzzy/phonetic/wildcard: replace `split(/\s+/)` + `indexOf(word)` with an offset-tracking tokenizer. Fixes (a) punctuation glued to words hurting recall and (b) repeated words all reporting the first occurrence's offset (corrupted highlighting). - ngram search: whole-field Jaccard collapses toward 0 when the query is much shorter than a multi-word field, so short queries never matched real documents. Now also compares per word and keeps the best. Performance - Add InvertedIndex: build postings + DF once, answer BM25/TF-IDF by walking only the query terms' postings instead of re-tokenizing every doc per query. Numerically identical to RankingAlgorithms (0.0 diff over 1750 checks). 1000 docs x 200 queries: BM25 15.36 -> 0.52 ms/query (~29x), TF-IDF ~49x. SearchEngine caches the index and invalidates it on add/update/remove. - Levenshtein: full (m+1)x(n+1) matrix -> rolling 3-row buffer (keeps Damerau transposition). 2003x2003 pair: 32.1MB -> ~48KB allocation, identical result. Build - Declare @fastify/swagger (imported+registered in server.ts but never listed); frozen ref failed `tsc` on the missing module. Tests: +36 (98 total pass). tsc + eslint clean.
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.
What this does
Uplifts
src/api/search-algorithmswith correctness fixes to the phonetic and match-offset logic, a new inverted index that makes ranked search dramatically faster, and a memory-lean edit-distance — plus the tests and benchmarks to back all of it. Public API is unchanged; every ranked result is numerically identical to before.Scoreboard: +36 tests (98 total pass),
tscclean,eslintclean, and BM25 goes ~29x faster on repeated queries.Correctness bugs fixed
Soundex was wrong on the classic reference cases
The frozen implementation skipped vowels/H/W but never applied the separator rules, and never de-duplicated the first letter's own code. It failed the canonical Soundex reference table:
T520T522P123P236PH500H555NsNow 9/9 against the reference table (Robert
R163, RubinR150, AshcraftA261, etc.), and homophones like Smith/Smyth still collapse together as phonetic search relies on. H and W stay transparent (AshcraftA261, notA226); vowels act as separators.Repeated words reported the wrong position
fuzzySearch,phoneticSearch, andwildcardSearchsplit fields withsplit(/\s+/)and recovered positions withfieldValue.indexOf(word). That has two bugs:"TypeScript,"never equals the query"TypeScript"), quietly hurting recall.indexOfalways returns the first occurrence, so every repeat of a word reported the same (wrong) offset — which corrupts highlighting.Replaced with a single offset-tracking Unicode tokenizer. A field containing "word … word … word" now yields three distinct, correct offsets instead of three copies of the first.
N-gram search couldn't find short queries in real documents
ngramSearchcompared the whole field against the query with bigram Jaccard. When the query is much shorter than a multi-word field, their padded bigram sets barely overlap relative to their union, so the score collapses toward zero and never clears the threshold — e.g."JavaScript"vs a 3-word title scored0.36and a one-word title match was effectively unreachable for realistic documents. Now it also compares per word and keeps the best match (whole-field score retained as a floor), so short queries actually match, and the highlighted span is the specific word rather than the entire field.Performance
Inverted index for BM25 / TF-IDF (~29x / ~49x on repeated queries)
RankingAlgorithms.calculateBM25/.calculateTFIDFre-tokenize every document and rebuild the document-frequency table on every query —O(N·L)regardless of how selective the query is. NewInvertedIndexdoes that work once at build time, then answers each query by walking only the query terms' postings.SearchEnginebuilds it lazily and invalidates it onaddItems/updateItems/removeItems.Scores are a drop-in — verified identical to the stateless functions (max abs diff
0.0over 1750 doc×query checks, same top-5 ordering; a repeated query term is counted the same way).Benchmark (1000 docs, 20–60 words each, 200 queries):
End-to-end
SearchEngine.search({algorithm:"bm25"})with the index cached: ~1.0 ms/query.Levenshtein: full matrix → rolling buffer
levenshteinDistanceallocated the full(m+1)×(n+1)matrix. Damerau transposition only needs the row two above the current one, so it's now a rolling 3-row buffer — identical results (verified against an independent full-matrix reference, including transpositions and custom edit costs). On a 2003×2003 pair the allocation drops from ~32.1 MB → ~48 KB with the same output.Build fix
server.tsimports and registers@fastify/swagger, but it was never declared as a dependency — the subproject failedtscon the missing module out of the box. Added it todependencies(matching the version already used byapi-scenarios).Tests
src/uplift.test.tsadds 36 tests: the Soundex reference table, Levenshtein rolling-buffer parity vs an independent full-matrix reference (incl. transposition + custom costs),InvertedIndexBM25/TF-IDF equivalence + incrementaladdDocuments, andSearchEngineoffset/tokenization/n-gram/index-invalidation coverage. All 62 original tests still pass — 98 total.tscandeslintare clean.— Lumen Industries