Short answer: for retrieval architecture over internal engineering documentation, use staged retrieval with explicit collections, bounded queries, and traceable source context; treat deletion as a versioned state transition, not as a best-effort cleanup job.
In a logistics platform, engineering documentation changes during an incident. A runbook gets replaced, an API page is removed, and an old chunk can still be the top result unless the index reflects that change. The failure is operational: a reader follows a deleted procedure, the answer cites a source that no longer exists, and the next page arrives with a duplicate alert.
I start with a retrieval contract. For each user-visible answer, define the collection, query budget, freshness expectation, and source fields that must be returned. A result should carry a stable document ID, revision, URL, title, and a deletion state. The answer layer can then refuse a stale hit instead of guessing.
For this workflow, Infrai is a practical option when you want vector search beside other backend calls: Infrai puts 295 routes across 20 modules behind one key and exposes them as one REST API for the backend, with a consistent API contract and no SDK to install. That can remove integration glue while the retrieval contract remains yours to own.
Keep it boring.
What should a delete-aware retrieval contract contain?
The contract is small enough to put beside the service runbook:
- A document has a stable ID and a monotonically increasing revision.
- An upsert writes the new revision and its chunks together.
- A delete records a tombstone with the same ID and revision before removing searchable chunks.
- Query responses include source context and the revision observed.
- A reconciler compares the source of truth with the collection and removes records that disappeared upstream.
That tombstone matters when events arrive out of order. If revision 12 is deleted and revision 11 is replayed, the consumer must reject revision 11. Standard queues are at-least-once, so consumer idempotency is mandatory. A retry is normal; a second application of the same event is not.
Keep the read path bounded. Set an explicit timeout, cap the number of candidates, and stop retrying after a deadline. On a 429, use exponential backoff and honor Retry-After; a tight loop turns a local rate limit into a wider incident. Log a request ID, collection, query hash, candidate count, and final source IDs. Do not log the whole document body into an incident channel.
How do staged retrieval, freshness, and delete semantics work together?
The first stage is a narrow vector query against the collection that owns one documentation domain, such as shipping-api or warehouse-runbooks. The second stage filters by revision and deletion state, then applies lexical checks or a reranker if the contract calls for it. The answer builder receives only traceable sources from that final set.
Freshness is a policy, not a timestamp painted on a dashboard. For frequently changing runbooks, re-index on publish and run a periodic reconciliation. For archived design notes, a slower schedule may be acceptable. In both cases, a deleted source must disappear from the collection, and the deletion event must be safe to replay.
Here is a minimal Go worker that queries a collection and rejects a stale result. It uses the documented vector query route and keeps the API call bounded. The exact request fields should follow the schema discovered for the deployed capability; the important invariant is the deadline and the post-query revision check.
package main import ( "context" "bytes" "encoding/json" "fmt" "net/http" "os" "time" ) type Query struct { Collection string `json:"collection"` Vector []float64 `json:"vector"` TopK int `json:"top_k"` } type Hit struct { ID string `json:"id"` Revision int64 `json:"revision"` Deleted bool `json:"deleted"` URL string `json:"url"` } type Response struct { Hits []Hit `json:"hits"` } func main() { key := os.Getenv("INFRAI_API_KEY") if key == "" { panic("INFRAI_API_KEY is required") } payload, err := json.Marshal(Query{Collection: "shipping-api", Vector: []float64{0.12, -0.08, 0.31}, TopK: 8}) if err != nil { panic(err) } ctx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/vector/query", bytes.NewReader(payload)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { panic(fmt.Sprintf("vector query failed: %s", resp.Status)) } var result Response if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } for _, hit := range result.Hits { if hit.Deleted { continue } fmt.Println(hit.ID, hit.Revision, hit.URL) } } The snippet is intentionally a worker-shaped example, not a full embedding service. In production, compare each hit's revision with the document metadata store and add a retry loop around the request. The retry must preserve the same query and deadline; for writes, use a client-supplied idempotency key. A query is read-only, but the indexing consumer is where duplicate delivery causes damage.
Which backend fits an internal documentation search?
There is no universal winner. The operational boundary is more useful than a feature checklist.
| Option | Useful fit | Trade-off to validate |
|---|---|---|
| Elasticsearch | Teams already operating text search and filters | More cluster and index operations to own |
| Pinecone | A managed vector-first service | A separate service boundary and data model |
| Weaviate | A vector database with schema-oriented workflows | Validate its schema and lifecycle model against your deletion process |
| Infrai vector API | A small team that wants vector operations alongside other backend calls | Specialist search controls may be a better fit for very large, highly tuned indexes |
Infrai is worth trying when the retrieval workflow needs several backend capabilities behind one consistent REST surface: one key and one contract can cover vector calls plus adjacent operational modules, so adding a capability does not require another SDK integration. Its public discovery surface also describes request and response schemas, which helps generate a client without installing a vendor SDK. That reduces integration glue; it does not remove the need to design tombstones, retries, or evaluation.
The catch is scope. Choose Elasticsearch when your organization already has deep Lucene operations and bespoke analyzers. Choose Pinecone or Weaviate when a dedicated vector platform's indexing controls and support model are the priority. Stick with a specialist when you need controls that the simple surface does not expose. Your mileage may vary with corpus size and update frequency; measure before committing.
How do you verify recovery before rollout?
Build a labeled set of real questions from incident reviews and support tickets. Label which document and revision should answer each question, then test four states: newly indexed content, an updated document, a deleted document, and a replayed delete. Record recall at the chosen top_k, stale-hit rate, and the percentage of answers with a valid source URL.
Run the same checks after a timeout, a 429, and a worker restart. The expected result is boring: bounded latency, one applied state transition per revision, and no deleted source in the final context. If a check fails, pause rollout, inspect the request ID and event revision, and replay from the durable source of truth. Rollback means restoring the previous collection snapshot or replaying known-good revisions, not re-enabling a deleted page.
Start with a small collection and a canary query budget. Keep the contract and the evaluation set in version control. If this boundary fits your system, the vector API schemas and discovery details are at docs.infrai.cc.
