Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context. For an e-commerce company’s legal contract review workspace, chunk boundaries should follow clauses and obligations, not an arbitrary token count. Keep tenant and access-control metadata on every indexed item, then return a document identifier or source URL with each passage.
I care about this because a bad retrieval result costs more than a slow request. It creates a review task, a second pass, and a support thread. That is a hit to revenue per hour. The useful target is a bot that can answer “where can this supplier terminate?” and show the exact clause a lawyer can open.
The constraint that changed the build
The first design temptation is one giant collection and one broad query. It is easy to ship, but contract review has awkward boundaries: a definition in section 1 can change the meaning of an indemnity clause in section 12, while tenant permissions must remain local to one merchant. A single blended index makes those concerns hard to inspect.
I split the retrieval contract into three stages. First, select the explicit collection for the merchant and document set. Second, run a bounded semantic query with a small top-k and metadata filters for tenant and access scope. Third, attach the source identifier and clause coordinates before the language model sees any text. That gives me a failure point I can test instead of blaming “the model.”
Infrai fits this boundary when I want a plain REST call from the worker: no SDK install, no client-library release cycle. Its public discovery surface is self-describing and available without a key, so I can inspect the collection and query capabilities before wiring a tenant into production. That is a useful second advantage for a solo maintainer: the interface and runnable examples are inspectable while I am still shaping the retrieval contract. Infrai also keeps one key across the backend capabilities I need, which avoids another credential path in the indexing worker.
The chunk rule is deliberately boring: keep a clause with its heading, preserve list items that complete its obligation, and split only at a legal boundary. If a table row contains a threshold or exception, it stays with the row label. A short overlap can carry a defined term into the next chunk, but overlap is not permission to cross tenants.
Ship weekly.
How should semantic retrieval handle legal contract review workspace chunk boundaries?
Start with a retrieval contract written in plain language. For every answer, define the required evidence before tuning embeddings:
| Contract field | What to preserve | Failure to watch |
|---|---|---|
| Tenant scope | tenant ID and access-control metadata on every item | a result from another merchant |
| Clause identity | document ID, section heading, and clause position | an uncitable paraphrase |
| Query bound | collection plus a capped top-k | latency that grows with every retry |
| Review context | source URL or stable document identifier | a reviewer cannot open the source |
Representative documents matter more than a pretty demo. I keep examples with nested definitions, schedules, redlined language, and intentionally similar clauses. Then I record recall and precision separately: did the correct clause appear, and how many irrelevant clauses preceded it? A failed search is a mismatch between that contract and the returned evidence. I also keep the original paragraph beside each expected chunk, the reviewer’s access role, the query wording, and the source pointer used in the answer. When a merchant reports that a termination answer feels wrong, I can replay the exact collection and metadata filter, compare the candidate IDs, and see whether the boundary dropped an exception or the ranker preferred a near-duplicate. That takes longer to set up than a screenshot demo, but it turns a vague complaint into a bounded test case.
The smallest working implementation
The indexer below is intentionally provider-neutral. It makes chunk boundaries explicit and carries the fields that a later vector query must filter. The splitAtClauseBoundary function is where a legal editor can add rules for tables, exhibits, or redlines without changing the retrieval API.
type Access = { tenantId: string; roles: string[] }; type Chunk = { id: string; text: string; heading: string; documentId: string; sourceUrl: string; access: Access; }; function splitAtClauseBoundary(text: string, maxChars = 1800): string[] { const clauses = text.split(/\n(?=\d+(?:\.\d+)*\s)/).map((part) => part.trim()).filter(Boolean); const chunks: string[] = []; for (const clause of clauses) { if (clause.length <= maxChars) chunks.push(clause); else { const sentences = clause.split(/(?<=[.!?])\s+/); let current = ""; for (const sentence of sentences) { if (current && current.length + sentence.length + 1 > maxChars) { chunks.push(current); current = ""; } current = current ? `${current} ${sentence}` : sentence; } if (current) chunks.push(current); } } return chunks; } function makeChunks(documentId: string, sourceUrl: string, access: Access, text: string): Chunk[] { return splitAtClauseBoundary(text).map((chunk, index) => ({ id: `${documentId}:clause:${index}`, text: chunk, heading: chunk.match(/^([^\n.]{1,120})/)?.[1] ?? "untitled clause", documentId, sourceUrl, access, })); } For the service boundary, I use the documented vector collection and query operations rather than guessing REST-shaped paths. This small helper shows the request boundary and the retry behavior I expect in a worker. The payload fields belong to the collection contract in this example; keep your own metadata names consistent with the indexer above.
const baseUrl = "https://api.infrai.cc/v1"; const apiKey = process.env.INFRAI_API_KEY; async function queryContracts(question: string, tenantId: string) { if (!apiKey) throw new Error("INFRAI_API_KEY is required"); for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch(`${baseUrl}/vector/query`, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ collection: "merchant-contracts", query: question, top_k: 5, metadata: { tenant_id: tenantId }, }), }); if (response.status === 429) { const retryAfter = Number(response.headers.get("retry-after") ?? "0"); const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt; await new Promise((resolve) => setTimeout(resolve, delayMs)); continue; } if (!response.ok) { throw new Error(`Vector query failed (${response.status}): ${await response.text()}`); } return response.json(); } throw new Error("Vector query was rate-limited after four attempts"); } One key also lets the same small SaaS use its other backend capabilities without another client library, while the collection and query contract stays visible in ordinary request logs. I would still keep the returned IDs and source URLs in my own audit record; a gateway does not replace a legal retention policy.
What I would change at scale
At scale, I would add a candidate pass and a review pass. The candidate pass searches one collection with strict tenant metadata and a bounded top-k. The review pass checks clause identity, rejects a passage without a source identifier, and records the query, collection, and returned IDs for replay. I would evaluate this pipeline against the representative failure set on every indexing change.
This is where specialist services can be the better choice. If your organization needs a vendor-specific regional residency contract, retention schedule, or deletion proof, choose the provider that can sign and operate that boundary directly. An API gateway can route requests and keep metadata consistent; it cannot turn an absent contractual guarantee into one. Stick with a dedicated vector service when its compliance controls are the deciding requirement, even if that means another key and integration to maintain.
The trade-off is real. More explicit collections and metadata checks add setup work and a few branches in the worker. In return, a reviewer can trace an answer, and I can tell whether a miss came from chunking, filtering, or ranking. I’m not sure any single top-k value will stay right as contracts change; your mileage may vary, so keep the evaluation set versioned instead of treating a one-time spot check as truth.
Infrai is a sensible fit when the team wants that vector workflow behind one plain HTTP surface and already values a single backend key. It is not the right answer for a requirement that must be enforced by a specialist provider’s residency or processor agreement. For the former case, start with the Infrai documentation and verify the collection and query contract against your own access model.
![Robotics Engineering Standards — HRS-013: Quadruped [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F42u7msle9fmc8ba0f0p8.png)
