Text search returns a ranked list of existing passages. It does not write a new answer.
This is the first half of a text RAG system: find the evidence before asking a language model to explain it.
It is tempting to treat retrieval as plumbing and focus on the model that writes the answer. That gets the priority backwards.
The generator can only work with the passages it is handed. If the right passage never arrives, no amount of prompt engineering produces a correct answer — the model will simply write something fluent from the wrong material. Most disappointing RAG systems are not suffering from a weak generator; they are suffering from retrieval that quietly returned the wrong five passages.
So the question this lesson answers is a narrow and important one: given a question, how do you find the passages that actually contain the answer?
Your help centre contains this sentence:
To change your login credentials, open Settings.
A user types:
How do I reset my password?
A human sees instantly that these match. A computer does not, because the two sentences share almost no words. "Reset" is not "change", "password" is not "login credentials".
This single example contains the whole tension of text search. Match on words and you miss this passage entirely. Match on meaning and you find it — but you also become fuzzy about things where the exact characters matter, like an invoice number or a product code.
Taking the same example, here is how the two approaches differ:
| Search type | What it matches | Strength | Weakness |
|---|---|---|---|
| Keyword (TF-IDF/BM25) | Exact or related words | Fast, clear, good for names and IDs | Can miss synonyms |
| Dense semantic | Meaning represented by vectors | Finds paraphrases | Can blur negation or numbers |
| Hybrid | Keyword + semantic scores | Covers both signals | Needs score tuning |
A bi-encoder encodes the query and documents separately into the same vector space.
Document vectors can be calculated once and reused. At request time, only the new query must be encoded.
Similarity is often measured with a dot product or cosine similarity:
cosine_similarity(x, y) = (x · y) / (||x|| × ||y||)
A larger value means the vector directions are more similar.
Comparing a query exactly with millions of vectors can be too slow. Approximate nearest-neighbour (ANN) indexes such as FAISS or HNSW quickly find a strong candidate set.
Then a more careful reranker reads the query and each candidate together.
Conceptual code:
query = "How do I reset my password?"
query_vector = encoder.encode(query)
candidate_ids = index.search(query_vector, top_k=20)
ranked = reranker.rank(query, passages[candidate_ids])
context = ranked[:5]
The reranker would be too expensive over every passage, but it is practical over 20 or 50 candidates.
During contrastive training:
A random passage about gardening is easy to reject for a password query. A passage about changing an email address is a better training challenge because it is a near miss.
Common research benchmarks include MS MARCO for passage ranking, BEIR for testing across different domains, and MTEB for many embedding tasks.
A long document must usually be split.
There is no universal best chunk size. Test it on real questions.
A retriever trained on general web pages may perform poorly on medical, legal, or company-specific language.
“Revenue grew” and “revenue did not grow” can look deceptively close as vectors. Hybrid search, reranking, and exact validation help.
A passage can discuss the same topic without answering the question. Evaluate whether results are answer-bearing, not merely similar.
Text retrieval uses fast vector search to find candidates and an optional reranker to improve precision; hybrid signals protect exact words and numbers.