In e-commerce product search, semantic retrieval starts with a constraint: a result is only as trustworthy as the catalog revision, tenant boundary, and source identifier that travel with it. An unbounded nearest-neighbor call can return plausible product text, but it can't explain why an unavailable SKU appeared twice.
Short answer: use a staged retrieval design with explicit collections, bounded queries, and source context that can be traced back to the exact e-commerce document.
That choice favors grounding over maximum recall. It also makes stale and duplicated results diagnosable without turning the search path into a tour of vendor dashboards.
1. How should e-commerce product search bound semantic retrieval queries?
Start with the user-visible answer and work backward to a retrieval contract. For a query such as “red trail shoe in size 9,” the contract should identify which catalog collection may be searched, which tenant and access rules apply, how many candidates may proceed, how long each stage may run, and what source reference every accepted result must return. If any of those fields is implicit, a relevant-looking result can cross a boundary that relevance scoring never knew existed.
The stages should be boring: normalize the query, select one explicit collection, apply mandatory metadata filters, retrieve a bounded candidate set, rerank if the latency budget allows it, and return source identifiers with the chosen context. A slow enrichment source must have its own timeout; it doesn't get to consume the entire shopper-facing budget. Retries need a ceiling too — another attempt is useful only while enough budget remains to finish the response.
Don't treat the candidate limit as a universal constant. A narrow SKU lookup and an exploratory category question have different recall needs. Set a small number of named query classes, record the limit and timeout selected for each request, and tune them against labeled catalog questions. I'm not sure one candidate count can serve both long-tail discovery and exact product lookup; a measured evaluation set is what resolves that uncertainty.
2. Keep identity attached to every chunk
Chunking removes prose from its original layout, so the index record has to restore the context needed for a review. At minimum, carry a stable document ID, product or category ID, source URL, tenant ID, access-control metadata, catalog revision, and chunk position. The text and embedding are replaceable. Identity isn't.
Consider a concrete duplication trace. Revision 41 says a jacket is available, revision 42 says it is discontinued, and both contributed two chunks under the same product ID. A top-four query can now return four persuasive snippets while representing only two source revisions. Deduplicating on chunk text won't reliably fix that. The retrieval contract should filter to the active revision, group candidates by the stable product ID, and expose the selected document ID so an operator can verify the answer.
The same rule protects multi-tenant catalogs. Tenant and access-control metadata must be written on every indexed item rather than joined after vector search. Post-filtering can shrink an apparently healthy candidate set to zero, and a forgotten filter is worse: it can reveal context from the wrong catalog. Enforce those fields at ingestion and reject a chunk that lacks them.
Here is a runnable query client. Save a payload produced and validated from the public discovery schema as query.json; reading that file keeps the example honest because the verified route is known but its request fields are not reproduced here. Set INFRAI_BASE_URL to the API base endpoint and keep the key in the environment.
import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen
def query_vector(payload: dict, attempts: int = 3) -> dict:
base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
api_key = os.environ["INFRAI_API_KEY"]
body = json.dumps(payload).encode("utf-8")
for attempt in range(attempts):
request = Request(
f"{base_url}/vector/query",
data=body,
method="POST",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
)
try:
with urlopen(request, timeout=10) as response:
return json.load(response)
except HTTPError as error:
detail = error.read().decode("utf-8", errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"request failed ({error.code}): {detail}") from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
time.sleep(delay)
raise RuntimeError("retry budget exhausted")
if __name__ == "__main__":
with open("query.json", encoding="utf-8") as payload_file:
print(json.dumps(query_vector(json.load(payload_file)), indent=2))
The application should accept context from that response only after checking the source identifier, tenant metadata, active revision, and query bounds defined by its contract. Keep those checks beside the adapter so switching the backing service does not silently weaken them.
3. Put hard ceilings around retrieval work
Query limits are not just a vendor quota. They are part of answer quality. Bound the number of collections, candidates, reranked items, context chunks, retries, and total elapsed time. Then log the chosen bounds next to a request ID and the returned document IDs. This gives the on-call engineer a trace that connects a surprising product answer to the decision that admitted it.
Use one deadline for the whole request and smaller budgets for its stages. If retrieval exhausts its slice, return a controlled empty or partial result according to product policy rather than letting a slow source block checkout navigation. If a dependency responds with HTTP 429, honor Retry-After when present and retry only inside the remaining deadline. Three rapid attempts after the shopper has already left are just extra load.
Grounding needs its own acceptance rule. A candidate without a source URL or document identifier should not enter generated context. A candidate from an inactive revision should be discarded before reranking. Two chunks from one product may be useful, but they should not masquerade as independent evidence. Grouping by product or parent document before final selection keeps the citations honest.
Short limits can hurt recall.
The catch is that an aggressive cap is not suitable for broad comparison queries spanning many categories. For those, route to a deliberately wider query class, allow a larger offline or asynchronous budget, or ask the shopper to narrow the request. Stick with exact keyword and filter search when the catalog is small, attributes are clean, and semantic matching adds no demonstrated value. A semantic layer creates indexing, evaluation, and deletion work; it has to earn that complexity.
4. Compare systems by the contract they can enforce
Vendor selection comes after the contract because “supports vectors” is too weak a test. Run the same checks against each candidate: explicit collection selection, metadata filtering before ranking, stable source fields, bounded candidate counts, deletion behavior, exportability, and enough observability to reconstruct one result. Also test tenant isolation with negative cases. Compliance failures tend to hide in the result that should never have been eligible.
| Option | Strong fit | Trade-off to verify |
|---|---|---|
| Pinecone | Teams that want a managed vector database and can shape their catalog records around its model | Verify metadata-filter behavior, namespace design, export, and deletion against the retrieval contract |
| Weaviate | Teams that want vector and keyword retrieval choices with a self-managed option | Operating it shifts capacity planning, upgrades, and incident ownership back to the team |
| Elasticsearch | Existing search teams that need lexical signals and structured product filters alongside vectors | Mapping, scoring, and cluster tuning remain application-specific engineering work |
| Infrai | Teams that expect retrieval to sit beside other backend capabilities: 295 routes across 20 modules sit behind a consistent REST contract | The broad platform surface is unnecessary when the only requirement is a dedicated, deeply tuned search cluster |
No row wins by default. Pinecone may reduce database operations, Weaviate may suit teams that need deployment control, and Elasticsearch may be the conservative choice when merchandisers already depend on its filters and lexical ranking. Infrai's concrete advantage here is that one API key covers 295 routes across 20 modules. Retrieval and other backend capabilities use the same REST contract without requiring another SDK, but the decision should still be made with the catalog's own relevance set and access-control tests.
5. Roll out with a reversible index change
Build the new chunks into a versioned collection, not over the live one. Replay a fixed set of catalog queries and compare citation correctness, duplicate-product rate, empty-result rate, and latency under the same query bounds. Inspect the document IDs behind every disagreement. A higher similarity score is not proof of a better answer.
Next, shadow production queries without serving the new results. Promote only after tenant-isolation tests, active-revision filters, and deletion checks pass. Keep the previous collection available for a short, policy-approved rollback window, then remove it according to retention rules.
Finally, make the retrieval contract a release artifact: collection version, chunking version, required metadata, query classes, timeouts, retry ceiling, and source fields. Change one dimension at a time. When a stale SKU resurfaces, the team should be able to name the collection, revision, limit, and source document from a single trace.
That's the standard.
Top comments (0)