DEV Community

Cover image for I Rebuilt My AI Legal Assistant After Learning Why Vector-Only RAG Wasn't Enough
Ishwar
Ishwar

Posted on

I Rebuilt My AI Legal Assistant After Learning Why Vector-Only RAG Wasn't Enough

DEV Weekend Challenge: Passion Edition Submission

This is a submission for Weekend Challenge: Passion Edition


What I Built

LawDecoder is an AI-powered legal assistant that explains Indian laws in plain language while showing the exact legal provisions used to generate each answer.

Unlike many dense-only retrieval prototypes, LawDecoder focuses on retrieval quality. It was born out of a real-world failure. One day I asked my original prototype:

"Someone forged my signature."

The first law it retrieved wasn't about forgery. It was about counterfeit coins.

That was the moment I realized the problem wasn't the LLM—it was my retrieval pipeline.

The surprising part: I didn't change the LLM. Nearly all of the massive retrieval accuracy gains came from redesigning the search architecture to combine semantic search, keyword search, Reciprocal Rank Fusion (RRF), and deterministic domain reranking.


Demo

Here is a visual walkthrough of the production UI, showing how the hybrid search handles citations and developer metrics:

1️⃣ User Chat Interface

Clean, legal explanation interface for end users:
LawDecoder Streamlit user chat landing page showing response layout

2️⃣ Structured Offence & Citation Details

Deduplicated citations with developer metrics visible in Developer Mode:
LawDecoder citation view in developer mode displaying RRF ranks and selection reasons

3️⃣ System Evaluation Dashboard

Performance comparisons and technical architecture story:
LawDecoder developer dashboard showing performance latency and accuracy benchmarks comparison table


Code

The complete implementation—including SQLite ingestion, FTS5 indexing, Reciprocal Rank Fusion, evaluation queries, and benchmark samples—is available on GitHub:

⚖️ LawDecoder: A Case Study in Hybrid Retrieval for Legal AI

LawDecoder is an AI-powered legal assistant that explains Indian laws in plain language while showing the exact legal provisions used to generate each answer.

Unlike many dense-only retrieval demos, LawDecoder focuses on retrieval quality. It combines semantic search, keyword search, Reciprocal Rank Fusion (RRF), and deterministic reranking to improve legal citation accuracy.


🚀 Features

  • 🔍 Hybrid Retrieval: Fuses SQLite FTS5 (sparse BM25 keyword matching) and local dense vector embeddings.
  • 🔀 Reciprocal Rank Fusion (RRF): Fuses sparse and dense search rankings to prioritize matches returned by both.
  • 🎯 Domain Reranker: Deterministically demotes irrelevant matches (like counterfeit stamp/coin sections) and boosts direct offences (like document forgery acts) for signature queries.
  • 🧾 Citation Transparency: Shows the exact acts, sections, and selection details for every explanation.
  • 🔧 Developer Mode: Toggle view to inspect RRF ranks and retrieval selection reasons.
  • 🤖 Empathetic AI: Structured…

How I Built It

The Failure: Tracing the Cause

The original vector-only (v1) search failed on a simple forgery query:

Query:
"Someone forged my signature"

Top result (v1):
❌ BNS Section 180 — Possession of counterfeit coin

Expected:
✅ BNS Section 336 — Forgery
✅ BNS Section 340 — Using forged document
Enter fullscreen mode Exit fullscreen mode

The embedding model wasn't "wrong"—it placed semantically close concepts close together in vector space. But in legal search, semantically similar isn't the same as legally relevant.

Because "forgery" and "counterfeit" ended up close in embedding space, the retriever ranked counterfeit coin and government stamp provisions above the actual definition of document forgery. The retriever generalized too aggressively, missing the direct definition of forgery (BNS Section 336) because the word "signature" was semantically distant from generic statutory descriptions of the offence.

Additionally, caching large raw text strings (text, titles, act names) in a JavaScript array caused the Node.js process to consume over 320 MB of RAM at startup.


The Redesigned Retrieval Pipeline

I redesigned the retrieval pipeline to combine sparse and dense search methods.

LawDecoder hybrid retrieval architecture diagram: SQLite FTS5 sparse keyword index and dense vector ONNX pipeline merged via Reciprocal Rank Fusion (RRF) and Domain Reranker

The LLM remained almost unchanged. Nearly all improvements came from redesigning retrieval.

1. SQLite + FTS5 Sparse Indexing

I moved all 4,892 legal sections out of JSON files and persisted them in a local SQLite database. A virtual FTS5 index handles exact-match keyword indexing (BM25 ranking), ensuring precise matches for terms like "Section 65", "forgery", or "signature".

2. Lightweight Vector Memory Cache

I stripped all text metadata from Node.js memory. The startup script now loads only the id (string) and the coordinate list—pre-processed into a compact Float32Array object—into RAM. The actual text content remains on disk in SQLite and is hydrated for the top 5 matched sections on-demand, reducing memory usage by 85% (from 320 MB to 48 MB).

3. Reciprocal Rank Fusion (RRF)

Fuses the top 50 semantic matches (dense) and top 50 keyword matches (sparse) into a single unified list using the reciprocal ranks of both retrievers. Here is the core JS implementation of the RRF merge:

const rrfScores = new Map();
const k = 60; // Standard constant for RRF

// Process vector ranking positions (dense)
vectorRankings.forEach((item, index) => {
  rrfScores.set(item.id, 1 / (k + index + 1));
});

// Process FTS5 keyword ranking positions (sparse) and add to scores
ftsRankings.forEach((item, index) => {
  const existingScore = rrfScores.get(item.id) || 0;
  rrfScores.set(item.id, existingScore + (1 / (k + index + 1)));
});

// Sort matched IDs based on fused RRF score
const mergedRanking = Array.from(rrfScores.entries())
  .sort((a, b) => b[1] - a[1])
  .slice(0, 20); // Top 20 candidates for reranking
Enter fullscreen mode Exit fullscreen mode

4. Domain Reranker (Deterministic Guardrail)

It evaluates the top 20 candidates returned by the RRF step. This is a deterministic rule-based reranker tailored to the legal domain—not a learned neural cross-encoder:

  • If the query is document/signature forgery-related, it checks if a retrieved document is a coin or banknote counterfeit section. If yes, it penalizes the score by 99% (* 0.01).
  • It boosts direct document forgery offences (containing "forgery" or "forged") by 300% (* 3.0).
  • It filters out duplicate sections using content snippet prefixes.
// Heuristic Domain Reranking
if (isDocumentForgeryRelated) {
  const isCoinOrStampOrCurrency = 
    titleLower.includes('coin') || titleLower.includes('stamp') || 
    titleLower.includes('currency') || titleLower.includes('bank-note') ||
    contentLower.includes('coin') || contentLower.includes('stamp') || 
    contentLower.includes('currency-note');

  if (isCoinOrStampOrCurrency) {
    adjustedScore *= 0.01; // heavily penalize counterfeit coin/stamps (reduce by 99%)
  } else if (titleLower.includes('forgery') || titleLower.includes('forged')) {
    adjustedScore *= 3.0; // strong boost for direct forgery definitions/offences
  }
}
Enter fullscreen mode Exit fullscreen mode

Performance & Evaluation Benchmarks

To evaluate the redesign, I assembled a benchmark of 100 manually verified legal queries spanning criminal law, cybercrime, family law, consumer protection, and procedural law.

Metric v1 (Naive Vector RAG) v2.1 (Hybrid Search - Current) Change
Search Engine Dense Vector (Linear JSON scan) Hybrid (SQLite FTS5 + Dense Vector + RRF + Reranker) Major retrieval precision upgrade
Avg. Query Latency 466 ms 12 ms 97.4% speedup
Memory Cache Footprint ~320 MB ~48 MB 85.0% RAM savings
Duplicate Citations Present (up to 40% overlaps) Deduplicated (0% overlaps) Verified
Top-5 Relevant Retrieval Rate ~68% ~91% +23% accuracy gain

Latency is based on 100 benchmark queries. Memory is process-level heap size at startup. Accuracy is evaluated on top-5 target matches using a manually verified benchmark dataset of 100 queries.

None of these improvements required changing the language model. The gains came almost entirely from retrieval engineering. For this benchmark query, the top retrieved references aligned with the expected legal provisions:

  1. BNS 340: Forged document and using it as genuine
  2. BNS 336: Forgery definition and penalty
  3. BNS 339: Possession of forged document
  4. BNS 335: Making a false document
  5. Evidence Act Section 65: Proof of signature and handwriting

Lessons Learned

  • Retrieval quality sets the upper bound for RAG quality: The LLM can only reason over what you retrieve. Improving retrieval turned out to be far more impactful than switching models.
  • Dense embeddings alone are rarely enough for domain-specific search: Hybrid retrieval is often a better default because it combines exact keyword matching with semantic understanding.
  • SQLite + FTS5 is a powerhouse: For corpora under 100,000 documents, SQLite FTS5 and typed arrays in Node.js deliver sub-15ms latency on CPU with zero operational complexity.
  • Simple deterministic rerankers work: They can eliminate domain-specific retrieval errors without requiring another neural model.

What's Next

If I continue evolving this project, the next improvements I'd explore are:

  • Cross-encoder reranking: Integrate lightweight cross-encoders (e.g. BGE reranker) for advanced ranking.
  • Metadata-aware retrieval: Allow users to filter queries by Act or category before searching.
  • Legal Case Retrieval: Expand indexing to cover legal precedents and court cases.
  • Multilingual support: Support query translation for regional languages.

Prize Categories

Best Use of Google AI

LawDecoder integrates the Google Gemini API (gemini-3.5-flash or custom models) for generating context-grounded, empathetic, and structured legal advice derived from the hybrid SQLite-FTS5 retrieval output.

Top comments (0)