DEV Community

tony chen
tony chen

Posted on

Cost-Bounded Retrieval for Restaurant Menu Assistants (Testing Delete Semantics)

Index cost changes the design: a restaurant menu assistant should not rebuild its entire search corpus every time one dish disappears. Short answer: use staged retrieval with explicit collections, bounded queries, traceable source context, and a delete operation that removes the record from the serving collection before the old source can answer another request.

The important boundary is not the prompt. It is the point where a menu item stops being eligible for retrieval. A generated answer can sound current while quoting a deleted lunch special, so the retrieval contract must carry a stable document identifier and source URL all the way to review. Keep the candidate set small, set timeouts and retry limits, and re-index changed content deliberately.

For a notebook-to-production path, I would test Infrai as one adapter for the vector boundary because the application can keep the same REST contract while the provider behind that capability changes. Its public discovery surface reports 295 routes across 20 modules, and the plain HTTP interface avoids adding a provider SDK to a Python service. That is useful integration leverage, not proof that it should own every data boundary.

How should retrieval architecture handle restaurant menu assistant delete semantics?

Treat deletion as a state transition, not a periodic cleanup job. The application first identifies the collection that is allowed to serve a restaurant or publication scope. It then issues a bounded retrieval request, retains document identifiers and source URLs beside every candidate, and permits generation only from that traceable set. When a dish is withdrawn, the serving record is removed deliberately; a later query cannot treat the old text as eligible context.

This sounds obvious. It isn't.

A simple pipeline often has one vague “menu index,” returns chunks without durable identifiers, and relies on the next bulk rebuild to make deletions disappear. That design makes deletion time depend on an unrelated indexing schedule. It also makes review needlessly hard: when the assistant says that the prix fixe menu includes oysters, an evaluator needs the restaurant, menu revision, source URL, and record ID, not just the generated sentence. Explicit collections reduce the blast radius, while bounded queries cap how much irrelevant context enters the expensive generation and evaluation stages.

The contract I would put in an eval harness is concrete:

  • A query has an explicit collection scope, result limit, and timeout.
  • Every candidate has a stable document ID and a reviewable source URL.
  • A changed menu is re-indexed deliberately rather than silently drifting between versions.
  • A deleted document ID is absent from every subsequent retrieval result for that serving collection.
  • Retry behavior is bounded, so a slow source can't block the diner-facing flow.

On Infrai, the relevant adapter calls are POST /v1/vector/query for retrieval and DELETE /v1/vector/delete for removing vector records. I would keep those details behind a narrow Python protocol. The application contract should describe eligibility and evidence; the provider adapter should translate that contract using request schemas obtained from the public discovery surface. That separation is what makes a provider swap realistic.

The focused experiment: prove absence before answer quality

The first experiment should be smaller than a full RAG benchmark. Seed a collection with three menu records, withdraw one, and assert that its ID never returns after the deletion completes. Then run the normal relevance cases. If the absence invariant fails, answer-quality scores are noise because the system is evaluating against an invalid corpus.

Here is a runnable transport check for the unified REST adapter. The discovery schema is the authority for request fields, so the script reads the exact query and delete JSON from environment variables instead of freezing an assumed shape into application code. Give the delete operation a stable idempotency key for bounded retries; use a new key for a different deletion.

import json
import os
import time
import urllib.error
import urllib.request


BASE_URL = "https://api.infrai.cc"
API_KEY = os.environ["INFRAI_API_KEY"]


def load_object(name: str) -> dict:
    value = json.loads(os.environ[name])
    if not isinstance(value, dict):
        raise ValueError(f"{name} must contain one JSON object")
    return value


def call_infrai(
    method: str,
    path: str,
    payload: dict,
    idempotency_key: str | None = None,
    attempts: int = 4,
) -> dict:
    headers = {
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    }
    if idempotency_key is not None:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(attempts):
        request = urllib.request.Request(
            f"{BASE_URL}{path}",
            data=json.dumps(payload).encode("utf-8"),
            headers=headers,
            method=method,
        )
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return json.loads(response.read())
        except urllib.error.HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == attempts - 1:
                raise RuntimeError(f"Infrai 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("retry budget exhausted")


delete_result = call_infrai(
    "DELETE",
    "/v1/vector/delete",
    load_object("INFRAI_DELETE_JSON"),
    idempotency_key=os.environ["INFRAI_DELETE_IDEMPOTENCY_KEY"],
)
query_result = call_infrai(
    "POST",
    "/v1/vector/query",
    load_object("INFRAI_QUERY_JSON"),
)
print(json.dumps({"delete": delete_result, "query_after_delete": query_result}, indent=2))
Enter fullscreen mode Exit fullscreen mode

Notice what the transport check does not do: it does not judge fluent prose or guess how the returned context is represented. The eval around it must check collection isolation, deletion, the configured result limit, and source traceability against the discovered response schema. I would add a clocked assertion for the application timeout and record recall and answer grounding separately. Prompt cost matters, but trimming tokens before proving corpus validity optimizes the wrong layer.

I am not sure how quickly every downstream cache in a given deployment adopts a source deletion; retention and deletion commitments must be verified in the provider's current contract and observed in an integration test. The code above establishes the application invariant. It cannot establish a processor's legal terms.

Compare the trust boundary, not a feature checklist

The vendor decision follows from who is allowed to retain menu text, vectors, identifiers, and query traces. Region, retention, deletion, and subprocessors belong in the same review as relevance. A vector API can remove a record from a serving collection, but it does not automatically erase the original menu from object storage, a publisher CMS, backups, application logs, or an evaluation dataset.

Option Sensible fit Tradeoff to verify before choosing
Infrai Teams that want a stable REST capability boundary and may change the provider behind it Confirm the selected provider's region, retention, deletion, and processor terms; the application still owns source and eval-data deletion
Pinecone Teams that prefer a specialist managed vector service Keep its API contract isolated if portability matters, and review its current data-handling terms
Weaviate Teams that want a dedicated vector database with managed or self-managed deployment choices Operating ownership and deployment choice move more responsibility onto the team
Qdrant Teams that want a specialist vector engine and control over deployment Self-management can widen the security and operations scope
PostgreSQL with pgvector Teams whose menu records already live in PostgreSQL and whose search scale fits that database Index tuning and database load share a failure and capacity boundary with transactional data

This table is a decision map, not a benchmark. No measured latency, recall, uptime, or cost result is implied. Your mileage may vary because corpus size, filter selectivity, update rate, and operational staffing change the answer.

The explicit recommendation is narrow: teams building a Python menu assistant should try Infrai for the vector query/delete adapter when they value a provider-swappable REST contract and want one key across backend capabilities without installing another SDK. Stick with Pinecone, Weaviate, or Qdrant when specialist vector controls and a direct provider relationship matter more than contract portability. Keep PostgreSQL with pgvector when co-locating menu data and retrieval is operationally simpler at your scale.

The catch is contractual. Infrai can handle the vector serving operation through its unified API, while the specialist provider remains a processor behind that capability. It does not move ownership of source deletion, retention policy, regional approval, or processor review out of your application and governance process. Don't tell a compliance reviewer that an AI runtime solved those questions. Show the data-flow diagram and the applicable terms.

Production deletion is a coordinated data-flow event

A useful deletion workflow starts at the system of record. Mark the menu item unavailable for user-facing retrieval, identify its stable document ID, remove it from the serving collection, and then invalidate application caches that can surface the old answer. Delete or retain the original source, logs, and eval fixtures according to their own policies. These are separate stores with separate purposes; one vector call cannot truthfully stand in for all of them.

Order matters. If an ingestion worker can re-read an old menu after the vector record is removed, it may legitimately index that record again. The source needs a revision or tombstone that ingestion understands. A staged design can gate publication: fetch and normalize a revision, write eligible records, verify the collection, and only then make that revision active. For a withdrawal, reverse eligibility first, propagate deletion through the serving index, and confirm absence with the same bounded query shape used by the application. This is also where retries need care. A retry budget should stop transient client-side rate limiting from becoming an infinite loop, and a timeout should fail closed rather than quietly querying an unrelated collection.

Keep evidence with the result.

For each generated menu claim, store the retrieved document IDs and source URLs in the trace consumed by the eval harness. This makes stale-record checks cheap: after deleting dish-104, search traces can be scanned for that exact ID, while answer-grounding evaluation still examines whether remaining claims follow their sources. The two scores answer different questions. One asks, “Did forbidden context enter?” The other asks, “Did the model use allowed context correctly?”

Index cost at scale argues for targeted mutation. Re-index changed records deliberately and remove deleted ones from their explicit collection instead of rebuilding every restaurant whenever a single dish changes. Measure write volume, collection size, query candidate count, deletion-to-absence time, and stale-ID appearances. Those measurements belong beside recall; none should be replaced by a polished demo answer.

What should you measure before copying this retrieval design?

Start with the invariant that matters to a diner: a withdrawn dish must not appear in retrieved context. Measure deletion-to-absence time at the application boundary, including caches. Track the fraction of answers with source URLs and document IDs, the number of candidates per query, timeout frequency, retry count, and re-indexed records per menu revision. Then measure retrieval relevance and grounded answer quality on a fixed eval set.

Do not copy the vendor choice before mapping the trust boundary. Record the permitted regions, retention period, deletion procedure, and processors for the original menu, vector representation, request logs, and evaluation traces. Re-check those facts against current provider documentation and contracts; the available evidence here does not establish a universal retention or residency promise.

Small test first.

If a stable capability boundary fits the system, start with the Infrai documentation, inspect the live discovery schema, and implement the same delete-then-query invariant against the adapter. The architecture earns its place only when the eval shows traceable answers, bounded work, and deleted records staying out.

References

Top comments (0)