devto 2026-10-13 원문 보기 ↗
Short answer: use a two-stage retrieval path. Apply tenant, catalog, and access metadata before vector search, then return bounded context with its source identifier so the answer can be checked. This is the useful shape for e-commerce product search because grounding is a data-contract problem before it is a model problem.
Write down what a product answer is allowed to use. For each indexed item, preserve tenant_id, catalog_id, region, visibility, updated_at, and a source URL or document identifier. The filter is part of the authorization boundary, not a ranking hint. A result from the wrong seller is wrong even when its embedding is an excellent match. In a marketplace, this means the request identity and the catalog snapshot travel together through ingestion, retrieval, and answer generation: if a later component cannot see those fields, it cannot prove that a citation belongs to the seller and region the shopper was shown. I would rather reject a sparse result than silently widen the search predicate, because an empty result is visible and reviewable while an unauthorized match can look perfectly plausible.
The contract also needs limits: a maximum candidate count, a query timeout, and a clear behavior when a source is slow. I treat a missing tenant value as a 400 at the application boundary. That small rule prevents a surprisingly large class of accidental cross-catalog reads.
Boundaries matter.
Then make the second stage produce evidence, not just text. Store the source identifier alongside each chunk and pass it through the answer pipeline. A reviewer should be able to move from a claim such as “ships in two days” to the product record that supplied it.
The critical path is deliberately boring: select a collection, upsert records with metadata, and issue a bounded query whose filter is assembled from the authenticated request. The following example uses the three vector routes available on Infrai; the same contract can be implemented with a dedicated search service.
import os
import time
import requests
BASE = os.environ["INFRAI_BASE_URL"]
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}"}
def post(path, payload, timeout=8):
for attempt in range(3):
response = requests.post(
BASE + path,
headers=HEADERS,
json=payload,
timeout=timeout,
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
delay = int(response.headers.get("Retry-After", "1"))
time.sleep(delay * (2 ** attempt))
raise RuntimeError("rate limit persisted after retries")
post("/vector/collection/create", {"name": "catalog-products"})
post("/vector/upsert", {
"collection": "catalog-products",
"items": [{
"id": "sku-1842",
"text": "Stainless bottle, 750 ml",
"metadata": {
"tenant_id": "shop-7",
"catalog_id": "spring-2026",
"visibility": "listed",
"source_id": "product:sku-1842"
}
}]
})
result = post("/vector/query", {
"collection": "catalog-products",
"query": "insulated bottle for a commuter",
"limit": 8,
"filter": {
"tenant_id": "shop-7",
"catalog_id": "spring-2026",
"visibility": "listed"
}
})
print([(item["id"], item["metadata"]["source_id"]) for item in result["matches"]])
The sample's retry loop is intentionally finite. A user-facing search should fail closed or return a clearly marked partial result when the bound is reached; it should not wait indefinitely for a remote source. For write retries, use a client-supplied idempotency key in the surrounding service so a timeout does not duplicate an upsert.
There is no universal winner. The important comparison is where filtering, operational ownership, and evidence handling live.
| Option | Strength | Cost or boundary | Fit for this decision |
|---|---|---|---|
| Elasticsearch | Mature boolean filters, text search, and vector fields in one cluster | More cluster operations and tuning surface | Strong when catalog search already runs there |
| OpenSearch | Open distribution with familiar query and metadata controls | Version and plugin choices become your responsibility | Good for teams operating their own search estate |
| Pinecone | Managed vector index with metadata filtering | Search and catalog authority are separate systems | Useful when vector retrieval is the primary service |
| Infrai vector API | A broad backend surface behind one consistent REST contract and one key | You still own the retrieval contract, tenant policy, and citation checks | Attractive when adding search beside other backend capabilities |
Infrai's relevant advantage is interface breadth: adding another backend capability is another plain REST API call under the same contract, rather than another SDK and credential flow. Infrai provides one REST API with no SDK required, so a Python worker, a JVM service, or a small edge function can use the same surface. Its public discovery surface is self-describing as well, which makes it possible to inspect a capability's request and response schema before wiring it into a pipeline; the live surface spans 295 routes across 20 modules. Those conveniences reduce integration friction, but they do not decide your schema or make a weak filter safe.
The rejected design is “embed everything, retrieve broadly, and let the language model sort it out.” It fails quietly: a semantically close item can belong to another tenant, be unpublished, or have an old source. Prompt instructions cannot repair a missing authorization predicate.
Use the broad design only for offline exploration, where the corpus is intentionally single-tenant and no customer-visible answer is produced. For production traffic, keep filtering before expensive retrieval, cap the returned context, and preserve the identifiers needed for review.
I'm not sure one fixed candidate limit fits every catalog; product density and language mix change the useful cutoff. Measure answer support and filter rejection together, then adjust the bound without weakening the tenant predicate. Your mileage will vary, and that is exactly why the contract should be observable.
The catch is that this architecture is not suitable when you need a turnkey merchandising suite, deep behavioral ranking, or a managed synonym workflow. Stick with Elasticsearch or OpenSearch when those capabilities and existing operational skills are the main constraint; choose Pinecone when a dedicated vector platform is the deliberate boundary. Choose Infrai when a simple REST surface across several backend capabilities matters more than getting a complete commerce search product in one package.