Recall wide, score narrow
Semantic search is good at recall and bad at exact names; BM25 is the opposite. Running BM25 only over the semantic candidates gets both without scoring 2.67M documents lexically on every query.
Entity resolution for company names across 2.67M company and subsidiary records: semantic recall, lexical rescoring, rule boosts, and an LLM plus a person only for the hard cases.
The same company shows up as an acronym, a spelling variant, a legal-suffix variant, or one of its subsidiaries, depending on the dataset. That breaks joins and every SQL query built on them. Exact matching misses real matches; loose fuzzy matching merges companies that are different.
01
Names are normalized and legal suffixes stripped. Multi-word names also get an acronym token, so a short form can match the full name.
02
All queries in a batch are embedded at once and searched against a FAISS index of about 2.67M company and subsidiary records. Candidates are over-fetched at ten times top-k, at least 50, so the lexical step has room to work.
03
BM25 runs over the semantic candidates rather than the whole corpus, which keeps lexical scoring cheap at this scale. Ineligible records drop out, and the score is 0.6 semantic plus 0.4 normalized BM25.
04
Fixed boosts cover the cases embeddings get wrong: an exact clean match (+0.3), a prefix match (+0.15), the query equal to the candidate's acronym (+0.5), and the parent name contained in the candidate (+0.2).
05
A master record within 0.20 of the top score is promoted to first place, and a winning subsidiary is rolled up to its parent company.
06
A score of 0.85 or more is accepted without any model call. Below that, an LLM decision agent chooses among the candidates, and when the top two stay within 0.05 of each other the pipeline pauses for a person.
Semantic search is good at recall and bad at exact names; BM25 is the opposite. Running BM25 only over the semantic candidates gets both without scoring 2.67M documents lexically on every query.
Cost and auditability go together: most names resolve with arithmetic, some need a model, a few need a person.
The engine was later extracted into a standalone service (dense HNSW vectors plus sparse BM25 behind FastAPI), so the multi-agent app no longer loads the embedding stack and degrades to a stub when the service is down.
Python · sentence-transformers (MiniLM) · FAISS · BM25 · RapidFuzz · LangGraph · Pydantic · Milvus · FastAPI