POST
Your search problem is not solved by vectors
Aug 20266 MIN READ
Somewhere along the way, vector search became the default answer to every search problem, and a lot of teams discovered the hard way that it has a blind spot you can drive a truck through. Ask a vector index for the exact string "TQ-8842" and it will shrug. Ask it for a phrase that appears verbatim in one document and a paraphrase in another, and it might return the paraphrase, because embeddings love paraphrase and could not care less about your exact phrase.
The search features that survive contact with real users are the ones that refuse to pick a single approach. They run the keyword path and the vector path, and they merge the results. That combination has a name, hybrid search, and it is not a feature you add when you have time. For most products it is the baseline.
Why keyword search refuses to die
The word "keyword" got a bad reputation, but the underlying technique, lexical or full text search, is doing something no embedding model does reliably: exact and near exact matching. When a user types an error code, a product name, a citation, a string of digits, they are not expressing a concept. They are quoting an identifier, and the only correct behavior is to find that identifier, not a semantically similar thing.
BM25, the classic scoring function, and Postgres full text search, its practical sibling, are good at this. They tokenize, they count term overlap, they weight rarity, and they return the documents that actually contain the words. They are also fast, cheap to run, and fully deterministic, which makes them testable in a way embeddings are not.
Their weakness is the mirror image of the embedding weakness. Two documents about the same concept with no shared vocabulary will not match each other. The user asks "how do I cancel my subscription" and the document that says "you can end your plan at any time" shares no tokens with the query. Lexical search returns nothing. This is the gap that embeddings fill, and it is exactly why the two approaches are complements, not competitors.
The demo case is not the production case
Here is the failure pattern I keep seeing. A demo shows off vector search on a corpus of long, well written documents, and the results look amazing, because long well written documents embed well. Then production gets the messy reality: tickets, logs, user generated content, product data, code. And the queries are not elegant sentences. They are fragments, identifiers, and misspelled names.
A real search bar receives things like "safepass 2fa not work", "err 5008", and "the thing with the blue icon". The misspelled "safepass" is a token overlap problem that embeddings handle poorly and fuzzy lexical matching handles fine. The error code is an exact match problem that only lexical search handles. The blue icon thing is a concept problem that embeddings handle well. No single path covers all three.
Merging the paths: reciprocal rank fusion
So you run both searches and you have two lists of results. Now what? You could interleave them, which is naive. You could normalize scores and add them, which breaks because the two score distributions are not comparable.
The technique that works well in practice is reciprocal rank fusion. For each result, you take one over its rank, where rank one contributes a full point, rank two contributes a half, rank three a third, and so on. Then you add the contributions across both lists and sort by the total. A document that ranked well in both searches beats a document that ranked well in only one, which is exactly the signal you want: something is probably relevant if both a lexical and a semantic path found it.
The pgvector examples have shipped this pattern for a while, and it is a few lines of code. The beauty of it is that it needs no training, no score calibration, and no per corpus tuning. It just works, and it holds up when either path changes.
Reranking is where the quality actually comes from
Reciprocal rank fusion gets you a decent merged list. If you want great results, you add one more step on top: a reranker. The difference between the first stage and the reranker is the difference between a cheap filter and a careful reader.
The first stage, the vector index and the lexical index, is designed to be fast, because it has to scan a lot. It retrieves a broad candidate set, maybe fifty or a hundred documents. The reranker then takes that small set and scores each document against the query more carefully, often with a cross encoder, a model that reads the query and the document together. That interaction is what makes the difference. An embedding model scores the query and the document independently and compares the two points. A cross encoder reads the pair as one unit and can see interactions, negation, and specific evidence.
This is the standard modern architecture: broad recall from the cheap paths, precise ordering from the reranker. It also fixes a subtle problem with pure vector search, where the top few results can all be the same general document in slightly different chunked forms, because nothing ever explicitly de-duplicated or compared them.
Filters are part of search, not an afterthought
Search is not just text matching. It is text matching within a set of constraints. The user wants results from the last month, in English, in the accounts category, for their own tenant. If you ignore these dimensions during retrieval, you are asking the model to compensate for missing information.
The practical rule is to push filters into the retrieval step, not apply them after. In Postgres, that means an index on the filter columns, and it means being honest about what approximate indexes can and cannot do. A vector index with a WHERE clause applied after the fact can return too few results when the filter is selective, because the index only scanned a small neighborhood. pgvector added iterative scans to handle exactly this, rescanning the index until enough results survive the filter. If you have never hit this, it is because you have not filtered much yet. You will.
The honest architecture
The search systems I trust look like this. A lexical path for exact matches, identifiers, and shared vocabulary, backed by Postgres full text search or a dedicated engine. A vector path for paraphrase and concept matching, backed by an index with checked recall. A fusion step to merge the two lists, usually reciprocal rank fusion. A reranker for the final ordering, if the corpus and traffic justify it. Filters applied at retrieval time, with the indexes to support them.
Each piece is weak on its own and strong in combination. This is not a trend I am predicting. It is the boring consensus that has emerged from years of production search, and the demo systems that skip it are not ahead of the curve. They are behind it, and their users are about to find out.