Skip to content
Veristamp
Type to search the engineering journal.Full-text index · ⌘K to open
Qdrant Hybrid Search for Legal RAG Tested on 2,034 CasesWe ran Qdrant hybrid search (RRF + ColBERT + FormulaQuery) on 2,034 Supreme Court precedents. Top-1 went from 31.8% to 86.4%. Code, weights, misses inside.QdrantVector SearchRAGEngineeringFastEmbed
15 min readSrimon Danguria

Qdrant Hybrid Search for Legal RAG Tested on 2,034 Cases

We ran Qdrant hybrid search (RRF + ColBERT + FormulaQuery) on 2,034 Supreme Court precedents. Top-1 went from 31.8% to 86.4%. Code, weights, misses inside.

Diagram of JurisBoost single query_points call with RRF prefetch, ColBERT rescore, and statutory FormulaQuery

Legal precedent retrieval is a search pipeline that ranks binding judicial decisions against statutory claims and factual queries. Finding the governing authority in legal research is rarely a pure keyword match or a generic semantic similarity lookup. A single misidentified statutory section or an overturned precedent can invalidate an entire legal argument.

The Indian legal system presents a severe retrieval challenge. Seven decades of Supreme Court jurisprudence rest on the Indian Penal Code of 1860, the Code of Criminal Procedure of 1973, and the Indian Evidence Act of 1872. In 2023, India replaced these foundational codes with the Bharatiya Nyaya Sanhita (BNS), the Bharatiya Nagarik Suraksha Sanhita (BNSS), and the Bharatiya Sakshya Adhiniyam (BSA).

Precedent archives are written in the language of the old codes. Modern legal queries increasingly arrive phrased in the new statutory sections or as natural language case facts without section numbers. Effective legal precedent retrieval requires connecting these two disparate legislative eras.

When we evaluate standard dense vector retrieval against this corpus, it fails on two out of every three queries, achieving only 31.8% Top-1 accuracy. Dense embeddings blur section boundaries and confuse distinct procedural mechanisms.

To solve this, we built JurisBoost on Qdrant 1.19(opens in a new tab)1. The pipeline executes a three-stage legal precedent retrieval ladder inside a single database network round-trip. It combines 1-bit Binary Quantization (BQ), sparse BM25 with inverse document frequency modifiers, server-side ColBERT multi-vector late interaction, and a mathematical FormulaQuery with statutory concordance bridging.


The Lineage and Inspiration

JurisBoost builds directly upon the architectural insights published by Akshay Kumar Sharma in his analysis of legal discovery optimization2, as well as his open-source codebase Cappybara12/legal-rag3.

Sharma demonstrated that standard two-stage Retrieval-Augmented Generation (RAG) pipelines suffer from three structural inefficiencies:

  1. Network Overhead: Passing candidates from a vector database to an external reranking endpoint (such as Cohere or a dedicated Cross-Encoder) introduces redundant serialization and network latency.
  2. Operational Fragility: Introducing a separate reranking microservice doubles the failure surface of the retrieval path.
  3. Context Window Inflation: Passing entire 500-to-1000 token contract chunks into an LLM wastes context budget on boilerplate legalese, increasing generation latency and API cost.

Sharma proved that native ColBERT multi-vector MAX_SIM rescoring inside Qdrant, paired with local sentence isolation on the client CPU, cut LLM input token costs by 67.1% on corporate contract discovery while eliminating external rerankers entirely.

JurisBoost extends Sharma’s foundation into full-scale criminal legal precedent retrieval. We integrated the official NyayaRAG(opens in a new tab) dataset4, designed a 75-section statutory concordance map (IPC/CrPC/IEA to BNS/BNSS/BSA), configured three hardware memory tiers, and implemented Qdrant 1.19 FormulaQuery scoring with live facet analytics.

JurisBoost Retrieval Architecture


Upstream Pipeline: DuckDB Deduplication and Concordance Extraction

Raw legal case datasets contain extensive citation noise. The NyayaRAG source corpus consists of 4,960 Supreme Court judgments. Each judgment cites other cases, so the pipeline first flattens those citations, then deduplicates.

We use DuckDB in memory to collapse duplicate cited holdings and count how often each precedent is cited:

import duckdb

def process_nyayarag_corpus(precedent_occurrences) -> list[dict]:
    con = duckdb.connect(":memory:")
    con.register("precedent_occurrences", precedent_occurrences)
    return con.execute("""
        SELECT
            precedent_key,
            arg_max(title, text_length) AS title,
            arg_max(text, text_length) AS text,
            count(DISTINCT source_case_id)::INTEGER AS source_case_count,
            list(DISTINCT source_case_id) AS source_case_ids
        FROM precedent_occurrences
        GROUP BY ALL
        ORDER BY precedent_key
    """).df().to_dict("records")

That query does three things:

  • Groups flattened citations by canonical precedent key: 4,960 source judgments → 9,337 unique cited precedents.
  • Keeps the longest title and holding via arg_max.
  • Calculates citation in-degree (source_case_count).

Python then extracts statutes, writes dual concordance payloads, and keeps only criminal-code points (ipc / crpc / iea and their 2023 maps). That filter is what produces the 2,034 indexed precedents.

Statutory Concordance Payloads

Each precedent is scanned for historical statutory references and mapped to its 2023 equivalent. For example, a landmark 1980 ruling citing Section 438 of the Code of Criminal Procedure (Anticipatory Bail) receives dual payload attributes:

  • legal_references: ["crpc:438"]
  • mapped_references: ["bnss:482"]

This dual indexing enables legal precedent retrieval to resolve queries phrased under either historical acts or new criminal codes without requiring document text rewriting.


Storage Architecture and Memory Tiers in Qdrant 1.19

High-throughput legal search engines must balance RAM allocation against precision. Multi-vector ColBERT representations require substantial storage, since every token produces a 128-dimensional float vector.

JurisBoost configures three distinct memory tiers in Qdrant 1.19:

Vector / Index Type Representation Memory Tier Engineering Role
Dense 384-d (BGE-small) Cosine + 1-bit BQ CACHED vectors, BQ PINNED Sub-5 ms prefetch. Fast candidate selection.
Sparse BM25 Qdrant/bm25 (Modifier.IDF) CACHED (SparseIndexParams, 1.19 tier) Lexical hits on exact statutory section numbers.
ColBERT 128-d MAX_SIM Multi-Vector (m=0) COLD Disk Token-level late interaction on top-40 candidates only.
Payload Indexes Keyword (prefix=True), Datetime, Integer PINNED (explicit) Statutory filters with formula terms evaluated without disk seeks.
from qdrant_client import QdrantClient, models

def setup_jurisboost_collection(client: QdrantClient, collection_name: str):
    client.create_collection(
        collection_name=collection_name,
        vectors_config={
            "dense": models.VectorParams(
                size=384,
                distance=models.Distance.COSINE,
                memory=models.Memory.CACHED,
                quantization_config=models.BinaryQuantization(
                    binary=models.BinaryQuantizationConfig(memory=models.Memory.PINNED)
                ),
            ),
            "colbert": models.VectorParams(
                size=128,
                distance=models.Distance.COSINE,
                multivector_config=models.MultiVectorConfig(
                    comparator=models.MultiVectorComparator.MAX_SIM
                ),
                memory=models.Memory.COLD,
                hnsw_config=models.HnswConfigDiff(m=0),
            ),
        },
        sparse_vectors_config={
            "bm25": models.SparseVectorParams(
                index=models.SparseIndexParams(memory=models.Memory.CACHED),
                modifier=models.Modifier.IDF,
            ),
        },
    )

    # Payload indexes for statutory lookups and ranking decays (tiers explicit)
    for field in ("legal_references", "mapped_references"):
        client.create_payload_index(
            collection_name,
            field,
            models.KeywordIndexParams(
                type=models.KeywordIndexType.KEYWORD,
                prefix=True,
                memory=models.Memory.PINNED,
            ),
        )
    client.create_payload_index(
        collection_name,
        "judgment_date",
        models.DatetimeIndexParams(
            type=models.DatetimeIndexType.DATETIME, memory=models.Memory.PINNED
        ),
    )
    client.create_payload_index(
        collection_name,
        "source_case_count",
        models.IntegerIndexParams(
            type=models.IntegerIndexType.INTEGER, memory=models.Memory.PINNED
        ),
    )

Setting hnsw_config=models.HnswConfigDiff(m=0) on the ColBERT vector configuration is a critical optimization. Because ColBERT vectors are used exclusively to rescore the pre-filtered candidate pool produced by Stage 1, constructing an HNSW graph for ColBERT is unnecessary. This eliminates indexing overhead and avoids allocating memory for multi-vector graphs.

For more details on how Qdrant manages sparse vector scoring across collections, see our benchmark on Qdrant per-tenant IDF scoring.


The Four-Mode Search Ladder

To evaluate how each component contributes to legal precedent retrieval accuracy, JurisBoost implements a four-rung search ladder.

Search Ladder Evaluation

Mode 1: Dense Binary Quantization (Baseline)

Mode 1 performs standard cosine similarity over 384-dimensional BGE-small dense embeddings compressed via 1-bit Binary Quantization.

results = client.query_points(
    collection_name="nyayarag_legal_precedents",
    query=dense_query_vector,
    using="dense",
    limit=5,
    with_payload=True,
)

Dense retrieval alone is fast (2.4 ms p50 latency) but struggles on precise statutory language. On the default anticipatory-bail query (Section 482 BNSS / 438 CrPC), dense ranks Siddharam Satlingappa Mhetre (2010) first. Sushila Aggarwal (2020 Constitution Bench) is second. Hybrid onward ranks Sushila first. Dense finds a 438 case. It does not rank the governing 2020 holding first.

Mode 2: Hybrid RRF (Dense + BM25)

Mode 2 combines dense candidate selection with server-side BM25 sparse retrieval using Reciprocal Rank Fusion (RRF)5.

results = client.query_points(
    collection_name="nyayarag_legal_precedents",
    prefetch=[
        models.Prefetch(
            query=dense_query_vector,
            using="dense",
            limit=100,
            params=models.SearchParams(
                quantization=models.QuantizationSearchParams(rescore=True)
            ),
        ),
        models.Prefetch(
            query=models.Document(text=query_text, model="Qdrant/bm25"),
            using="bm25",
            limit=100,
        ),
    ],
    query=models.FusionQuery(fusion=models.Fusion.RRF),
    limit=5,
    with_payload=True,
)

RRF merges rankings from disparate score distributions without requiring score normalization. Adding BM25 allows exact statutory tokens (such as Section 154 or FIR) to surface relevant precedents that dense embeddings miss, lifting legal precedent retrieval Top-1 accuracy from 31.8% to 45.5%.

Mode 3: Hybrid RRF + Statutory FormulaQuery

Mode 3 pipes the hybrid RRF candidate pool into Qdrant 1.19’s server-side FormulaQuery. The formula injects domain-specific ranking factors:

  1. Base Score: The underlying fusion score.
  2. Temporal Decay: An exponential decay (ExpDecay) with a 20-year half-life on judgment_date, favoring recent interpretations while preserving historical authority.
  3. Citation Authority: A logarithmic boost log10(source_case_count + 1) rewarding heavily cited precedents.
  4. Direct Statutory Match: A positive boost (+0.25 × domain scale) when a query references a statute present in legal_references.
  5. Concordance Bridge Match: A substantial boost (+0.50 × domain scale) when a query references a new statute that maps to an old statute in mapped_references.
statutory_formula = models.FormulaQuery(
    formula=models.SumExpression(
        sum=[
            models.MultExpression(mult=[1.0, "$score"]),
            models.MultExpression(
                mult=[
                    0.20 * 6.0,
                    models.ExpDecayExpression(
                        exp_decay=models.DecayParamsExpression(
                            x=models.DatetimeKeyExpression(datetime_key="judgment_date"),
                            target=models.DatetimeExpression(datetime="2026-09-03T00:00:00Z"),
                            scale=86400 * 365 * 20,
                            midpoint=0.5,
                        )
                    ),
                ]
            ),
            models.MultExpression(
                mult=[
                    0.15 * 6.0,
                    models.Log10Expression(
                        log10=models.SumExpression(sum=["source_case_count", 1.0])
                    ),
                ]
            ),
            models.MultExpression(
                mult=[
                    models.FieldCondition(
                        key="legal_references",
                        match=models.MatchValue(value="crpc:438"),
                    ),
                    0.25 * 6.0,
                ]
            ),
            models.MultExpression(
                mult=[
                    models.FieldCondition(
                        key="mapped_references",
                        match=models.MatchValue(value="bnss:482"),
                    ),
                    0.50 * 6.0,
                ]
            ),
        ]
    ),
    defaults={"source_case_count": 0.0, "judgment_date": "2000-01-01T00:00:00Z"},
)

Mode 3 delivers a major accuracy gain on explicit statutory citations and new-code queries, driving legal precedent retrieval Top-1 accuracy to 72.7%.

Mode 4: Universal Pipeline (RRF + ColBERT MaxSim + Formula)

Mode 4 executes the complete three-stage pipeline in one round-trip:

  1. Stage 1 (Prefetch): Hybrid RRF retrieves the top 100 candidate precedents using dense BQ and BM25.
  2. Stage 2 (ColBERT Rescore): Qdrant loads the 128-dimensional ColBERT token matrices from cold disk for the top 40 candidates and computes token-level MAX_SIM late interaction.
  3. Stage 3 (Formula Ranking): Qdrant applies the statutory concordance FormulaQuery over the ColBERT scores.
results = client.query_points(
    collection_name="nyayarag_legal_precedents",
    prefetch=[
        models.Prefetch(
            query=[list(vec) for vec in colbert_query_matrix],
            using="colbert",
            limit=40,
            prefetch=models.Prefetch(
                query=models.FusionQuery(fusion=models.Fusion.RRF),
                prefetch=[
                    models.Prefetch(
                        query=dense_query_vector,
                        using="dense",
                        limit=100,
                        params=models.SearchParams(
                            quantization=models.QuantizationSearchParams(rescore=False)
                        ),
                    ),
                    models.Prefetch(
                        query=models.Document(text=query_text, model="Qdrant/bm25"),
                        using="bm25",
                        limit=100,
                    ),
                ],
                limit=100,
            ),
        )
    ],
    query=statutory_formula,
    limit=3,
    with_payload=True,
)

“Setting rescore=False on the dense prefetch avoids reading full-precision float32 vectors from disk, since ColBERT token comparison handles final rescoring.”

Notice the parameter rescore=False inside the inner dense prefetch. When using Binary Quantization, Qdrant normally retrieves candidates using binary vectors and then loads full float32 vectors from disk for rescoring. Because Stage 2 performs a more accurate ColBERT token-level rescore, disabling dense float32 rescoring saves disk I/O and reduces query latency.


22-Query Benchmark Results

We evaluated the four modes against 22 distinct legal queries divided into three difficulty tiers:

  1. Explicit Citations (8 queries): Queries containing direct statutory references (such as Section 438 CrPC or Section 302 IPC).
  2. Natural Language (8 queries): Paraphrased factual scenarios without explicit section numbers (such as mandatory registration of FIR in cognizable offenses or circumstantial evidence five golden principles).
  3. Edge and New-Code Traps (6 queries): Queries phrased entirely in new statutory codes (BNS/BNSS/BSA), prefix ambiguities, and legal negation patterns.

Benchmark Results Summary

Here are the measured results across all 22 legal precedent retrieval evaluation queries:

Evaluation Tier Query Count (n) M1 Dense BQ M2 Hybrid RRF M3 Hybrid + Formula M4 RRF + ColBERT + Formula
Explicit Citations 8 3/8 (37.5%) 5/8 (62.5%) 8/8 (100.0%) 8/8 (100.0%)
Natural Language 8 3/8 (37.5%) 3/8 (37.5%) 2/8 (25.0%) 5/8 (62.5%)
Edge / New-Code Traps 6 1/6 (16.7%) 2/6 (33.3%) 6/6 (100.0%) 6/6 (100.0%)
Combined Top-1 22 7/22 (31.8%) 10/22 (45.5%) 16/22 (72.7%) 19/22 (86.4%)
Combined Recall@3 22 12/22 (54.5%) 18/22 (81.8%) 19/22 (86.4%) 21/22 (95.5%)
p50 Latency 22 2.4 ms 8.8 ms 7.5 ms 13.2 ms

Analyzing the Retrieval Shifts

The benchmark reveals why multi-stage late interaction is necessary for legal precedent retrieval:

  • The M2 to M3 Leap: Adding statutory concordance and citation authority solves explicit section references and new-code bridges, jumping Top-1 from 45.5% to 72.7%.
  • The Natural Language Challenge in M3: On natural language queries without statutory numbers, statutory formula weighting can inadvertently amplify unrelated precedents, dropping natural language Top-1 from 37.5% to 25.0%.
  • The ColBERT Recovery in M4: Introducing ColBERT MaxSim in Stage 2 corrects semantic drift on paraphrased text, recovering natural language accuracy to 62.5% and achieving 86.4% overall Top-1.

Only one query (N8, custodial disclosure under Section 27 IEA / Section 23(2) BSA) missed Top-3 in M4, caused by a coverage gap in the source dataset rather than a ranking error.


Downstream Precision: Local Sentence Isolation

Retrieving the correct legal precedent chunk is only half the battle. In legal RAG, precedent holdings often span 400 to 800 words of background reasoning. Forwarding entire judicial paragraphs into an LLM context window increases prompt costs and triggers attention dilution.

JurisBoost executes local sentence isolation on the client CPU following the retrieval step:

import re
import numpy as np

# Protect legal citations and abbreviations from naive period splitting
LEGAL_ABBREV_PATTERN = re.compile(
    r"\b(Sec|Secs|Section|Sections|v|vs|Ors|Anr|Art|Arts|No|Hon'ble|para|paras)\.\s*",
    re.IGNORECASE,
)

def isolate_operative_holding(
    passage_text: str,
    colbert_query_matrix: np.ndarray,
    colbert_embedder,
    top_n: int = 1,
) -> list[tuple[str, float]]:
    # Protect abbreviations before sentence tokenization
    protected_text = LEGAL_ABBREV_PATTERN.sub(r"\1<DOT>", passage_text)
    raw_sentences = [
        s.replace("<DOT>", ".").strip() 
        for s in re.split(r"(?<=[.!?])\s+", protected_text) 
        if len(s.strip()) > 20
    ]
    
    if not raw_sentences:
        return [(passage_text, 0.0)]
        
    sentence_matrices = colbert_embedder.embed_sentences(raw_sentences)
    
    scored_sentences = []
    for sentence, sent_matrix in zip(raw_sentences, sentence_matrices):
        # ColBERT MAX_SIM: Sum of maximum cosine similarities across query tokens
        similarity_matrix = colbert_query_matrix @ sent_matrix.T
        max_sim_score = float(np.sum(np.max(similarity_matrix, axis=1)))
        scored_sentences.append((sentence, max_sim_score))
        
    scored_sentences.sort(key=lambda item: item[1], reverse=True)
    return scored_sentences[:top_n]

On the default anticipatory-bail query, the retrieved Constitution Bench precedent (Sushila Aggarwal v. State (NCT of Delhi), 2020) contained a 60-word holding. Local ColBERT MaxSim isolated:

“The court held that the protection granted to a person under Section 438 CrPC should not be limited to a fixed period, and it can continue till the end of the trial.”

That is 60 words to 32 words (46.7%) with zero LLM API calls. For detailed strategies on text splitting and boundary preservation, see our guide on RAG chunking visualizer workbench.


Beyond individual query execution, legal research tools require instant corpus aggregation across statutory categories.

Live Statute Faceting

Using Qdrant 1.19 client.facet, JurisBoost aggregates precedent frequency distributions across 2,034 cases. Raw top keys include noisy section:* values (for example section:3 at 150) where the 80-character act window missed; filtering to canonical act:section keys puts ipc:302 first at 296.

facet_result = client.facet(
    collection_name="nyayarag_legal_precedents",
    key="legal_references",
    limit=10,
)

for hit in facet_result.hits:
    print(f"Statute: {hit.value:<12} | Precedents: {hit.count}")

Grouped Precedent Diversity

When presenting legal precedent retrieval results to counsel, returning five cases that all interpret the same statutory clause reduces variety. We use query_points_groups to retrieve balanced sets across statutory boundaries:

grouped_results = client.query_points_groups(
    collection_name="nyayarag_legal_precedents",
    query=dense_query_vector,
    using="dense",
    group_by="legal_references",
    group_size=2,
    limit=6,
    with_payload=True,
)

group_size=2 and limit=6 asks for two hits in each of six statute groups. The showcase then drops noisy section:* keys and prints the first four canonical act:section groups. For developers exploring programmatic query structures, see QQL Go retrieval operations and our browser-based QQL WASM playground.


Research Sweeps and Parameter Tuning

To confirm our default configuration choices for legal precedent retrieval, we executed parameter sweeps across all 22 benchmark queries:

  1. RRF Smoothing Factor (k): Sweeping k from 2 to 10 lifted standalone M2 Hybrid Top-1 from 10/22 to 14/22. In the full M4 pipeline, accuracy remained at 19/22 because ColBERT and Formula terms already compensate for ranking rank variance.
  2. Weighted RRF: Setting weights to [0.6, 1.4] (favoring BM25 lexical hits) increased M2 performance to 15/22 while keeping M4 stable at 19/22.
  3. Formula Domain Boost: Testing domain_boost_scale showed:
    • Scale 1.0: 13/22 Top-1 (statutes under-weighted)
    • Scale 6.0: 19/22 Top-1 (optimal validation peak)
    • Scale 9.0: 18/22 Top-1 (statutes over-weighted, suppressing natural language text)

We maintain k=2, equal weights, and domain=6.0 as reliable defaults.


Frequently Asked Questions

Qdrant executes nested prefetch stages inside a single client.query_points invocation. It filters candidate vectors with binary-quantized dense and BM25 representations, rescores top candidates with multi-vector ColBERT MaxSim on cold disk, and applies a server-side FormulaQuery to compute final ranks.

Why use ColBERT MaxSim instead of an external cross-encoder?

External cross-encoders require sending text payloads over the network to secondary GPU endpoints, introducing 50 to 100 ms of latency and additional operational failure points. ColBERT MaxSim computes token-level matrix dot products directly inside the database engine without external network hops.

How does statutory concordance bridging resolve old and new Indian laws?

JurisBoost indexes historical precedents with dual statutory tags: original citations (such as crpc:438) and mapped citations (such as bnss:482). When a user queries under a new code, Qdrant’s FormulaQuery matches the mapped concordance terms to boost relevant historical case law.

What is the latency cost of running three stages in Qdrant?

In our benchmarks across 2,034 precedent records, the baseline dense search executed in 2.4 ms p50. The complete M4 pipeline (Hybrid RRF prefetch + ColBERT top-40 rescore + FormulaQuery) executed in 13.2 ms p50. This small database-level cost saves hundreds of milliseconds in downstream LLM processing time by eliminating irrelevant context tokens.


References

Footnotes

  1. Qdrant Team. Qdrant 1.19 Release Notes: Multi-vector enhancements and FormulaQuery support. Qdrant 1.19.x(opens in a new tab)

  2. Akshay Kumar Sharma. How Qdrant Reduced RAG Token Costs by 67% with Native ColBERT Reranking. Towards AI, 2024. Towards AI Article(opens in a new tab)

  3. Akshay Kumar Sharma. legal-rag: Optimizing RAG Token Costs in Legal Discovery with Qdrant. Cappybara12/legal-rag(opens in a new tab)

  4. Shubham Kumar Nigam et al. NyayaRAG: Benchmark for Legal Retrieval-Augmented Generation in Indian Courts. NyayaRAG Repository(opens in a new tab)

  5. Gordon V. Cormack, Charles L. A. Clarke, and Stefan Büttcher. Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods. SIGIR 2009. DOI.1145/1571941.1572114(opens in a new tab)