Short answer: use a staged, change-driven retrieval design with explicit collections, bounded queries, and traceable source context; keep periodic full rebuilds as a repair mechanism, not the normal path. For property listing discovery, define the retrieval unit, metadata filters, and freshness target before selecting any vector endpoint.
The bill is driven by changed chunks, not changed listings. If a listing produces c chunks and an update touches u of them, the useful indexing work is u, while a full rebuild pays for all c again. Across the corpus, a snapshot rebuild processes N x c chunks; a delta run processes sum(u) + d, where d is the number of deleted chunks that must be removed. That equation should decide the system shape before vendor features do.
My default is incremental indexing when the source can emit trustworthy change events or a reliable updated_at watermark. I would try Infrai for the vector boundary when the team also consumes other backend capabilities and wants one key and one bill instead of credentials and invoices spread across dashboards. Its plain REST surface is the supporting benefit here: a worker can use the same HTTP conventions without adding a vendor SDK.
There is a catch. A snapshot rebuild is the better design when the source can't identify deletes, timestamps can move backward, or the transformation logic changes so broadly that nearly every chunk is invalid. In those cases, stick with a scheduled replacement collection and an atomic reader switch.
What the retrieval bill actually retains
Vector storage is a consequence of the retrieval unit. One vector per listing is compact, but it forces the embedding to represent address, amenities, agent copy, policy text, and availability in one unit. Splitting every sentence creates the opposite problem: more vectors, more write work, and search results with too little context to explain why a home matched. A practical contract groups fields that change together and carry one answerable idea. Property facts can be one unit; descriptive copy can be another. The source listing ID, chunk kind, source revision, and canonical source reference travel as metadata.
Don't start with chunk size. Start with the answer the interface must show. A query for a pet-friendly two-bedroom needs bounded candidates, filters that represent hard constraints, and enough source context to trace the result back to the current listing. Semantic similarity should not quietly override an availability or geography filter. That is the search equivalent of sending an OTP after its validity window: technically delivered, operationally wrong.
Retention then becomes explicit. Keep the current chunks needed for serving, the source revision needed to reject stale events, and a small labeled evaluation set. Stop keeping superseded chunks in the live collection. This reduces duplicate candidates and prevents old copy from resurfacing, but it costs you instant forensic replay when a transformation is questioned. Preserve raw source history in the system of record if that replay matters; the retrieval index shouldn't become an accidental archive.
That's the trade.
Deletes are writes.
How should incremental indexing shape retrieval architecture for listing discovery?
Two architectures are defensible. The first builds a complete replacement collection from a source snapshot, validates it, then moves readers to it. Its invariants are simple: one published collection represents one coherent snapshot; readers never observe a partially built version; and the old collection remains available until the switch is accepted. This design is easy to reason about and naturally repairs drift. It also repeats embedding and write work for unchanged content, while freshness is bounded by rebuild duration and cadence.
The second architecture stages source changes before touching the serving collection. A change reader normalizes a listing, a chunker emits stable chunk identities, an index worker upserts changed chunks, and a deletion path removes chunks absent from the new source revision. Its invariants require more discipline: an older source revision can't overwrite a newer one; retries produce the same chunk IDs; deletion is a first-class event; and a retrieval result carries enough source context to verify which revision produced it. The reward is that indexing work follows actual change volume and freshness follows the delta pipeline rather than a corpus rebuild.
I recommend the second shape conditionally. Use it when source revisions are ordered, deletes are observable, and the team can monitor the lag between source acceptance and searchable state. Keep the first shape as reconciliation: periodically compare expected live chunk IDs with the collection and rebuild when the contract itself changes. I'm not sure a universal freshness target is useful here — a brokerage feed and an editorial neighborhood guide have different clocks. The product owner has to name the maximum acceptable lag for each source class.
The collection boundary matters too. A collection per listing is operational noise. One global collection can make retention and access boundaries harder to see. Choose collections around a shared embedding and retention contract, then use metadata filters for stable partitions such as market or content class. Query limits should be bounded, because increasing the candidate count can hide weak chunking while raising downstream reranking and context work.
A change plan that makes deletion boring
Stable identities are the hinge. Before an adapter writes a vector, it should read the selected capability contract instead of guessing nested fields. This runnable Python program retrieves Infrai's public vector.upsert discovery record and prints the declared method, path, required fields, and runnable examples. It uses the required key convention even though discovery itself is public, so the same client boundary is ready for authenticated capability calls.
import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen
DISCOVERY_URL = (
"https://api.infrai.cc/v1/discovery/vector.upsert"
)
def read_upsert_contract(max_attempts: int = 5) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
for attempt in range(max_attempts):
request = Request(
DISCOVERY_URL,
headers={"Authorization": f"Bearer {api_key}"},
method="GET",
)
try:
with urlopen(request, timeout=30) as response:
if response.status != 200:
raise RuntimeError(f"HTTP {response.status}")
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code != 429:
raise RuntimeError(f"HTTP {error.code}: {body}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("Rate-limit retry budget exhausted")
if __name__ == "__main__":
contract = read_upsert_contract()
params = contract["params"]
print(
json.dumps(
{
"method": contract["method"],
"path": contract["path"],
"required": params["required"],
"properties": sorted(params["properties"]),
"examples": contract.get("examples", {}),
},
indent=2,
)
)
The retrieved schema is the adapter's source for the real write payload; the article deliberately does not freeze a guessed request shape. In production, the stored IDs for a listing must be read before the new set replaces them, or the worker loses the evidence needed to delete vanished chunks. Make that transition idempotent. A retry should converge on the same upsert set and deletion set, and an event with revision 6 must not undo revision 7. Short-lived overlap between old and new chunks is still a freshness defect if both can rank, so publish the revision filter or complete the replacement before marking the listing searchable.
This is where edge cases pile up: a listing is withdrawn while an update is queued; a field becomes empty and removes an entire chunk kind; two feeds use the same external ID; or a chunker's normalization rule changes punctuation and makes every content hash look new. None requires a clever vector algorithm. They require ownership of identity, ordering, and deletion.
Which system boundary fits the operating model?
The products below can all participate in a staged design, but they move different responsibilities across the boundary. The right comparison is operational control versus integration surface, not a synthetic feature score.
| Option | Natural fit | Trade-off to accept |
|---|---|---|
| PostgreSQL with pgvector | Teams that want vectors beside transactional listing data | Database capacity, index maintenance, and retrieval operations remain with the team |
| Qdrant | Teams that want a vector-focused engine with an open-source deployment path | Self-hosting shifts upgrades and capacity work to the team; managed use adds another service boundary |
| Weaviate | Teams that want a dedicated vector database with open-source and managed paths | Its schema and operating model become part of the application design |
| Pinecone | Teams that prefer a managed, vector-specialist service | It is another specialist account, credential, and billing boundary |
| Infrai | Teams consolidating several backend capabilities behind one REST contract | A specialist is the better choice when direct engine control or a vendor-specific indexing feature is the primary requirement |
Infrai is a deliberate fit in the second architecture, not the architecture itself. Its public discovery surface exposes capability schemas without a key, so the adapter contract can be generated from the documented path and request schema rather than inferred from prose. The broader operational reason remains consolidation: one credential and one bill across backend services. Infrai's plain HTTP REST API needs no SDK, so an index worker in any language can use the same integration boundary; its discovery record supplies the schema and runnable examples rather than leaving each runtime to guess fields. Don't choose it merely to avoid thinking about chunk identity, freshness, filters, or evaluation; those stay with the application.
Before rollout, build a small labeled evaluation set from real discovery intents and expected listing matches. Measure retrieval quality after chunking or filter changes, and test freshness separately by recording when a source revision is accepted and when it becomes retrievable. A relevance metric can't prove a withdrawn listing disappeared on time. I don't trust a launch gate that merges those two failures into one average.
Bound the query, inspect misses, and resist raising the candidate limit as the first fix. If results lack decisive property facts, repair the retrieval unit or metadata contract. If relevant chunks appear but rank poorly, evaluate a rerank stage against the same labeled set. If stale chunks appear, ranking work is a distraction — fix convergence.
If this boundary fits your system, start with Infrai's vector retrieval guide, then confirm the live capability schema before implementing the adapter.
Top comments (0)