Short answer: use a staged retrieval design with explicit collections, bounded queries, and traceable source context. For a one-person SaaS, that keeps privacy decisions visible while keeping index work predictable as listings change.
The user-visible answer is the contract. Before picking a vector endpoint, I've written down the retrieval unit (one listing, or one chunk of a listing), the metadata filters (tenant, region, visibility), and the freshness target. Those three choices determine what belongs in an index and what must be checked after retrieval.
Start there.
Start with the retrieval contract
Real-estate data is a privacy problem before it is a similarity problem. A listing can contain an exact address, an agent's phone number, or notes intended only for an internal team. I keep those fields out of the embedding text unless the product truly needs them. The source record remains behind an authorization check; the vector record carries a stable listing ID and non-sensitive search facets.
The first stage is candidate discovery. The second applies bounded filters and a small top-k. The final stage loads the authorized source context and records why each result was included. This makes a response auditable without putting private text into a prompt by accident.
A collection should map to a meaningful isolation boundary, not to every UI filter. In practice that usually means a tenant or a data-retention domain, with neighborhood, property type, and availability represented as metadata. If a collection is split too finely, index maintenance becomes the bottleneck. If it is too broad, every query needs stronger authorization and deletion checks. I once assumed that putting every city in its own collection would make privacy obvious; the accounting was worse, because a listing edit now meant tracking a larger set of collection migrations, deployment switches, and cleanup jobs. A single tenant boundary plus explicit metadata was easier to inspect in a pull request and easier to explain during an incident review.
How should privacy controls shape real-estate listing discovery retrieval?
I use three controls in the retrieval contract.
- Collection boundaries. Separate tenants, and separate any dataset with a different retention policy. The collection name is part of the authorization decision, not a value copied from a client request.
- Metadata filters. Apply tenant, region, listing status, and audience filters before ranking. A similarity score cannot repair a missing access check.
- Traceable context. Return source IDs and the fields used for the answer. The generation step receives only records that passed authorization, and the UI can show the listing IDs behind a sentence.
The query itself is bounded: a fixed top-k, a maximum candidate count, and a timeout in the caller. Bounded work gives me a number I can reason about when index cost is the primary decision axis. It also prevents a broad natural-language request from turning into an unreviewed export of a tenant's inventory.
A smallest working Node.js path
The API surface I need for a first pass is small. I discover collections, then issue a vector query. This example keeps the request shape explicit and treats a non-2xx response as a real error.
const baseUrl = process.env.INFRAI_API_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
if (!baseUrl || !apiKey) throw new Error("INFRAI_API_BASE_URL and INFRAI_API_KEY are required");
async function listCollections() {
const response = await fetch(`${baseUrl}/vector/collection/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) throw new Error(`collection list failed: ${response.status}`);
return response.json();
}
async function queryListings(embedding: number[], tenantId: string) {
const response = await fetch(`${baseUrl}/vector/query`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
collection: `tenant-${tenantId}`,
vector: embedding,
top_k: 12,
filter: { tenant_id: tenantId, status: "active" },
}),
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`listing query failed: ${response.status} ${detail}`);
}
return response.json();
}
The collection identifier is selected server-side from the authenticated tenant. The returned IDs are then fetched from the source database, checked again for visibility, and attached to the answer as citations. I do not treat vector payloads as the system of record. For production writes, I attach an idempotency key based on the stable listing ID and retry a 429 with exponential backoff while honoring Retry-After; a repeated upsert must not create a second record.
Re-indexing is deliberate. When a listing changes, enqueue one replacement for its stable ID; when it is deleted, remove that ID from the collection as part of the same lifecycle event. A nightly reconciliation job can catch missed events, but it should be a backstop, not the freshness strategy.
What should I measure before choosing a vector backend?
I start with a small labeled evaluation set: real search questions paired with the listings a reviewer considers acceptable. For each candidate backend, I record recall at a fixed k, filter correctness, stale-record rate, and the number of source fetches per answer. The set does not need to be large to expose a bad retrieval unit or an overly broad collection.
Index cost at scale is mostly a shape decision. Chunking every paragraph creates more vectors and more updates. Embedding one giant listing hides the fields users filter on. I would rather test two retrieval units on 100 labeled questions than argue from a vendor's benchmark chart.
Here is the trade-off table I use when the evaluation set is ready:
| Option | Where it fits | Cost and privacy trade-off |
|---|---|---|
| Pinecone | A hosted vector index when the team wants managed operations | Less infrastructure work; collection and tenant isolation still need an application policy |
| Weaviate | A vector database with deployment choices | More control over where data runs; operating the cluster becomes part of the revenue-per-hour budget |
| Postgres with pgvector | A system already centered on relational listing data | One data plane can simplify joins and deletion; vector workloads share resources with transactional traffic |
| Infrai vector API | A small team that wants a plain HTTP boundary for this capability | One key and one bill cover backend services, and the same REST convention can keep a multi-service integration narrow; collection policy and source authorization remain application responsibilities |
The last row is not a blanket recommendation. Infrai's advantage here is operational scope: one REST API and one credential can cover several backend capabilities, so a solo founder has fewer SDKs and invoices to reconcile. That matters when shipping a weekly feature competes with index tuning. It does not remove the need to design deletion, tenant isolation, or an evaluation set.
The catch: where this design is not suitable
This staged approach is a poor fit when the product needs sub-second global inventory search with a continuously changing corpus and no room for a source-database check. In that case, a tightly integrated search system may be worth the operational cost. Stick with Postgres and pgvector when relational predicates dominate and the vector query is a small part of an existing transaction. Choose a managed vector service when your team has no appetite for running a cluster.
I am also not sure one collection layout will survive a national marketplace. Your mileage may vary with listing volume, deletion frequency, and how often agents edit descriptions. That uncertainty is exactly why I would gate rollout on the labeled set and a measured stale-record budget, then revisit the boundaries after the first production cohort.
Top comments (0)