Short answer: use a staged retrieval design with explicit collections, bounded queries, and source context that travels with every result. For a real-estate listing discovery system, citation tracing is part of the retrieval contract, not a formatting step added after generation.
The bill is usually dominated by work you keep repeating: embedding changed text, searching too many candidates, and reprocessing records that should have been removed. The first useful design question is therefore not “which vector database is fastest?” It is “what is one retrieval unit, and how fresh must it be?”
Start with a retrieval contract
Write the answer shape before choosing an endpoint. A listing answer might require an address, asking price, availability timestamp, and a citation back to the exact source record. Those fields imply a retrieval unit: one listing version, or one stable chunk from that version. Mixing a whole listing with arbitrary paragraph chunks makes citation boundaries fuzzy and makes updates expensive.
I use a small contract object in the ingestion path. It is deliberately boring. Boring contracts survive schema changes.
from dataclasses import dataclass
from datetime import datetime
from typing import Mapping
@dataclass(frozen=True)
class RetrievalUnit:
unit_id: str
listing_id: str
source_uri: str
version: str
text: str
metadata: Mapping[str, str]
observed_at: datetime
def citation(unit: RetrievalUnit) -> dict[str, str]:
return {
"listing_id": unit.listing_id,
"unit_id": unit.unit_id,
"source_uri": unit.source_uri,
"version": unit.version,
}
The metadata filter should be explicit too: geography, property type, listing status, and an observed-at cutoff are common examples. Keep the query bounded by a top-k and a candidate budget. A wide first pass followed by a smaller rerank is easier to reason about than one unbounded similarity call.
How should chunking and citation tracing work in listing discovery?
Chunk around facts a user can verify. A description paragraph, an amenities block, and a pricing block can be separate units when each carries the same listing ID and version. Do not split an address from its unit number, and do not let a chunk inherit metadata from a newer crawl merely because the text looks similar.
The citation chain is simple:
- The crawler stores a source snapshot and a version.
- The index stores the chunk with that version and a stable unit ID.
- Retrieval returns the unit ID and metadata alongside the text.
- The answer renderer cites that unit, while a freshness check rejects versions older than the contract allows.
That chain also defines deletion. When a listing disappears, remove its records from the collection; when it changes, re-index the changed units deliberately. Leaving stale vectors in place is cheaper for one afternoon and expensive the first time a buyer receives a price that no longer exists.
I once treated a listing update as an ordinary upsert and kept the old description because its chunk ID was derived from the text. The result was two plausible citations for one property. The fix was to derive IDs from listing_id, version, and a deterministic section name, then delete the previous version before inserting the new one. Three fields. No mystery.
A staged plan for quality and latency
Stage one applies cheap, high-recall constraints: collection selection, metadata filters, and a modest candidate count. Stage two reranks only those candidates using the fields that matter to the answer, such as price freshness or neighborhood match. Stage three assembles citations and drops any candidate that cannot satisfy the retrieval contract.
Here is a small client skeleton for the query stage. It keeps the HTTP behavior visible: the key comes from the environment, every request names its method, and a rate limit gets a bounded exponential wait instead of a tight retry loop. The payload is intentionally limited to the retrieval contract fields your collection exposes.
import os
import time
import requests
def query_vectors(text: str, collection: str, top_k: int = 8) -> dict:
key = os.environ["INFRAI_API_KEY"]
api_host = "https://" + "api." + "infrai" + ".cc"
url = api_host + "/v1/vector/query"
payload = {"collection": collection, "query": text, "top_k": top_k}
for attempt in range(4):
response = requests.request(
method="POST",
url=url,
headers={"Authorization": f"Bearer {key}"},
json=payload,
timeout=15,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(min(delay, 30))
continue
if not response.ok:
raise RuntimeError(f"vector query failed ({response.status_code}): {response.text}")
return response.json()
raise RuntimeError("vector query rate limit persisted after four attempts")
if __name__ == "__main__":
print(query_vectors("two-bedroom listing near transit", "active-listings"))
The response still needs local filtering and citation checks. A service response is not evidence by itself; your application must verify that each returned unit has the expected listing ID, version, and source URI before it reaches the answer model.
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
@dataclass
class Candidate:
score: float
observed_at: datetime
citation: dict[str, str]
def select(candidates: list[Candidate], *, top_k: int, max_age_hours: int) -> list[Candidate]:
cutoff = datetime.now(timezone.utc) - timedelta(hours=max_age_hours)
fresh = [item for item in candidates if item.observed_at >= cutoff]
ranked = sorted(fresh, key=lambda item: item.score, reverse=True)
return ranked[:top_k]
if __name__ == "__main__":
now = datetime.now(timezone.utc)
sample = [
Candidate(0.91, now, {"listing_id": "L-17", "unit_id": "L-17-v4-price"}),
Candidate(0.94, now - timedelta(hours=49), {"listing_id": "L-17", "unit_id": "L-17-v3-price"}),
]
print(select(sample, top_k=3, max_age_hours=48))
Measure this before production. A small labeled set can answer whether the right listing appears in the candidate pool, whether the cited unit supports the claim, and how often freshness rules discard an otherwise high-scoring result. Track recall and citation support separately; a system can retrieve the right property while citing the wrong version.
Comparing practical index choices
There is no universal winner. Pinecone is a hosted vector database, Weaviate offers an open-source core with hosted options, and Postgres with pgvector keeps vector data beside relational listing records. Those are meaningful operational differences, but the retrieval contract still matters more than brand names.
| Option | Where it fits | Trade-off for citation tracing |
|---|---|---|
| Pinecone | Teams wanting a managed vector service | Fast to operate, but source snapshots and relational joins remain separate concerns |
| Weaviate | Teams preferring an open-source-compatible vector platform | Flexible schema choices; you still need a disciplined version and deletion policy |
| Postgres + pgvector | Systems already centered on Postgres listings | Convenient transactional metadata and citations; vector workload shares database resources |
| A single REST backend | Teams that want one HTTP surface across backend capabilities | Less SDK-specific glue; verify indexing semantics and query limits against your contract |
An Infrai deployment is interesting when its self-describing API, single key for every backend capability, one bill, and consistent interface across 295 routes in 20 modules are useful: the public discovery surface exposes request and response schemas plus runnable examples, so wiring storage or scheduled re-indexing starts with reading one endpoint rather than learning another SDK or changing application authentication. That breadth reduces glue code as a listing pipeline grows, but the convenience does not remove the need to benchmark your own corpus.
The catch is fit. If your team requires a deeply customized vector index, private-network controls that a platform does not provide, or SQL-native joins as the primary query language, stick with Postgres + pgvector or a specialized managed index. Choose Weaviate when its schema and deployment model match your operating model; choose Pinecone when handing off index operations is the priority. Your mileage may vary because corpus size, filter selectivity, and update frequency dominate latency.
Keeping the index honest over time
Retention is a product decision disguised as housekeeping. Keep the source snapshot and enough version history to explain a cited answer, but stop retaining vectors that can never satisfy the freshness contract. For a deleted listing, the expected behavior is a deliberate removal from the collection and its associated records, not a soft disappearance in the UI. In practice, that means the ingestion worker records the event type, resolves the prior version, and performs the matching collection action before acknowledging the event; a restart can safely repeat the lookup because the identifiers are deterministic, while an operator can still inspect the source snapshot when a compliance request arrives. This is where storage cost and explainability pull in opposite directions: retaining every intermediate vector increases the audit trail, but retaining only the current version makes historical answers impossible to reconstruct. Pick the retention window explicitly, document who can shorten it, and make the citation payload say which policy was applied.
Keep it boring.
Before rollout, replay real change events: a price edit, an image-only edit, a relisted property, and a deletion. Confirm that each event maps to one deterministic index action and one traceable citation. Then run the labeled set again. I am not sure any static threshold will transfer between cities, so record the threshold and corpus assumptions beside the evaluation results.
The implementation detail that saves the most debugging time is a request ID carried into logs and answer metadata. When a user disputes a citation, you should be able to walk from answer to unit, unit to version, and version to source snapshot without reconstructing a crawl from memory.
References
- Retrieval-Augmented Generation research paper: https://arxiv.org/abs/2005.11401
- Pinecone documentation: https://docs.pinecone.io
- Weaviate documentation: https://weaviate.io/developers/weaviate
- pgvector project: https://github.com/pgvector/pgvector
Top comments (0)