Conversation
Add the `name_joined` keyword set: every matchable name of an entity with
its parts' comparable forms concatenated. It is the exact bridge for merged
or split tokens ("alqaeda" / "Al Qaeda"), which the trigram sub-field on
`names` used to approximate at 20-1000x the candidate breadth.
Remove `name_phonetic` and the `names.ngrams` sub-field with its analyzer.
Metaphone is never the only channel that reaches a variant pair and is
unavailable for the scripts where a cross-script bridge is needed; trigrams
at 70% overlap do not tolerate typos in name-length tokens (0% overlap on
john/jon). The indexer skips metaphone computation altogether.
Bump INDEX_VERSION to 019: the mapping changed, so deploying requires a full
reindex.
Refs #1253, #1254
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Rewrite `names_query` so that each unique comparable part of the query's names contributes exactly one `dis_max` clause, combining three channels: an IDF-scored exact `term` on `name_parts`, a `fuzzy` clause (AUTO, prefix_length 1, max_expansions 200, boost 0.7; only when MATCH_FUZZY is on and the part has at least three characters) and a `dis_max` of `term`s on `name_symbols` at boost 0.9 for the known-name identities rigour tagged on the part. One `terms` clause on `name_joined` covers all space-less forms of the query names. Weak aliases are queried as before. A document therefore scores each query part once, whichever channel matched it and however many of its aliases contain it, and documents rank by how many query parts they cover weighted by part rarity. This removes the alias penalty that the BM25 `match names` clause imposed on heavily merged records, and it stops trigram and text-field matches from filling the candidate window ahead of near-misses. Gone with it: `representative_names`, the `SYMBOL_BOOSTS` table (IDF already ranks org-class and location parts low), the `is_short` rule, query-time name consolidation (the one place query and index analysis differed), the `names` text clause, the n-gram clauses and the phonetic channel. Clause count is bounded by MAX_PARTS=100 and MAX_SYMBOLS_PER_PART=30, well under the ES default clause limit; primary names are visited before aliases so a truncated query keeps its primary name whole. Refs #1252, #1253, #1254 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With names scored per part, an exact two-part match on common parts is worth about 20 points, while a year-only birth date at boost 3.0 scored 25 on its own. Over a thousand records share any given birth year, so on the US Congress positives fixture they filled the candidate window ahead of the exact name match (recall@50 98.6% -> 86.2%). At boost 1.0 a shared year adds to a name match but cannot carry a candidate past one; the fixture is back at 98.6%, the other fixtures are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Elasticsearch rewrites a `fuzzy` clause into an OR of the expanded terms and sums the matching ones per document, so a record carrying ten spellings of one name part scored ten times for that single query part: the alias bonus this query is meant to remove. "Andrea Salinas" lost its exact two-part match (23 points) to a record with ten transliterations of Andrey (40 points) for the part "andrea" alone. With `rewrite: constant_score` a fuzzy hit is worth FUZZY_BOOST once, whichever and however many neighbours matched. The boost moves from a fraction of the exact score to an absolute 6.0, about the exact score of a part shared by 25,000 records, so an exact hit on any but the most common tokens still outranks an approximate one. Measured on the five positives fixtures, boosts 6, 7 and 8 give identical recall@50 and no fixture falls below the previous query; 6 is the lowest such value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The `constant_score` rewrite enumerates every term within the edit budget instead of the top max_expansions, which made the many-part queries three to five times slower (names-ermakov-20: 57 -> 316 ms ES took). Wrapping the bare fuzzy clause in a `constant_score` filter keeps the bounded expansion and still scores the hit once: 36 ms on the same query, on par with or below the summed clause. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Member
Author
|
Underlying prompt: I feel like the
Other failure modes:
For context, see existing work in the name matching benchmarks, and these issues:
The goal here is not to come up with a more complex, but with a really simple, explainable, error-reistant and reliable logic. Other rules:
Let's leave the weak alias query logic untouched, that's not a goal for this. |
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.
Rebuilds how
/matchretrieves name candidates from Elasticsearch: one rigour normaliser on both sides, one unit of evidence per name part, and four explicitly named ways a query part may reach an indexed one. Closes #1252, closes #1253, closes #1254.Index
build_indexable_entity_docnow writes a third derived name field,name_joined: for every matchable name, the comparable forms of its parts concatenated without spaces (alqaeda,vladimirputin). It is a plain keyword set excluded from_source, likename_partsandname_symbols. Thename_phoneticfield and thenames.ngramstrigram sub-field (with itsosa-ngram-filter/osa-ngram-analyzer) are removed; the indexer no longer computes metaphones. Thenamestext field and itsweak_length_normsimilarity stay for/search;name_partskeeps its normalizer because weak aliases share the field. Field sizes on the fulldefaultcollection:name_joined89 MB added,name_phonetic28 MB andnames.ngrams117 MB removed; total store size 2,645 MB vs 2,657 MB.Query
names_querybuilds onedis_max(tie_breaker 0) per unique comparable name part across all of the query's names, taken from the same cachedentity_names(entity, is_query=True)the scorer uses, so twenty transliterations of one name yield about as many clauses as one name has parts. Inside eachdis_maxare up to three channels; a document scores each query part once, whichever channel matched and however many aliases carry it, and the outershouldsums over parts, so a document ranks by how many query parts it covers, weighted by part rarity. Onetermsclause onname_joinedcovers all space-less forms of the query names; weak aliases are queried exactly as before.The four channels, in plain words:
term.AUTOof the query part (0 edits up to 2 chars, 1 edit at 3–5, 2 edits from 6) with the first letter fixed; only whenMATCH_FUZZYis on and the part has at least three characters. Run as aconstant_scorefilter so a record with ten spellings of one part scores the hit once, worth a fixedFUZZY_BOOST = 6.0(about the exact score of a part shared by 25,000 records, so an exact hit on any but the most common tokens outranks an approximate one).dis_maxofterms onname_symbols, boost 0.9. The only bridge for scripts that are not latinised (Arabic, Han) and for variants beyond two edits (alexander/aleksandr).termsonname_joined.Caps:
MAX_PARTS = 100,MAX_SYMBOLS_PER_PART = 30bound the query at roughly 3,200 clauses (observed maxima: 22 parts, 29 symbols on one part). Primary names are visited before aliases so a truncated query keeps its primary name whole.Two calibrations of clauses outside the name channels were needed once the ×3
match namesclause was gone:TYPE_BOOSTS[date]goes from 3.0 to 1.0, because a year-only birth date at 3.0 scored more than an exact two-part name match and over a thousand records share any given year, so they filled the candidate window on their own (US Congress fixture: 98.6% → 86.2% before this change, 98.7% after). The fuzzy clause is wrapped inconstant_scorebecause a bare fuzzy clause is rewritten into an OR of its expansions and sums the ones a document carries, which handed heavily aliased records the very alias bonus this query removes ("Andrea Salinas" lost to a record with ten transliterations of Andrey scoring 40 on "andrea" alone). Using theconstant_scorerewrite instead would have been 3–5× slower on many-part queries because it enumerates every term in the edit budget rather than the top 200.Recall@50, before/after
Presence of the expected id(s) in a 50-candidate window; router-identical query construction (
entity_query, filter_op OR,DEFAULT_SORTS,track_total_hits: false). "Before" isorigin/mainagainst the 018 index ofdefault, "after" is this branch against a 019 index of the same collection built with this code (a slightly newer data release: 4,049,275 vs 4,049,113 documents). Fixtures fromcontrib/validation_report/build/fixtures.Treated UN fixture by whether the name was mangled: name changed 238, 96.2% → 96.2%; name unchanged 494, 96.8% → 96.8%. The 25 UN misses are shared by both queries (entities outside the window for both).
Misses that changed:
namestext field.rimaldiV nituPandVladimir nitPu(reversed strings, only trigrams could match these),Kim Yong-chol→ Kim Yong-chun twice (two edits on a four-letter syllable, outsideAUTO),عبد الفتاح السري→ السيسي (Arabic single-part edit, first letter fixed and no NAME symbol).Latency
contrib/perf_bench/perf_bench.py run --output …from both trees, same index builds as above, 100 queries, medians of 3 warm runs.names-ermakov-20(22 unique parts, one fuzzy clause each) is the one query that got materially slower;dense-stroygazis the other. Hamas and Hezbollah now return 36 and 12 candidates rather than a window padded with trigram matches; the numeric-only company name12345returns nothing instead of 50 trigram neighbours.Known limits
entity_idorder decides. Previously BM25 on alias count decided. Whetherentity_values_countshould break ties for/matchis a separate decision.vladimirputinagainst an entity that only has "Vladimir Vladimirovich Putin") is not reached;name_joinedis exact.AUTOwith a fixed first letter.names.ngramsremoval drops the reversed-string and split-token cases that trigrams happened to catch.Removed, and why
representative_names(clause count no longer depends on alias count),SYMBOL_BOOSTS(IDF already ranks org-class and location parts near zero), theis_shortrule (short parts get exact and symbol channels;AUTOgives 0 edits under 3 chars anyway),Name.consolidate_namesat query time (the one place query and index analysis differed), thematch namesclause and its ×3 boost (the alias penalty of #1252), the n-gram clauses and analyzer (#1254; trigrams at 70% have 0% overlap on john/jon and admit 20–1000× the documents), the phonetic channel (never the only bridge for a variant pair on the fixtures, unavailable for the scripts where a cross-script bridge is needed),NAME_PHONETIC_FIELDandNAME_NGRAMS_FIELD.Operational
INDEX_VERSIONis bumped to019: the mapping changed, so deploying this triggers a full reindex of every dataset, and the old query code cannot serve from a 019 index nor the new code from an 018 one.YENTE_MATCH_FUZZYnow toggles only the fuzzy sub-clause; exact, symbol and joined matching are always on, so merged names ("alexandervyacheslavovichzakharov") are retrieved regardless of the setting.Tests
tests/test_queries.pyis new: onedis_maxper unique part across seven transliterations, fuzzy sub-clause shape and absence whenMATCH_FUZZYis off or the part is under three characters, symbol sub-clause presence and cap, joined clause contents, weak alias clause, the 100-part cap, and primary-before-alias ordering.test_mappings.pyassertsname_joinedis indexed (andname_phoneticis gone);test_match_name_without_spacesnow asserts the merged name works with fuzzy on and off.ruff,ruff formatandmypy --strictpass; the full suite passes except the four pre-existinghttpx_mockfixture errors intest_loader.py/test_updater.py(pytest-httpx not installed in the local environment).🤖 Generated with Claude Code