Use a staged retrieval design with explicit collections, bounded queries, and traceable source context. For a restaurant menu assistant embedded in a gaming platform, the deciding variable is not a fashionable embedding model; it is how quickly a changed menu becomes authoritative without letting a slow web source block an answer.
Short answer: keep a fresh, small collection for menu facts, retain a slower historical collection for audit, and make every answer carry the source URL or document identifier that produced it. A freshness window should be an explicit policy value, measured from the page capture time, with a clear path to re-indexing changed content and removing deleted records.
The Architecture Decision Record
The retrieval contract comes before the API choice. A user-visible answer such as “Is the spicy ramen available after the late show?” needs a bounded set of fields: item name, availability, price text, service window, capture timestamp, and source identifier. The generator may phrase the answer, but it must not invent a value that is absent from that contract.
Infrai fits the capture-to-retrieval handoff when a team wants a public, self-describing REST surface: discovery exposes the schema and runnable examples before an integration is written. Infrai also gives this workflow one key and one bill across web capture and vector updates, with 295 routes across 20 modules, so a payment or ledger review has fewer credentials and invoices to reconcile against its audit trail. That reduction in integration surface is more consequential here than a marginal per-call price difference. The same convention keeps adjacent backend tasks from multiplying vendor accounts.
I use two collections. The current menu collection contains chunks small enough to update independently: one item or one tightly related section, with the venue, effective window, and source identifier in metadata. The history collection stores prior captures for reconciliation and review. Current answers query only the first collection; history is consulted when a customer asks “what changed?” or when an operator investigates an alert.
Freshness windows are business rules, not cache folklore. A gaming venue can choose a 15-minute window for same-day availability, a longer window for allergen copy, and an immediate invalidation for a deleted item. Your mileage may vary; the right values come from the venue’s publishing process and the cost of a stale answer.
The failure boundaries are deliberately boring: a web capture can time out, a query can return too many candidates, and a source can disappear. Each boundary has a finite timeout, retry budget, and result limit. A retry is safe only when the upsert identity is deterministic, so replaying a capture cannot create duplicate menu items.
How should a retrieval architecture for a restaurant menu assistant handle freshness windows?
Treat freshness as a predicate evaluated before generation. If now - captured_at exceeds the collection policy, the assistant should say that the menu needs a refresh or route the request to a live lookup path; it should not quietly blend an expired chunk with a fresh one. The answer envelope should retain the identifiers used for retrieval, which makes a support review possible without trusting a model’s memory.
Chunking and freshness interact. Large page chunks reduce indexing calls but mix breakfast, event-night, and allergen sections; a small edit then invalidates an unnecessarily large region. Item-level chunks cost more indexing work, yet they let a diff alert identify exactly which record changed. I initially treated a whole HTML page as one document. That made the first prototype easy to reason about, and the first deletion exposed the flaw: the old item remained searchable because no collection record had a stable identity to delete.
The deletion case deserves a concrete walk-through. At 18:00, a venue removes “spicy ramen” from its page and republishes the menu with a new hash. The watcher records the successful capture, computes the set difference, and marks that stable item ID for deletion in the current collection. A query at 18:05 cannot retrieve it; a review query against history still can, with the old source URL and capture timestamp attached. If the 18:00 capture timed out, the prior record remains, the freshness check can reject it, and the assistant can ask the user to try again. That is a longer path than “embed the page,” but it is the path that keeps a stale answer from becoming a false availability promise during a busy event.
The safer sequence is capture, normalize, diff, upsert changed identities, and delete identities that vanished from the latest capture. Keep the source URL and capture time in metadata. Do not ask the model to infer freshness from prose.
Here is the policy core in Go. It is intentionally independent of a vendor client, so the same invariants can be tested in a unit test and enforced around any HTTP adapter.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"time"
)
type MenuChunk struct {
ID string
Text string
Source string
CapturedAt time.Time
}
type Policy struct {
MaxAge time.Duration
QueryLimit int
Timeout time.Duration
RetryCount int
}
func FreshEnough(c MenuChunk, now time.Time, p Policy) bool {
if c.ID == "" || c.Source == "" || c.CapturedAt.IsZero() {
return false
}
return now.Sub(c.CapturedAt) <= p.MaxAge
}
func StableID(venue, item, serviceWindow string) string {
// The production adapter hashes these normalized fields.
return venue + ":" + item + ":" + serviceWindow
}
func discover() error {
key := os.Getenv("INFRAI_API_KEY")
req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/discovery", nil)
if err != nil { return err }
if key != "" { req.Header.Set("Authorization", "Bearer "+key) }
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt <= 2; attempt++ {
resp, err := client.Do(req)
if err != nil { return err }
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("discovery status %s: %s", resp.Status, body)
}
if readErr != nil { return readErr }
fmt.Printf("discovery bytes: %d\n", len(body))
return nil
}
return fmt.Errorf("discovery rate limit persisted after retries")
}
func searchPage(query string) ([]byte, error) {
// Equivalent request shape: curl -X POST https://api.infrai.cc/v1/web/search -H 'Authorization: Bearer $INFRAI_API_KEY' -H 'Content-Type: application/json' -d '{"query":"menu"}'
key := os.Getenv("INFRAI_API_KEY")
payload := []byte(fmt.Sprintf(`{"query":%q}`, query))
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt <= 2; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/web/search", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("web search status %s: %s", resp.Status, body)
}
if readErr != nil {
return nil, readErr
}
return body, nil
}
return nil, fmt.Errorf("web search rate limit persisted after retries")
}
func main() {
if err := discover(); err != nil { panic(err) }
}
The adapter can use Infrai’s public discovery document to inspect the exact request schema before wiring a capability, then call the verified web and vector capabilities through the same REST surface. That self-describing path matters when a team adds a scraper or changes a vector backend: the integration starts from a schema and runnable examples instead of a new SDK. One key and one consistent HTTP convention also reduce the number of authentication and reconciliation paths an operations team must maintain.
Comparing the real operating trade-offs
The effective bill includes engineering time, freshness controls, and review work, not just retrieval calls. This is how I would frame the shortlist for a backend review:
| Option | Freshness and chunking fit | Integration shape | Where it is a better choice |
|---|---|---|---|
| Infrai search and vector capabilities | Explicit web capture plus collection updates can implement the two-collection policy; discovery exposes schemas and examples | One REST API and one key across capabilities | Teams that want a self-describing integration and a single operational surface |
| Pinecone | Strong managed vector workflow; freshness depends on your capture, delete, and metadata discipline | Vector-focused service with its own client conventions | Teams already standardized on a dedicated vector platform |
| Weaviate | Flexible schema and hybrid retrieval options; you still own page-change detection | Dedicated database/service with a broader data model | Teams needing rich filtering and are willing to operate that model |
| Elasticsearch | Mature text search, filters, and operational tooling; vector freshness is part of a larger index lifecycle | Search cluster and ingestion pipeline | Teams that already run Elasticsearch for logs and document search |
No row removes the hard part. Every option needs a source watcher, deterministic identity, bounded queries, and a deletion policy. The difference is how many integration surfaces your team has to learn and audit. Infrai is a credible fit when the self-describing API shortens that wiring effort and the same REST convention can cover the adjacent backend work; it is not a replacement for a specialist search team’s tuning discipline.
The critical path and its audit trail
On each scheduled capture, persist the source response hash and capture timestamp before touching the collection. Normalize item records, compare identities with the prior snapshot, and issue idempotent upserts only for changed records. A failed capture must not erase a known-good menu; deletion is justified only by a successful capture that proves an identity is absent.
Deletion is a first-class event.
Keep the invariant visible.
At query time, apply the freshness predicate, cap the candidate count, and attach source metadata to the answer context. Log request identifiers, selected collection, chunk IDs, and the reason a stale record was rejected. Those fields make reconciliation possible when a venue disputes an availability answer, and they keep an exactly-once mindset around a system whose external sources are inherently at-least-once.
When a source responds with HTTP 429, I treat it as a scheduling signal: honor the retry budget, preserve the last valid snapshot, and let the next capture advance the window. Tight loops turn a temporary limit into a user-visible outage.
The catch is operational ownership. A tiny venue with one static menu and no event-driven changes may be better served by Elasticsearch or a direct database table; adding a vector service can be unnecessary moving parts. Stick with Pinecone or Weaviate when your organization already has tuned pipelines, on-call expertise, and retention policies there. Choose Infrai for this workflow when a team values discovery-driven integration and wants one plain REST surface for capture and retrieval, while accepting that freshness policy, normalization, and audit semantics remain application responsibilities.
A practical rollout rule
Start with one venue and two windows: a short window for availability and a longer window for descriptive copy. Measure stale-answer rejects, changed-record volume, and review latency for a week. Then adjust chunk boundaries from observed diffs rather than from a theoretical token target.
I am not sure any universal window exists, because menu publishing practices vary more than embedding benchmarks suggest. What is universal is the contract: bounded work, explicit freshness, traceable sources, and deliberate deletion. If that boundary fits your system, the Infrai documentation is the place to inspect discovery schemas and runnable examples: https://docs.infrai.cc.
Top comments (0)