Article 02 Retrieval / Beyond naive RAG

Beyond Vector Search: Retrieval That Respects Business Rules.

Published
Reading time
13 minutes
Topics

Hybrid search, rank fusion, and reranking can improve your evidence. Production retrieval also needs to respect the parts of a question that aren’t negotiable.

Dark desk with two monitors full of code and a phone lit by warm orange light
On this page

Try this question on a support copilot:

“Find unresolved incidents for our enterprise customers in the EU, opened in the last 30 days, mentioning AUTH-1042 or a login loop after SSO was enabled.”

A basic RAG system embeds the sentence, finds nearby document vectors, and passes the top results to an LLM. You might get a beautifully written answer about SSO troubleshooting.

You might also get a resolved incident from last year, a US customer, and a document that mentions AUTH-1041. All three are semantically related. All three can be wrong for this request.

Retrieval-augmented generation, or RAG, gives a model external evidence before it answers. The quality of that evidence depends on how you retrieve it. Similarity is one useful signal; the question above also contains exact identifiers, access rules, dates, and business predicates.

Let’s build a pipeline that handles each part deliberately.

First, separate relevance from eligibility

Embeddings represent text as vectors so you can search for nearby meanings. That’s useful when a customer says “I keep getting bounced back to sign-in” and the runbook says “authentication redirect loop.”

But vector distance is not a reliable implementation of status = open, a timestamp comparison, or an exact error-code match. It also cannot decide whether a user is entitled to see a record. Those conditions need explicit enforcement.

For our example, divide the request into two groups:

Fig. 01Eligibility versus relevance

Hard constraints

Filter. Must hold for every result.
  • Records the caller may access
  • Enterprise customer tier
  • EU customer region
  • Unresolved incident status
  • Opened within the chosen 30-day window

Enforced by access control, metadata filters, or a structured query.

Relevance signals

Rank. Helps order what is eligible.
  • The phrase “login loop”
  • Semantically related SSO symptoms
  • The exact code AUTH-1042
  • Evidence about enabling SSO
  • Descriptions of the same failure pattern

Scored by BM25, embeddings, fusion, and reranking.

Hard constraints decide which records may appear at all. Relevance signals only decide the order of what remains.

The Boolean meaning matters too. “AUTH-1042 or a login loop” permits either branch. If the user asks for “AUTH-1042 only,” the code becomes a hard exact-match condition. A search system that quietly rewrites one into the other has changed the question.

Resolve relative time once, using an explicit time zone and an as_of timestamp. Decide what “EU customer” means in your data model: billing region, account region, or incident location. If those interpretations materially change the result, ask rather than guessing.

Let different retrieval methods do different jobs

BM25 covers the lexical side. It scores documents using query terms, with factors for term frequency, how common a term is across the collection, and document length. It is commonly implemented over an inverted index. This is the sparse branch: documents contribute term evidence for the vocabulary they contain. Rare technical terms can be especially useful signals.R1

That makes BM25 a strong companion for product names, configuration keys, and distinctive error messages. It still isn’t an exact-equality operator. Tokenization may split punctuation or normalize text. Preserve error codes in a dedicated keyword field when exact identity matters.

Dense search covers the semantic side. An embedding model helps retrieve passages that express a related idea using different words. The query and document encoding must be compatible, including any query/document modes the chosen model requires.R2

Structured queries cover the rules. Use indexed metadata filters for fields already in the search index. Use a relational query or a dedicated domain service when the request depends on joins, authoritative status, or calculations. “Accounts with two overdue invoices and an active enterprise contract” should not become a vague similarity query.

Hybrid search combines lexical and semantic retrieval.R3 A practical version runs the two searches concurrently over the same authorized, eligible corpus, then combines their candidates. The two branches should refer to the same document and chunk identities so their results can be reconciled.

Fig. 02Retrieve, combine, then read closely
Step 1Resolve access + hard filters

Tenant, permissions, status, region, time window. Fixed in application code.

SparseBM25 candidates

Lexical match over an inverted index. Top 50.

  • words
  • codes
  • exact fields
DenseVector candidates

Embedding similarity over the same eligible corpus. Top 50.

  • paraphrase
  • related meaning
Step 3RRF → rerank → evidence

Fuse by rank, deduplicate by chunk ID, rerank a shortlist, keep sources attached.

Authorization applies before candidate text reaches a model or an external reranker.

Give both indexes something worth finding

Before tuning search, inspect what you indexed. A chunk reading “This fixed it” is almost useless if the product version, error message, and actual fix were cut into other chunks.

For incident records, preserve a meaningful unit: the incident title, a coherent event or resolution passage, and enough surrounding context to interpret it. Attach stable document and chunk IDs, tenant and access metadata, timestamps, region, status, source URL, and content version.

Keep lexical text and embeddings tied to the same content version. If one index contains a corrected runbook while the other still serves an old copy, fusion can combine contradictory evidence. Treat deletions and permission changes as ingestion events too. An inaccessible or deleted document shouldn’t survive indefinitely in a search cache.

Prefer filtering within candidate retrieval where the engine supports it. Retrieving ten global neighbors and discarding nine forbidden records afterward leaves one result; it doesn’t magically discover nine eligible replacements.

Fig. 03When the filter runs changes what you get

Filter after retrieval

Top 10 from the global index

✕ 9 forbidden records discarded, 1 result left

Filter during retrieval

Top 10 from the caller’s eligible set

✓ 10 eligible results, nothing to discard

Post-filtering shrinks the result set. Filtering during retrieval searches the right population in the first place.

Engines differ in how approximate nearest-neighbor search interacts with filters, so measure recall under your actual tenant and permission distribution.R4

If your architecture requires broader internal retrieval, keep those candidates inside the trusted retrieval service, authorize them before any external reranker or model sees their text, and overfetch only within a controlled budget. Recheck permissions when necessary to cover stale index metadata.

Merge rankings without pretending their scores match

Now you have two lists. BM25 might assign a document 12.7 while the vector search returns a similarity of 0.82. Adding those numbers directly makes the result depend on incompatible score scales.

Reciprocal rank fusion, or RRF, combines list positions instead. Each document gets a contribution of 1 / (c + rank) from every list where it appears; ranks start at one. Missing from a list means no contribution. The constant c softens the difference between high and lower positions.R5 R8

Here’s a toy example with c = 60. These are calculated illustrations, not measured search results. Change the ranks or the constant to see how fusion responds.

Fig. 04Reciprocal rank fusion, live

score(d) = Σ 1 / (c + rank)

CandidateBM25 rankVector rankRRF score
A Exact error, wrong symptom0.01639
B Matching SSO failure0.03200
C General SSO guide0.01639

Fused order: B → A → C. B leads with 0.03200. Smaller values of c make first place count for more.

With the defaults, B wins because both lists support it. Push B down both lists and lower c, and a single first place starts to beat agreement.

Candidate B wins because both lists support it. That agreement is helpful, but it doesn’t prove B answers the question. An exact-match route may still be needed for identifier lookups, and rank fusion cannot recover a document that neither branch retrieved.

The following small Python function illustrates fusion over lists of stable chunk IDs:

PythonReciprocal rank fusion
def rrf(rankings, c=60):
    scores = {}
    for ranking in rankings:
        seen = set()
        for rank, chunk_id in enumerate(ranking, start=1):
            if chunk_id in seen:
                continue
            seen.add(chunk_id)
            scores[chunk_id] = scores.get(chunk_id, 0.0) + (
                1.0 / (c + rank)
            )
    return sorted(scores, key=lambda x: (-scores[x], x))

The final ID sort makes ties deterministic. Deduplication within each list prevents repeated IDs from collecting extra votes. Choose candidate windows and the fusion constant using evaluation; don’t mistake an example default for a universal optimum.

To wire the branches together, the orchestration can look like this. search_bm25 and search_dense are application adapters, not SDK methods. Both must enforce the same trusted scope, including permissions and validated business filters, and return chunks with consistent IDs and versions. query_vector comes from your compatible embedding model.

PythonConcurrent hybrid retrieval
import asyncio

async def hybrid_candidates(query, query_vector, scope):
    lexical, semantic = await asyncio.gather(
        search_bm25(query, scope=scope, limit=50),
        search_dense(query_vector, scope=scope, limit=50),
    )
    by_id = {hit["id"]: hit for hit in lexical + semantic}
    ranked_ids = rrf([
        [hit["id"] for hit in lexical],
        [hit["id"] for hit in semantic],
    ])
    return [by_id[chunk_id] for chunk_id in ranked_ids[:40]]

The shortlist now has one entry per chunk, ready for reranking. Handle an empty list before invoking the reranker. Adapters should reject an invalid scope rather than retrying without its filters.

Reranking asks a more focused question

Fusion asks how well a candidate placed across retrieval methods. A reranker looks more directly at how a candidate relates to the query.

A common design uses a cross-encoder: it processes the query and a candidate together, allowing detailed interaction between their words. That’s more expensive than retrieving precomputed document vectors, so you usually apply it to a shortlist.R6

Try retrieving 50 candidates from each branch, fusing the union, and reranking the top 40. Then assemble perhaps six to ten useful passages within a token budget.

Fig. 05Narrowing the evidence
  1. BM25 candidates50
  2. Dense candidates50
  3. Fused union, deduplicated≤ 100
  4. Reranked shortlist40
  5. Passages in context6–10
Initial experimental settings, not a prescription. Each stage spends more compute on fewer candidates.

Cohere Rerank is one hosted option. Its v2 API accepts a query, document strings, a model identifier, and a requested result count. Returned indices map back to the documents you submitted.R7

PythonCohere Rerank v2
import os
import cohere

# candidates: authorized chunks in their original order
client = cohere.ClientV2(api_key=os.environ["COHERE_API_KEY"])
response = client.rerank(
    model=os.environ["COHERE_RERANK_MODEL"],
    query=query,
    documents=[c["text"] for c in candidates],
    top_n=min(8, len(candidates)),
)
selected = [candidates[r.index] for r in response.results]

This fragment assumes a nonempty shortlist and a model available to your account. Add a deadline, error handling, and an empty-result branch in the application. Only send content to a hosted service if your data-handling policy permits it.

Keep identifiers alongside the submitted strings. Don’t sort the candidate array before mapping returned indices, or your citations may point at the wrong text. Also watch document length: the API documents truncation limits, so a decisive sentence at the end of an oversized passage may never be considered.R7

Assemble evidence instead of filling a bucket

The top eight chunks may all repeat the same paragraph from one incident. Passing them all to the generator spends tokens without adding much evidence.

Deduplicate overlapping passages, retain their provenance, and include the surrounding section when a fragment depends on it. For a comparison question, seek coverage of both sides. For a troubleshooting question, distinguish symptoms, the proposed fix, and confirmation that the fix worked.

You also need a way to stop. If the retrieved material doesn’t establish whether SSO was enabled before the login loop began, say that evidence is missing. Nearby text about SSO doesn’t establish the sequence of events.

For exhaustive requests, retrieval needs another route entirely. “How many unresolved incidents meet these criteria?” requires an exact count over the eligible records. “List all of them” needs pagination or an export. A top-k semantic shortlist can support explanation; it cannot justify a claim that every match was found.

Add an agent only where another step can help

An agentic retrieval system can choose tools, reformulate a query, or fetch follow-up evidence based on what it finds. That flexibility is valuable when the next step genuinely depends on the previous result.

For example, the first search finds the error code but lacks the deployment version. The agent can read the authorized incident record, extract the version, then retrieve the relevant runbook section. An explicit state machine can implement the same controlled workflow; you don’t need open-ended autonomy to benefit from multiple steps.

Fig. 06A bounded retrieval loop
  1. 01
    Search

    Initial retrieval. Tenant and business filters are fixed in application code.

  2. 02
    Check the evidence

    Does it satisfy the whole question, including the sequence of events?

    Sufficient → answer with citations
  3. 03
    Diagnose the gap

    Is the missing piece a fact, or ambiguity in what the user asked?

    Ambiguous → ask the user
  4. 04
    Follow up

    At most two extra retrievals, inside a total deadline and cost budget. Then check again.

    Loop back to step 02
  5. 05
    Stop honestly

    Answer what the records support and name what is still missing.

    Budget spent → explicit limitation
One initial search plus at most two follow-ups, inside a deadline and a cost budget. Filters never loosen between steps.

Keep the non-negotiable filters in application code. A query rewrite can improve wording, but it must not drop tenant restrictions, broaden access, or turn “unresolved” into “all incidents.”

Stop sooner when evidence is sufficient. If the remaining gap is ambiguity in the user’s question, ask. More searching won’t decide which business definition they intended.

Measure the stage that actually failed

Start with a labeled set of real questions: exact codes, paraphrases, date ranges, joins, multilingual phrasing, ambiguous requests, and questions with no answer. Include access-control cases as a separate, mandatory evaluation.

Compare lexical-only retrieval, dense-only retrieval, hybrid fusion, and hybrid plus reranking on the same set. Then inspect three different outcomes:

  • Candidate recall: Did the eligible evidence enter the candidate set? Recall@K measures how much of the labeled relevant evidence appears in the first K results.
  • Ranking quality: Did useful evidence reach the top? Metrics such as nDCG@K account for graded relevance and position; reciprocal-rank metrics help when finding one first useful result matters.
  • Answer quality: Does the answer follow the evidence, cite the right source, satisfy the filters, and abstain when support is missing?
Fig. 07Three stages, three different fixes
  1. 01Candidate recall

    Did the eligible evidence enter the candidate set?

    • Recall@K

    Where to look: Chunking, indexing, filters, query routing

  2. 02Ranking quality

    Did useful evidence reach the top?

    • nDCG@K
    • MRR

    Where to look: Candidate windows, fusion constant, reranker

  3. 03Answer quality

    Does the answer follow the evidence and abstain when support is missing?

    • Citation accuracy
    • Filter compliance
    • Abstention

    Where to look: Evidence assembly, stopping rules, generation

Track alongside quality: p95 latency · cost per request · index freshness · failure rate · access-control cases

A reranker can’t fix missing candidates. A larger candidate pool can’t fix a generator that invents a conclusion.

Track latency at the 95th percentile, cost per request, index freshness, and failure rates alongside quality. Looking at the stages separately tells you where to spend effort.

At larger corpus sizes, an approximate nearest-neighbor index can avoid an exhaustive vector scan, trading some recall for speed. Tune that tradeoff on your workload rather than assuming the closest results will always be found.R2

Run independent retrieval branches concurrently. Cache with keys that include the relevant identity or permission scope, filters, and corpus version. Give reranking a timeout and decide whether an authorized fused shortlist is an acceptable fallback for the use case. If it isn’t, return an explicit limitation. A missing authorization decision must never become a reason to search more broadly.

Make the system answer the actual question

Return to our support request. The system should resolve access, region, tier, status, and time first; search for the permitted code or symptom; fuse and rerank the eligible evidence; then explain what the records support.

The practical payoff is easy to recognize. A reader gets the incident they needed, understands why it matched, and can follow the citation. Your team can explain how it was selected and diagnose what happened when selection failed.

That’s a much more useful target than making cosine similarity look impressive in a demo.

Sources and further reading

  1. R1Apache Lucene, BM25Similarity. Term frequency, rarity, and length normalization.
  2. R2Sentence Transformers, Semantic search. Embeddings and query/document encoding.
  3. R3Elastic, Hybrid search. Combining lexical and semantic retrieval.
  4. R4Elastic, kNN search. Exact and approximate vector retrieval; linked filtering guidance.
  5. R5Elastic, Reciprocal rank fusion. Formula, rank constants, and candidate windows.
  6. R6Sentence Transformers, Retrieve & Re-Rank. Bi-encoder retrieval and cross-encoder reranking.
  7. R7Cohere, Rerank API v2. Request fields, output indices, and truncation behavior.
  8. R8Cormack, Clarke, and Buettcher, Reciprocal Rank Fusion outperforms Condorcet and individual rank learning methods. SIGIR 2009. The original rank-fusion formulation.

Technical references checked 27 Aug 2026. Examples are illustrative; code fragments are not complete applications.

Share

Continue the series

All writing
Laptop showing code on a bright, minimal desk next to a large monitor03

API contracts 12 min read

From as MyType to Real API Contracts

How to combine TypeScript, runtime schemas, authorization, and database guarantees without pretending one of them can do every job.