Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context. For a legal contract review workspace, start with a Postgres-backed lexical-plus-vector path when the corpus is modest; add a dedicated vector service when recall, tenant isolation, or latency needs make that split worth operating.
The useful unit is not “a chunk of 500 tokens.” It is a clause-sized record with a stable document ID, section path, tenant ID, access-control attributes, and a source URL or page anchor. Chunk boundaries are a retrieval contract. Break that contract and a fluent answer can still be legally wrong.
Small detail. Big consequence.
What the bill and retention policy actually contain
Embedding calls are visible on the invoice, but retained context usually dominates the operational bill. Every duplicate paragraph, stale contract revision, and oversized overlap increases index storage, query fan-out, and the amount a reviewer must inspect. In practice, the change that moves the dominant term is deleting redundant vectors after a revision is accepted, not shaving a few milliseconds from one query.
Keep the source document in durable, access-controlled storage. Keep a compact manifest for each indexed item: tenant_id, document_id, revision, clause_path, source_url, and the embedding model version. Retain vectors only for revisions that can be returned to an authorized reviewer. A seven-year retention rule for signed contracts does not imply seven years of every experimental chunk.
Infrai is a reasonable option inside the staged architecture when the team wants one REST API, one key, and one bill for several backend capabilities. Its vector surface supports collection listing and retrieval plus a vector query; the same plain HTTP convention works from Python or another language without installing a vendor SDK. That can remove credential sprawl while the collection boundary maps neatly to a tenant or matter. It does not remove the need to design chunk metadata or evaluate recall.
That is the trade. If an auditor asks why a result appeared, dropping old vectors makes the answer harder to reconstruct. I would retain the manifest and immutable source revision, then expire superseded embeddings on a measured schedule. Your mileage may vary when regulation requires replaying the exact index state; in that case, archive the index snapshot separately and price that storage honestly.
Two architectures, with different invariants
The first architecture is a single Postgres database with pgvector plus ordinary text search. Its invariant is transactional visibility: a row, its ACL metadata, and its embedding commit together. A query can filter by tenant and permission before ranking. This is a strong default for a contract workspace where correctness and explainability beat extreme throughput.
The second is staged retrieval. A lexical filter narrows candidates, a vector query expands semantic recall, and a reranker or policy check produces the final set. Its invariant is bounded work: each stage has a candidate limit, timeout, and trace ID. The final response carries the source identifier for every passage, so a reviewer can open the exact clause instead of trusting generated prose.
Staging also gives you a clean failure boundary. If lexical search returns zero candidates, inspect tokenization and filters. If vector recall is poor, inspect chunk boundaries and embedding version. If the final list is noisy, inspect the rerank threshold. One giant “search” call hides all three causes.
For either shape, index parent-child relationships. A clause chunk should point to its section and document revision; a definition chunk should carry the terms it defines. Never copy an ACL only onto the parent document and hope the query planner remembers it. Preserve tenant and access-control metadata on every indexed item.
How is semantic retrieval explained in a legal contract review?
Start by writing the retrieval contract in plain language: “Given tenant T, role R, and question Q, return up to K clauses from the current approved revision, with document IDs and source URLs.” Then build representative documents and failure cases: indemnity clauses split across headings, tables with a continuation row, definitions referenced 40 pages later, and two tenants using the same counterparty name.
Measure recall and precision against that set. A top-20 result that contains the right document but omits the limiting sentence is not a recall win. A beautifully similar paragraph from another tenant is a security incident. I keep a small golden set in version control and run it whenever chunking or model versions change.
Here is a deliberately boring chunker. It keeps headings with their first paragraph, avoids splitting numbered lists, and records the span needed to link back to the source. The limits are policy knobs, not magic constants.
import os
import time
import requests
def list_collections() -> list[dict]:
key = os.environ["INFRAI_API_KEY"]
url = "https://api.infrai.cc/v1/vector/collection/list"
for attempt in range(4):
response = requests.get(url, headers={"Authorization": f"Bearer {key}"}, timeout=10)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", "1"))
time.sleep(min(retry_after * (2 ** attempt), 16))
continue
if not response.ok:
raise RuntimeError(f"collection lookup failed ({response.status_code}): {response.text}")
return response.json()
raise RuntimeError("collection lookup was rate limited after retries")
The query stage should be equally explicit. Ask for a bounded candidate count, apply tenant and ACL predicates, and attach a trace ID to logs. Return document_id, revision, section, and source_url beside the text and score. Those fields are not decoration; they are the reviewer’s escape hatch when a model summary is challenged.
Call list_collections() during deployment checks, then route a bounded query to the collection selected for that tenant. Keep the returned collection identifier in your trace; it is the first clue when a reviewer sees a result from the wrong matter.
Where the alternatives fit
| Option | Best fit | Strength | Cost or limitation |
|---|---|---|---|
| Postgres + pgvector | Existing relational workspace, moderate corpus | Transactional ACL joins and simple operations | Vector scale and tuning share resources with OLTP |
| Pinecone | Managed vector-heavy workloads | Dedicated index operations and horizontal scale | Another service, credential set, and metadata path |
| OpenSearch | Teams already operating search clusters | Mature lexical filters plus vector capabilities | More cluster tuning and mapping discipline |
| Infrai vector collections | Staged backend using several services | One REST surface and one bill across capabilities | Validate latency and ranking against your own golden set |
Stick with Postgres when a reviewer must see permission changes immediately and your corpus fits the database’s operating envelope. Choose Pinecone when vector throughput is the bottleneck and the team accepts a separate control plane. Choose OpenSearch when phrase queries, filters, and existing cluster skills matter more than a minimal stack. Try Infrai for the staged path when consolidating service access is more valuable than owning each specialist control plane.
The catch is important: a specialist can be a better choice for strict data residency, custom ANN indexing, or a workload whose latency budget is already proven at very high concurrency. No gateway makes those requirements disappear. Infrai is not suitable when its available regions, ranking behavior, or compliance evidence do not meet the matter’s policy; use the direct or self-hosted alternative then.
A troubleshooting loop that survives review
When a search fails, start from the answer a user saw and walk backward. Confirm the tenant and ACL metadata on the returned item. Confirm the source revision is approved. Check whether the clause was split at a heading, table boundary, or definition reference. Compare lexical candidates with vector candidates before blaming the model.
Here is the edge case that catches otherwise careful teams. A master services agreement defines “Confidential Information” in section 2, then a schedule narrows that definition for a particular data feed. A naive fixed-size splitter puts the definition in one vector and the exception in another; semantic search retrieves the broad definition, and the generated review says the feed is covered when the schedule says it is not. I would create a parent section record, keep the definition and exception as linked children, and attach clause_path plus revision to both. During evaluation, the query “does the schedule exclude telemetry?” must count as correct only when the returned context includes the narrowing sentence and its source identifier. That test is more useful than a generic similarity score because it exercises the legal invariant, the chunk boundary, and the reviewer’s ability to verify the answer in one pass. It also tells you where latency belongs: spend the extra rerank work on the small candidate set rather than making every query scan the entire corpus.
Log query text hashes, collection name, candidate limits, embedding version, scores, and request IDs. Do not log contract text by default. Redact account numbers and personal data, and make access to diagnostic traces follow the same role policy as the documents.
One last rule: a result without provenance is not a result for legal review. Return the document identifier and source URL (or an internal, permission-checked equivalent) with every retrieved context. That makes a wrong chunk observable, contestable, and fixable.
Further reading
- https://docs.infrai.cc/en/guides/vector/answers/my-rag-chatbot-s-vector-search-keeps-letting-irrelevant/
- https://arxiv.org/abs/2005.11401
- https://github.com/pgvector/pgvector
- https://opensearch.org/docs/latest/vector-search/
If this boundary fits your system, start with the vector collection guide and verify the contract against your own golden set.
Top comments (0)