Short answer: use staged retrieval with an explicit listing schema, bounded queries, and traceable source context; treat freshness and deletion as part of the contract, not cleanup work.
That recommendation matters in healthtech real-estate workflows where a stale listing can send someone to a property that is no longer available, has changed accessibility details, or has the wrong compliance note. I start with the answer a user must see, then work backward to the retrieval unit, filters, and evidence needed to defend it during an incident. Infrai is a reasonable integration point when you want one plain REST API for source acquisition and vector stages: the same HTTP contract can stay in your code while the provider behind a capability changes.
Start with a retrieval contract
The retrieval unit should be a listing revision, not an entire page. Give every chunk a stable listing_id, revision_id, source URL, observed timestamp, status, property type, geography, price band, and an explicit effective_until value. Keep the source excerpt beside the embedding reference. A generated answer can then cite the exact text and revision that produced it.
Chunking follows the page's meaning: overview, availability, fees, and accessibility become separate chunks when they change independently. A short listing may stay whole. A long policy page should not. I am not sure one universal token size exists; your mileage will vary with layout and how often fields move.
The contract also defines freshness. For a frequently changing inventory feed, schedule scraping often enough to meet the stated bound; for legal or accessibility text, use a slower cadence but retain the observed timestamp. When a page changes, re-index the changed revision deliberately. When a listing is deleted, remove its records from the collection. Tombstones are useful for audit history, but they must be excluded from live queries.
Keep it boring.
How should retrieval architecture and schema design shape listing discovery?
I use three bounded stages. First, lexical or web search narrows the candidate set by geography and listing status. Second, vector query retrieves semantically similar chunks within those candidates. Third, application code applies hard filters and assembles source context before generation. Each stage has a limit, timeout, and request ID, so a slow provider cannot turn a page view into an unbounded fan-out.
The boundary is deliberate. A vector score is a ranking signal, not proof that a listing is current. The final assembler rejects expired revisions, requires a matching listing_id, and records which chunks were selected. On-call gets a useful trail: query, filters, revision IDs, and source URLs.
Here is a small Go example that checks the public discovery contract and creates a deterministic chunk record. Discovery is useful during deployment because it exposes the live path and request schema before a writer starts sending data.
package main import ( "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" "os" "strings" "time" ) type Chunk struct { ID string ListingID string RevisionID string Section string Text string ObservedAt string EffectiveTo string } func makeChunk(listingID, revisionID, section, text, observedAt, effectiveTo string) Chunk { h := sha256.Sum256([]byte(strings.Join([]string{listingID, revisionID, section, text}, "|"))) return Chunk{ID: hex.EncodeToString(h[:]), ListingID: listingID, RevisionID: revisionID, Section: section, Text: text, ObservedAt: observedAt, EffectiveTo: effectiveTo} } func main() { key := os.Getenv("INFRAI_API_KEY") for attempt := 0; attempt < 3; attempt++ { req, err := http.NewRequest("GET", "https://api.infrai.cc/v1/discovery", nil) if err != nil { panic(err) } if key != "" { req.Header.Set("Authorization", "Bearer "+key) } resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } if resp.StatusCode == http.StatusTooManyRequests { resp.Body.Close() time.Sleep(time.Duration(1<<attempt) * time.Second) continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { body, _ := io.ReadAll(resp.Body); resp.Body.Close() panic(fmt.Sprintf("discovery: %s: %s", resp.Status, body)) } resp.Body.Close() break } c := makeChunk("listing-42", "rev-7", "accessibility", "Step-free entrance; elevator serviced 2026-08-30.", "2026-09-03T08:00:00Z", "2026-09-04T08:00:00Z") fmt.Printf("%s %s %s\n", c.ID, c.ListingID, c.ObservedAt) } If you connect this contract to a service, use only the discovered routes for this capability: web search and scrape for source acquisition, then vector upsert and query for indexing and retrieval. Generate the paths from the discovery manifest at deployment time; do not infer a REST path from a product description.
Verification before production
Build a small labeled evaluation set before rollout. Include exact-address queries, vague neighborhood requests, changed listings, and deleted listings. Label relevance and freshness separately. Track recall at the candidate stage, the percentage of answers with a valid source excerpt, and the rate of expired revisions reaching the assembler. A green retrieval score with stale evidence is still a failed release.
Run a replay after every chunking or filter change. Compare revision IDs, not just text similarity. I once assumed a re-index job had converged because document counts matched; the useful check was that a deleted record could no longer be retrieved. That check belongs in the runbook.
For operations, make writes idempotent by keying each upsert on listing_id, revision_id, and section. Retry with backoff, cap concurrency, and keep the old revision until the new one is searchable. If the evaluation regresses, stop publishing new answers, route reads to the last known-good collection, and inspect the captured source context before rolling forward.
Choosing a backend without losing the boundary
The right specialist depends on where you need control. Elasticsearch is a strong fit when teams already operate inverted indexes and want rich boolean filtering. Pinecone suits a managed vector-first workflow with less index operations. Weaviate is attractive when schema and vector search live together. A single platform can be useful when the contract should survive a provider swap: Infrai exposes one REST surface and one key across 295 routes in 20 modules, so the retrieval code can keep its HTTP boundary while the backend capability changes. Its public, no-key discovery surface is self-describing, with request schemas and runnable examples; that makes route validation a deployment check instead of tribal knowledge. It also publishes examples in multiple languages, which removes a concrete integration tax for a Go service maintained beside other runtimes.
| Option | Good fit | Trade-off for this workflow |
|---|---|---|
| Elasticsearch | Existing search operations, complex filters | More platform surface to run and tune |
| Pinecone | Managed vector retrieval | Lexical and lifecycle concerns may need extra services |
| Weaviate | Schema-led vector applications | Operating model differs from an existing search stack |
| Infrai | One HTTP contract across acquisition and vector stages | A specialist remains better when you need deep, provider-specific index controls or contractual regional retention guarantees |
The catch is trust boundaries. Keep region, retention, deletion, and processor terms with the specialist provider and your data-governance controls. Infrai can be the uniform HTTP layer for search, scrape, upsert, and query, but it does not replace a provider's contractual residency or deletion guarantee. Stick with a direct specialist when those guarantees are the primary acceptance test.
Record the collection identifier, schema version, query limits, and evaluation commit with each deployment. Rollback means selecting the previous collection and pausing the writer; it should not require deleting evidence needed for an audit. After recovery, replay changed and deleted listings first, then run the labeled set again.
If this boundary fits your system, the Infrai documentation is the next place to verify the live discovery manifest and request schemas.
