Advanced Legal Retrieval
Summary
Keyword, hashing-vector, and hybrid search on the same question, plus the SQL a pgvector store would run.
Companion code: 03-advanced-retrieval in indian-legal-ai-lab.
Previous: Part 2: Simple RAG over Indian Court Judgments
One similarity score is a weak search over case law. Court, year, and the words in the query are part of the request.
Filters run before scoring
`SearchFilters` has court, year, case id, and a synthetic flag. `IndexedChunkFilter.Apply` drops rows that do not match. A year filter cannot be rescued by a high vector score, because those rows are already gone. `Year_filter_excludes_other_years` searches `freedom speech` with `Year = 2024` and expects every hit to be from 2024, which in this corpus means the fictional fixtures.
Three scorers, one index
`LegalIndex.Search` accepts `keyword`, `vector`, or `hybrid`.
`KeywordScorer` counts how many distinct query tokens appear in the chunk. `ScoreVector` embeds the query and calls `VectorMath.Cosine`. Hybrid divides each keyword score by the max in the candidate set, then mixes them:
var keyword = max <= 0 ? 0 : keywordScores[i] / max;
var vector = _math.Cosine(embedded, rows[i].Vector);
yield return ((0.55 * keyword) + (0.45 * vector), rows[i].Chunk, rows[i].Record);Anything with a score of 0 is dropped. `PassageFormatter` flattens whitespace and cuts the display text at 420 characters. The chunk offsets stay on the object.
What the lab printed
`dotnet run --project labs/03-advanced-retrieval` asked `Kharak Singh domiciliary visits`.
| Method | Top case | Score |
|---|---|---|
| keyword | 1962 INSC 389 | 4.000 |
| vector | SYN-INJECT-001 | 0.316 |
| hybrid | 1962 INSC 389 | 0.631 |
`SYN-INJECT-001` is a fictional fixture. Vector-only search ranked it first. Hybrid search put Kharak Singh first for this query. That is one query, not a general proof that hybrid search is always better.
PostgreSQL, not executed here
`PgVectorSql.InitScript` creates the `vector` extension and a `judgment_chunks` table with `embedding vector(64)`. `SearchSql` filters on court and year and orders by cosine distance. The unit test only checks that the SQL text contains those parameters. Docker was not available when this was built, so the statement was not run.
BM25
`Bm25Scorer` is a fourth method, `bm25`. On `Kharak Singh domiciliary visits` it returned `1962 INSC 389`. That is the same top case as keyword and hybrid for this query, and it is one query.
Next
Part 4: Function Calling and Legal Research Tools
Verified in this revision
BM25 remains a separate method (`K1` 1.2, `B` 0.75). Hybrid is still 0.55 keyword plus 0.45 hash cosine. The semantic HTTP embedder does not replace the index unless a caller uses `EmbeddingService` and rebuilds vectors. pgvector SQL is still text that this machine has not executed.
More in Applied AI
Simple RAG over Indian Court Judgments
Chunk a versioned judgment manifest, embed it locally, and return the passage with its character offsets.
Function Calling and Legal Research Tools
Deterministic C# tools for case metadata, passages, and a short constitutional catalog, with rejected blank input.
LLM Fundamentals in C#
A supplied judgment can be summarized only from passages you pass in. The same client, with no passage, names a citation that the catalog rejects.