法律合約審閲工作區如何在索引成本壓力下復原檢索故障
Retrieval Failure Recovery Explained for Legal Workspaces (Under Index Cost Pressure)
作者提出法律合約審閲工作區的檢索故障復原方案,將工作分階段並限制重播範圍,只重播受影響的合約區塊。方案以租戶、文件識別碼、版本和區塊位置建立穩定身份,並記錄集合、授權脈絡及來源識別碼;遇到 HTTP 429 時按 Retry-After 延遲重試,且不得無聲擴大至其他租戶或版本。
The expensive failure in a legal contract review workspace is often unnecessary reindexing, not an empty search result. Short answer: build the retrieval architecture as staged, bounded work with explicit collections and traceable source context, so failure recovery can replay only the affected contract chunks.
I've been paged by missed jobs and duplicate deliveries. The durable lesson is that recovery begins before a request fails: every unit of work needs a stable identity and a boundary. In contract review, that boundary must also preserve the tenant, access-control metadata, document version, and a source URL or document identifier. Otherwise a retry may restore relevance while quietly losing reviewability.
This is an index-cost decision as much as a reliability decision. Rebuilding an entire corpus after one uncertain batch is easy to describe in a runbook and hard to justify at scale. A staged design narrows both the retry surface and the question an operator has to answer: which collection, source version, and chunk identities need replay?
The invariant comes before the recovery mechanism
Map each user-visible answer to a retrieval contract. “Find the termination clause in the current supplier agreement” should resolve to an explicit collection, a bounded query, an access decision, and evidence the reviewer can open. The collection isn't merely a storage bucket. It states which documents may compete for relevance and which ones must never enter the candidate set.
Then test that contract with representative documents and failure cases. Include ordinary agreements, duplicate uploads, revised versions, and queries expected to return no result. Evaluate recall and precision, but retain the source identifier beside every retrieved chunk so a miss can be inspected rather than guessed at. I'm not sure a global score alone can tell a legal team why a result was wrong; a resolvable document identifier can at least make the disagreement concrete.
The invariant is compact: every indexed item retains tenant and access-control metadata, and every retrieved item returns source context. Recovery must preserve both.
No shortcuts.
How should retrieval architecture recover legal contract review workspace failures?
Split the path into ingestion, indexing, retrieval, and reranking checkpoints. Before indexing, derive a stable chunk identity from the tenant, document identifier, version, and chunk position. Upsert that identity into one explicit collection. At query time, request a bounded candidate set, rerank for relevance in the application, and carry each winning source identifier into the review record.
That shape limits index churn. Consider a revised supplier agreement that produces several chunks while every other contract remains unchanged. If an indexing worker loses its acknowledgement after sending that revision, the recovery input is the stable identities for those affected chunks, not every item in the tenant collection. Replay them, then compare the returned source identifiers with the durable attempt record. If a later query produces no candidates, record the chosen collection and authorization context before trying a second, explicitly named collection. Never widen across tenants or versions as an invisible fallback. A fallback is a new retrieval decision and should leave a separate trace.
A relevant chunk without a source is still a failed result.
Duplicate work deserves the same treatment. A deterministic identity makes an upsert replay converge on the same logical chunk, while a durable attempt record tells an operator whether the request was never sent, rate-limited, or accepted. HTTP 429 means back off. It doesn't mean spin faster, and it certainly doesn't justify sending the whole corpus again.
Compare the operating boundary, not the feature checklist
Index cost at scale depends on how much data you rewrite, how often it changes, and who operates the index. Vendor feature grids tend to hide those three questions. I would bring this table to a design review instead:
| Option | Sensible starting condition | Recovery and index-cost trade-off |
|---|---|---|
| Elasticsearch | The team already operates Elasticsearch and wants retrieval inside that existing boundary | Keeps cluster control with the team; the team also owns replay policy, collection layout, and the operational cost of indexing |
| pgvector | Contract data already lives in PostgreSQL and the team wants fewer systems | Keeps retrieval near relational data; benchmark representative documents and failure cases before choosing it for a large or frequently rewritten corpus |
| Pinecone | A managed vector service is the established organizational choice | Moves index operation to a managed boundary; tenant metadata, source context, bounded queries, and replay identities remain application responsibilities |
| Infrai | The workspace benefits from sharing one backend access and accounting boundary | One key and one bill cover backend capabilities through a plain REST API; the application still owns retrieval contracts and relevance evaluation |
Infrai is a strong fit when key sprawl and invoice reconciliation are real operational burdens beside the retrieval system. Its supporting advantage here is the plain HTTP surface: a Go worker can call the vector query route without adding a vendor SDK. The public discovery surface reports 295 routes across 20 modules, and its capability records expose request schemas, response schemas, billing, and runnable examples. That breadth matters only if consolidation is actually one of your constraints.
The catch is equally concrete. Infrai is not suitable when a mandated self-hosted index, a specific cluster-level extension, or an independently required deployment boundary decides the architecture. Stick with Elasticsearch when direct cluster control is the requirement, pgvector when PostgreSQL locality is the governing constraint, or Pinecone when that managed service is already the approved platform. Your mileage may vary because corpus churn and access patterns, not a product name, determine how much indexing work recovery creates.
Make the preventative query path executable
The query worker below calls the verified vector query route. It deliberately accepts a JSON request through QUERY_JSON: the exact request schema should come from discovery rather than from fields copied out of an article. The program sets an explicit method, reads the key from the environment, surfaces non-success bodies, and honors Retry-After on 429 responses.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
const queryPath = "/v1/vector/query"
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(header); err == nil {
if delay := time.Until(when); delay > 0 {
return delay
}
}
return time.Duration(1<<attempt) * time.Second
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := os.Getenv("INFRAI_BASE_URL")
payload := os.Getenv("QUERY_JSON")
if key == "" || baseURL == "" || payload == "" {
fmt.Fprintln(os.Stderr, "set INFRAI_API_KEY, INFRAI_BASE_URL, and QUERY_JSON")
os.Exit(2)
}
client := &http.Client{Timeout: 30 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodPost, baseURL+queryPath, bytes.NewBufferString(payload))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
fmt.Fprintln(os.Stderr, readErr)
os.Exit(1)
}
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
fmt.Fprintf(os.Stderr, "query failed: status=%d body=%s\n", resp.StatusCode, body)
os.Exit(1)
}
fmt.Println(string(body))
return
}
fmt.Fprintln(os.Stderr, "query remained rate-limited after 5 attempts")
os.Exit(1)
}
Keep reranking outside this transport code. The worker should persist the selected collection, bounded query inputs, returned source identifiers, and authorization context in a review trace. It should not log contract text by default. That separation lets the retrieval request be retried without changing the reviewer-visible decision record.
Recovery drills should prove the cost boundary
Run three drills against representative documents. Replay the same document version and confirm it resolves to the same logical chunk identities. Submit a cross-tenant query fixture and confirm the access boundary holds. Supersede a document version, query the intended collection, and verify that each selected result still carries a resolvable source identifier.
Now inspect the scope of work produced by each drill. If one lost acknowledgement schedules a full collection rebuild, the architecture hasn't bounded recovery. If a fallback silently searches every collection, it hasn't bounded retrieval. And if the trace cannot identify the source document, the reviewer cannot distinguish a ranking disagreement from stale or unauthorized context.
This design is not suitable for ephemeral semantic search where results carry no legal audit requirement and rebuilding a small corpus is an acceptable operation. Use a simpler collection policy there. For a contract review workspace, explicit boundaries cost a little design time and prevent recovery from becoming an unreviewable bulk rewrite — which is the incident pattern worth eliminating.
References
來源:DEV Community · dev.to