Queries with a part number or a clause reference come back empty
In short
An identifier carries almost no meaning for an embedding to preserve, so its vector is dominated by the words around it and two codes differing by one character land in the same neighbourhood. Confirm it by scanning the raw corpus for the identifier as an exact substring, then choose between a lexical arm and an extracted identifier field.
Key takeaways
- An embedding compresses meaning. An identifier has none to compress, so similarity ranks its neighbours by context.
- Scan the raw corpus for the exact string first. Present in the file and absent from results is a decisive finding.
- Subword tokenisation splits a code into fragments thousands of other codes share.
- Enumerable, well-formed identifiers get an extracted field and an exact filter. Open-ended ones get a lexical arm.
- Normalise case, separators and padding on both sides, in one shared function, or you will fix half the queries.
Identifiers are the one query class where embedding similarity is structurally the wrong instrument, and nothing in the retrieval stack will fix it: not a better model, not a larger k, not a reranker. An embedding compresses meaning into a fixed-length vector. A part number has no meaning to compress. What the vector ends up encoding is the words that happened to surround the code in the chunk, which is why a search for one code cheerfully returns passages about a different one.
Prose queries keep working throughout, which makes this confusing to report. 'What is the warranty period on the pressure regulator' answers fine. 'What is the warranty on ABC-4471-B' returns three paragraphs about a different product line, and the user concludes the system is broken generally.
Confirm it with an exact substring scan
Before any theory, establish whether the string exists in the corpus at all. Ten minutes, and it splits the problem cleanly in two.
- Take 20 identifiers that users have actually searched for and that you know are documented. Pull them from query logs, not from a catalogue.
- Scan the raw source files for each one as a literal substring — grep over the extracted text, or a LIKE query against the text column you indexed. Record present or absent.
- For each identifier present in the raw text, scan the chunk table for the same string. A code present in the file and absent from every chunk means it was destroyed between extraction and indexing.
- For each identifier present in a chunk, run it as a query through the live retrieval path and record whether that chunk appears in the top-k at all.
- Tally the three columns. Where the identifier survives into a chunk and still does not retrieve, you have confirmed a scoring problem rather than a pipeline problem, and the repairs in this page apply.
| In raw text | In a chunk | Retrieved | Diagnosis |
|---|---|---|---|
| Yes | Yes | No | Scoring. Similarity cannot rank the identifier — the case this page addresses |
| Yes | No | No | Pipeline. Cleaning, table handling or chunking removed it before indexing |
| No | No | No | Extraction. The identifier lives in an image, a scan, a header or a field the parser skipped |
| Yes | Yes | Yes, but wrong chunk | Ambiguity. The code appears in several places and nothing ranks the authoritative one first |
The second row sends you somewhere else entirely. Identifiers very often live in exactly the structures that parsing damages — a specification table, a column header, a repeated page header — which is the failure described in the number in a table the assistant cannot find. No amount of retrieval work helps when the string is not in the index.
Why similarity cannot rank an identifier
Four mechanisms, and they compound. Understanding them stops a team spending a quarter on embedding experiments.
- Subword tokenisation shreds the code. Embedding models split rare strings into fragments, so ABC-4471-B becomes a handful of pieces that thousands of other codes also contain. The model never sees the identifier as a unit, and the fragments carry no distinguishing signal.
- A one-character difference is semantically invisible. ABC-4471-B and ABC-4471-C are, to a similarity function, essentially the same point in space. For a reader they are different products with different fittings, and returning the wrong one is worse than returning nothing.
- The query is too short to carry context. A 3-token query gives the encoder almost nothing to work with, so the resulting vector sits in a low-information region and matches whatever is generically nearby — often the most verbose chunk in that neighbourhood.
- Rare strings have unstable representations. A code that appeared a handful of times in training, or never, gets a vector assembled from fragments rather than learned as a concept, and small formatting differences move it unpredictably.
Similarity search answers 'what is this like'. An identifier query asks 'where is exactly this', and those two questions do not have the same instrument.
The four causes worth checking, in order
| Cause | Check | Repair |
|---|---|---|
| Tokenisation splitting the identifier | Confirmed by the scan: present in the chunk, never retrieved, prose queries fine | A lexical arm, or an extracted field. Nothing else touches it |
| Case, separator and padding mismatch | Search the corpus for the same code in 4 forms — hyphenated, spaced, unseparated, lowercase — and count hits per form | One normalisation function applied at index time and at query time |
| Identifier only in a table cell or a heading | Print the chunk containing the code and look at what surrounds it | Carry headers into chunks, or extract the identifier to metadata |
| Cleaning stripped or altered it | Compare the raw extracted text against the stored chunk text for one known code | Fix the cleaning rule; punctuation stripping and whitespace collapsing are the usual culprits |
The second row is the quiet one. A document set written over 10 years will hold the same code as ABC-4471-B, ABC 4471 B, abc4471b and, in one system's export, ABC-04471-B with a zero-padded middle segment. Each is a different string to a keyword index and a slightly different point to an embedding, so a repair that normalises only the query fixes about half the corpus and reads as an intermittent bug afterwards.
The two repairs, and the test that picks one
Both are ordinary engineering. Choosing between them is a property of your identifiers, not a preference.
| Lexical arm | Extracted identifier field | |
|---|---|---|
| What it is | A keyword index — BM25 in Elasticsearch or OpenSearch, or full-text search in Postgres — queried alongside the vector index, with the lists merged | A regex or parser that pulls identifiers out at ingest and stores them as a filterable field on each chunk |
| Use it when | Identifiers are open-ended, inconsistently formatted, or you cannot enumerate the patterns | Identifiers follow one or two well-formed patterns you can match reliably |
| Cost | A second index to keep in sync, plus a fusion step and its tuning | An extraction rule per pattern, and re-indexing when a new pattern appears |
| Failure mode | Analyzer defaults split on hyphens, so the code is fragmented again in the keyword index too | A pattern nobody anticipated is silently never extracted and never matches |
The deciding test takes an hour. Sample 100 identifiers from real queries and try to write a regex that captures them. If a single expression captures 95 percent or more, extract the field: an exact filter on metadata is faster, more precise and easier to explain than any ranking change. If your sample needs 4 expressions and still misses a tail, build the lexical arm instead, because you will not keep 4 patterns current. The argument for running both arms permanently is made in keyword matching versus vector similarity, and merging the two ordered lists is its own decision, covered in rank fusion across two result lists.
One deployment caution on the extracted-field route. An exact filter is a filter, and where it is applied decides whether it helps or hurts: applied inside the search it narrows the space before ranking, applied after a nearest-neighbour search it eats a candidate set that was already fixed at k. That is the same arithmetic that produces recall collapsing the day permission filtering was turned on, and it catches identifier filters just as reliably.
What the answer layer must do with an identifier
Retrieval is only half of it. An identifier is exactly the kind of token a language model reproduces approximately, and an approximate part number is worse than no answer: it looks authoritative and orders the wrong component.
- Require verbatim provenance. Any identifier in the output must appear character for character in a retrieved passage, and this is cheap to check programmatically after generation rather than hoping the prompt held.
- Refuse rather than approximate. If the identifier in the question does not appear in any retrieved passage, the correct output says so. A model asked about a code it never received will supply a plausible one from memory, which is the failure pattern in the model answering from memory instead of the passage.
- Echo the user's identifier back in the answer. It gives the reader a one-glance check that the system understood the code it was asked about, and it makes transposition errors obvious.
- Never re-derive a code from a description. If the passage says the regulator, and the user asked about a specific part number, the answer must not connect them unless the passage does.
One cost detail before you wire the lookup in. If an identifier lookup injects a record into the front of the prompt, it changes the prefix on every request and destroys any cached-prefix saving you relied on. Put stable instructions first and the resolved record after them, for the reason set out in a prefix cache that stopped paying for itself.
Clause references are the harder version of the same problem
A part number is at least globally unique. A clause reference is not: nearly every contract in a set has a section numbered 12.4, and the correct one depends on which agreement is meant. Treat the reference as a search string and you get the most verbose clause 12.4 in the corpus, from a document nobody asked about.
So a clause reference is a two-part key — document identity plus reference — and must be resolved as one. Extract the section number to metadata at ingest, filter by document first and reference second, and if the question names no document, ask rather than guess. Where the clause is the natural unit of retrieval, those boundary decisions are covered in choosing chunk boundaries for contracts.
None of this is exotic work — an extraction rule, a second index or a filter, and a post-generation check — but it sits outside what a general retrieval library gives you by default, and it is usually discovered late. It is the kind of bounded build we scope under AI agents and automation, and who should build it is the subject of choosing an AI development partner. This page sits with retrieval and grounding, inside the wider engineering library.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
Why can't vector search find an exact part number?
Because an embedding stores meaning and an identifier has none to store. The model splits the code into subword fragments that thousands of other codes share, then places the chunk according to the words around it, so a search for one code returns passages about a different one. This is a property of similarity search rather than a defect in a particular model, and changing models does not address it.
Do I need a keyword index, or is a metadata field enough?
Sample 100 identifiers from real queries and try to capture them with one regex. If a single pattern gets 95 percent or more, extract the identifier into a filterable field and use an exact filter — it is faster, more precise and easier to explain. If the sample needs several patterns and still misses cases, build the keyword arm, because a set of patterns nobody maintains fails silently.
Will a reranker fix identifier queries?
No. A reranker reorders a list it is given, so if the chunk containing the identifier is not in the candidate set, reranking cannot recover it. Rerankers help when the right passage is present and ranked low, which is a different failure. Check whether the correct chunk appears anywhere in a wide candidate set before spending anything on ordering.
How should the system handle a clause reference like 12.4?
Treat it as a two-part key: the document plus the reference. Section numbers repeat across every contract in a set, so a reference alone is ambiguous and will resolve to whichever clause happens to rank highest. Extract the section number to metadata at ingest, filter by document first, and if the question does not identify a document, ask which one rather than guessing.
- retrieval
- lexical search
- identifiers
- tokenisation
The work behind this page
Builds from our portfolio that this page draws on.
AskVault
An AI internal knowledge-search platform that answers employee questions from your own docs — grounded in citations, with knowledge gaps surfaced and deflection tracked.
Productivity AIBrief Forge
Contract review AI for solo lawyers and small firms — extract, score, and redline contracts in minutes.
Legal TechRead next
- The number is in a table and the assistant cannot find itThe figure is in the document and the assistant says it cannot find it. The row and its header stopped being connected somewhere between the file and the chunk.diagnostic
- You know the document is indexed and it still never comes backFour layers can quietly drop one document, and they look identical from the outside. Probe them in cost order — existence, filter, exact text, rank — and each probe eliminates exactly one.diagnostic
- The model answered from memory and ignored the passage you gave itPlant a passage that contradicts common knowledge, then ask a question only that passage answers. Whichever the answer follows tells you whether you have an override or a payload that never arrived.diagnostic
- Adding one new source made unrelated answers worseNothing about the old passages changed. A new source ranks moderately well on a great many queries, and a fixed top-k has to give it those slots by taking them from something else.diagnostic
- Chunk overlap: what it protects against and what it duplicatesOverlap is insurance against a chunk boundary landing in the middle of one idea. The premium is paid in duplicated candidates crowding a fixed number of prompt slots.definition
- Facts in the middle of a long context get missedHold the passage set constant, walk the answer-bearing chunk from first position to last, and measure. The shape of the resulting curve is the diagnosis, and it takes about an hour to produce.diagnostic
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation