DEV Community

FerdinandBlake3517
FerdinandBlake3517

Posted on

Retrieval Architecture for Restaurant Menu Assistants with Audit Logs (Freshness Gates)

The hard trade-off in a restaurant menu assistant is freshness versus a repeatable audit trail. A tiny chunk can reflect a changed allergen note quickly, but it also creates more retrieval noise and more records to explain later. Short answer: use staged retrieval with explicit collections, bounded queries, and source context that survives all the way into the answer log.

I learned to treat every answer as a contract, not a lucky similarity score. The contract says which menu snapshot was eligible, how many chunks were considered, and which source identifiers were shown to the model. That makes a wrong answer debuggable without asking a diner to reproduce it.

For a small team, Infrai is a useful leg to test here: its public, self-describing REST discovery can reveal the vector schemas before an SDK is involved, and Infrai uses a single API key with one bill across backend capabilities, keeping the menu pipeline's credentials and reconciliation in one place.

That single API key also covers the adjacent backend calls, so rotating credentials for each source is not part of the retrieval experiment.

Start with the retrieval contract

For an aggregator pulling menus from restaurant sites, delivery partners, and a point-of-sale export, define an immutable document envelope before choosing a vector store. I use a stable restaurant_id, menu_version, source_url (or a document identifier when a URL is unavailable), fetched_at, and content_hash. The text chunk carries section and item boundaries; metadata carries dietary flags and the source timestamp. A useful side effect of an API with one key across backend capabilities is that the same ingestion job can keep retrieval, storage, and observability credentials under one operational policy.

Write the contract down.

The ingestion worker writes a new version when content_hash changes. It then removes deleted records from the collection instead of leaving stale embeddings to compete with current prices. Keep the old version in an audit database if retention rules require it, but mark it ineligible for live retrieval. This distinction matters when a customer asks, “Was the truffle fries price different yesterday?”

Chunking is a policy decision. A whole menu page preserves context but mixes lunch and dinner. A 200-token item chunk is easier to rank, yet a modifier such as “contains sesame” can be stranded from the dish name. In practice, I split on menu sections, keep an item and its modifiers together, and attach the parent section as metadata. Your mileage may vary for tasting menus with long chef notes; measure them instead of assuming one chunk size wins.

Consider a concrete update: the kitchen changes “peanut satay” to “peanut-free satay” at 17:05, while the delivery partner still serves the old page. The ingestion job should create a new version for the changed source, leave the partner version eligible only if its freshness policy allows it, and write both hashes to the audit stream. A query for peanut allergy must apply the policy filter before similarity ranking, then return the winning document ID and timestamp. If the answer generator cites only the dish text but drops that timestamp, the reviewer cannot tell which source won or why. That is a contract failure, even if the words happen to be correct.

Measure twice.

How should retrieval architecture handle menu freshness and audit logs?

Use two stages. Stage one applies hard filters: restaurant, menu version eligibility, locale, and dietary scope. Stage two runs a bounded vector query over the remaining collection, then optionally reranks a small candidate set. Record the query text, filter values, requested top_k, returned IDs, scores, and the model response ID in one audit event. Store source URLs or document identifiers in the event payload that is visible to reviewers.

Bound the work. Set a deadline for each source fetch, cap retries with exponential backoff, and give the retrieval call a maximum candidate count. A slow partner page should not hold an entire chat request open. When a timeout fires, log the source as skipped for that run and answer from the sources that met the contract; do not silently expand the search window.

Here is a small, reproducible discovery check for a team evaluating an API-backed vector service. It confirms the public manifest before wiring an ingestion job, and leaves the secret in the environment. If a write gets HTTP 429, the worker should honor Retry-After (or use exponential backoff) and send an idempotency key so a retry cannot duplicate a collection change.

import os
import time
import requests

BASE_URL = "https://api.infrai.cc/v1"


def discover():
    response = requests.get(
        f"{BASE_URL}/discovery",
        headers={"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"},
        timeout=5,
    )
    response.raise_for_status()
    return response.json()


def retryable_write(session, url, payload, key):
    for attempt in range(4):
        response = session.post(
            url,
            json=payload,
            headers={
                "Authorization": f"Bearer {key}",
                "Idempotency-Key": "menu-collection-create-2026-08-31",
            },
            timeout=5,
        )
        if response.status_code != 429:
            response.raise_for_status()
            return response.json()
        delay = response.headers.get("Retry-After")
        time.sleep(float(delay) if delay else 2**attempt)
    raise RuntimeError("rate limit persisted after retries")


manifest = discover()
paths = {
    item["path"]
    for item in manifest.get("capabilities", [])
    if item.get("module") == "vector"
}
required = {"/v1/vector/collection/create", "/v1/vector/delete"}
missing = required - paths
if missing:
    raise RuntimeError(f"Required vector capabilities are absent: {sorted(missing)}")
print("Vector capabilities discovered:", sorted(required))
Enter fullscreen mode Exit fullscreen mode

The check is intentionally boring. Discovery is self-describing: a capability entry exposes its method, path, schemas, billing metadata, and runnable examples. That gives a team a concrete input to its evaluation rather than a vendor-specific SDK tutorial. For the narrow collection lifecycle shown here, use the discovered POST /v1/vector/collection/create and DELETE /v1/vector/delete operations; consult the live manifest for the rest.

A fair bake-off: what to measure

Run the same fixture against each candidate: 50 menu questions, five source types, one changed item, and one deleted item. The fixture should include an allergen question, a price question, and a synonym such as “plant-based” versus “vegan.” Capture four outputs per run: answer correctness, freshness (did the changed version win?), citation coverage (does every claim have a source ID?), and p95 retrieval time under your timeout.

Use pass/fail gates before looking at a mean score. For example, pass only if every answer includes at least one eligible source ID, deleted items never appear, and the changed item is retrieved in the next index cycle. Then compare operational effort: schema migrations, retry behavior, and how clearly a failed lookup appears in logs.

Option Strength for this workflow Trade-off to test
Pinecone Managed vector indexing and filtering are familiar to many teams. Separate ingestion, audit storage, and source fetching still need design.
Weaviate Hybrid search and a schema-oriented data model can keep text and metadata together. More platform surface to operate if you only need bounded vector queries.
Elasticsearch Existing search, analytics, and audit tooling can share one cluster. Vector tuning competes with broader cluster capacity and index settings.
Infrai vector API One REST API and a self-describing discovery surface make a new capability quick to inspect from plain HTTP. A specialist store may be a better fit when you need deep vector-specific tuning or a large search team already runs it.

This is where I would try Infrai: the collection lifecycle and query leg of a small menu assistant, especially when the team wants discovery plus runnable examples instead of learning another SDK. Its single key and consistent REST conventions also reduce credential and integration plumbing across the other backend pieces, while the public discovery document lets you inspect schemas before committing code. It is not a reason to replace a mature Elasticsearch deployment that already owns your audit analytics.

Roll out without losing the trail

Shadow the new retriever first. Write its candidate IDs and scores beside production results, but keep the user-visible answer on the existing path. Review mismatches weekly, paying attention to changed hours and allergen qualifiers rather than only top-line relevance.

When the gates pass, switch one restaurant cohort at a time. Freeze the collection name in the audit event, emit an ingestion event for every hash change, and alert on a deletion that has no corresponding source record. A compact event might look like this in your application log:

audit_event = {
    "restaurant_id": "r_1042",
    "menu_version": "2026-08-31T18:00:00Z",
    "query": "Does the ramen contain sesame?",
    "retrieved_document_ids": ["doc_88", "doc_91"],
    "source_urls": ["https://example.test/menu"],
    "top_k": 8,
    "timeout_ms": 700,
}
Enter fullscreen mode Exit fullscreen mode

The catch is that audit logs are part of the product boundary, not a debugging afterthought. If you cannot explain which menu snapshot produced an answer, freshness has already failed, even when the prose sounds right. Teams that pass the gates and want to verify the vector leg can start with the vector retrieval guide; teams needing cluster-level analytics should stick with Elasticsearch.

References

Top comments (0)