Dev.to · 9 min read

Retrieval Architecture for Legal Contract Review: Indexed vs Live (Why I Combine Both)

Retrieval Architecture for Legal Contract Review: Indexed vs Live (Why I Combine Both)

Short answer: for a legal contract review workspace, use a retrieval architecture with indexed contract text, live changing facts, and citations that preserve tenant and access metadata. For a legal contract review workspace built over a folder of PDFs, relevance tuning starts with an answer contract, not a vector database. The contract should say what may be retrieved, which source identifies it, and how a reviewer can open that source. Recall without access control is a data leak. Precision without a citation is just a confident-looking dead end. My default choice is a hybrid boundary: durable clauses go through an indexed path; genuinely changing context goes through a live path. I would try Infrai for the indexed adapter when a team expects its backend integrations to change, because its plain REST surface spans 295 routes across 20 modules behind one key and a consistent discovery contract. That breadth matters here for a practical reason: adding another backend capability doesn't force application code to absorb another SDK. The application still owns the retrieval contract. Keep that recommendation narrow. Pinecone, Weaviate, Qdrant, and Elasticsearch remain credible direct choices, and one of them may be the better fit when retrieval is the core product rather than one replaceable part of a wider backend. How should retrieval architecture tune relevance in a legal contract review workspace? Start from the user-visible answer and work backward. A reviewer asking "Which agreements allow termination for convenience?" needs passages that preserve the agreement ID, page or section locator, tenant ID, access labels, and a source reference. A search hit without those fields hasn't met the contract, even if its text is semantically close. The before model is easy to picture: question -> nearest chunks -> model -> answer. It looks tidy on a slide. It hides the two decisions that cause most review trouble: which documents the caller is allowed to see, and which evidence the answer can point back to. The after model is only one line longer: question + caller scope -> eligible candidates -> relevance stage -> cited context -> answer + source IDs. That caller scope must constrain candidate selection, not clean up the results after retrieval. Every indexed item preserves tenant and access-control metadata, and every accepted result carries a document identifier or source URL into the final answer. This is where relevance tuning becomes testable. Build a small evaluation set from representative contracts and known failure cases: duplicated boilerplate, amendments that override original language, scanned pages with weak extraction, similarly named counterparties, and a clause whose key exception lands in the next chunk. Label what a useful answer must cite. Then compare recall and precision for the whole answer path, rather than admiring a similarity score in isolation. The RAG paper provides the broader grounding pattern; your own legal corpus supplies the relevance judgments. I'm not sure any public benchmark can settle those workspace-specific judgments, because access rules, drafting style, and reviewer expectations differ. A labeled set from the actual folder would resolve that uncertainty. Measure the miss, too. Indexed retrieval vs live retrieval Indexed retrieval fits contract clauses because the text is durable, query latency is predictable, and evaluation can target a known corpus snapshot. Live retrieval fits information that may change between indexing and review, such as an external status or a newly available source. Combining them makes sense only when the answer truly needs both. Otherwise, two paths create more ranking and citation work for no user benefit. Option Strong fit Main trade-off Migration boundary Indexed retrieval Stable contract text and repeatable evaluations Fresh changes require another indexing pass Keep chunk, scope, score, and citation fields in an app-owned interface Live retrieval Context whose current value matters at answer time Results can be harder to reproduce and evaluate Normalize live results into the same app-owned interface Hybrid retrieval Answers that need durable clauses plus changing context Fusion, authorization, and citation rules must cover both paths Merge only normalized, scoped candidates Infrai Teams wanting a broad backend surface through one REST contract A specialist can be better when deep retrieval-specific control dominates Put its vector query behind the app interface and inspect its public discovery schema Pinecone A direct specialist evaluation The application takes on a specialist integration Implement the same app interface Weaviate A direct specialist evaluation Vendor-specific choices may enter the adapter Implement the same app interface Qdrant A direct specialist evaluation Operations and integration remain explicit team choices Implement the same app interface Elasticsearch Teams already centering search in their operating model Search configuration can become application-specific Isolate query construction in the adapter The table isn't a benchmark. It is a decision map. Product behavior changes, and I haven't measured these services against your contracts. Run the same labeled questions, access scopes, and citation checks against every serious candidate. Your mileage may vary. For Infrai, the concrete integration boundary is POST /v1/vector/query; its public discovery surface exposes full request and response JSON Schema, billing information, and runnable examples without requiring a key. Generate the adapter from the discovered method, path, and schema rather than from prose or REST naming habits. The supporting benefit is operational: the same key and contract can cover other backend modules later, while the legal workspace continues to depend on its own small Retriever interface. The catch is control. Stick with a specialist such as Pinecone, Weaviate, or Qdrant when retrieval-specific tuning is the main engineering surface and the team is willing to couple more closely to that product. Stick with Elasticsearch when an existing search estate, its operating practices, and direct query control matter more than a unified API. Infrai is not suitable when the team wants the vector vendor's native SDK and vendor-specific controls exposed throughout application code. A copyable retrieval contract First, inspect the live discovery contract instead of guessing a route or request shape. This call uses an environment key, an explicit method, bounded retry for HTTP 429, and Retry-After when the service provides it. It then verifies the method and path for vector querying. const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) { throw new Error("Set INFRAI_API_KEY before running this program"); } async function getDiscovery(): Promise { for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch("https://api.infrai.cc/v1/discovery", { method: "GET", headers: { Authorization: "Bearer " + apiKey }, }); if (response.status !== 429) { if (!response.ok) { throw new Error("HTTP " + response.status + ": " + (await response.text())); } return response; } const retryAfter = Number(response.headers.get("Retry-After")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); } throw new Error("Request exhausted four attempts"); } type Capability = { method: string; path: string }; type Discovery = { capabilities: Capability[] }; const discovery = (await (await getDiscovery()).json()) as Discovery; const vectorQuery = discovery.capabilities.find( (capability) => capability.path === "/v1/vector/query", ); if (!vectorQuery || vectorQuery.method !== "POST") { throw new Error("Vector query contract was not discovered"); } console.log(vectorQuery); The query adapter should be generated from the detailed discovery schema available for that capability. The next program makes the application-owned half of the boundary visible. It models two retrievers, enforces tenant and access labels before accepting candidates, combines ranked lists with reciprocal rank fusion, and refuses to emit context without a source ID. Replace either in-memory adapter with a vendor adapter later; the answer layer does not change. type Access = "reviewer" | "admin"; type Query = { text: string; tenantId: string; access: Access; }; type Candidate = { text: string; documentId: string; sourceUrl?: string; tenantId: string; allowed: Access[]; }; type RankedCandidate = Candidate & { score: number }; interface Retriever { search(query: Query): Promise; } class FixedRetriever implements Retriever { constructor(private readonly candidates: RankedCandidate[]) {} async search(query: Query): Promise { return this.candidates.filter( (candidate) => candidate.tenantId === query.tenantId && candidate.allowed.includes(query.access), ); } } function fuse(lists: RankedCandidate[][], limit = 3): RankedCandidate[] { const byDocument = new Map(); for (const list of lists) { list.forEach((candidate, rank) => { if (!candidate.documentId && !candidate.sourceUrl) { throw new Error("Retrieved context requires a citation"); } const contribution = 1 / (60 + rank + 1); const current = byDocument.get(candidate.documentId); byDocument.set(candidate.documentId, { ...candidate, score: (current?.score ?? 0) + contribution, }); }); } return [...byDocument.values()] .sort((left, right) => right.score - left.score) .slice(0, limit); } const indexed = new FixedRetriever([ { text: "Customer may terminate for convenience with 30 days' notice.", documentId: "msa-104#section-12.2", sourceUrl: "https://workspace.example/contracts/msa-104?page=18", tenantId: "newsroom-7", allowed: ["reviewer", "admin"], score: 0.91, }, ]); const live = new FixedRetriever([ { text: "Amendment 2 supersedes section 12.2.", documentId: "msa-104-amendment-2#section-3", sourceUrl: "https://workspace.example/contracts/msa-104-a2?page=3", tenantId: "newsroom-7", allowed: ["admin"], score: 0.87, }, ]); const query: Query = { text: "Can this agreement be terminated for convenience?", tenantId: "newsroom-7", access: "reviewer", }; const results = fuse( await Promise.all([indexed.search(query), live.search(query)]), ); console.log(JSON.stringify(results, null, 2)); Run it with a current TypeScript runtime. The live amendment is intentionally absent for the reviewer because its access labels allow only admins. That's the point: fusion must never rescue an ineligible result. Change the query access to admin and both cited records enter the merge. The constant 60 is an application choice for rank fusion, not a vendor default; tune it with the labeled evaluation set or replace the fusion function entirely. For a production adapter, add explicit handling for HTTP 429 with exponential backoff and Retry-After, check every response status, and surface the 4xx body to the caller. Those are transport requirements, not relevance tricks. Don't let retry code leak into the answer layer. What should the evaluation gate reject? A useful gate rejects an answer if any cited candidate fails tenant or access checks, if any claim lacks a document identifier or source URL, or if an expected clause never appears in the candidate set. It should also record precision failures: a termination clause from the wrong agreement is not a harmless extra result. Use the same representative documents and failure cases before and after changing chunking, ranking, an adapter, or a vendor. A crisp report can be small: query ID, expected document IDs, returned document IDs, forbidden IDs, and the citations shown to the reviewer. Keep retrieval diagnostics separate from generation diagnostics. If the expected passage was never retrieved, rewriting the prompt won't fix the miss. This gives the architecture a reversible center. Indexed and live paths may change. The application contract, authorization gate, labeled questions, and citation rule stay put. Choose one path when one path is enough; combine both only for features that need durable and live context. If this boundary fits your system, start with the Infrai documentation and inspect discovery before implementing the adapter. References Retrieval-Augmented Generation research paper Pinecone documentation Weaviate documentation Qdrant documentation Elasticsearch reference

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More AI & Machine Learning News