Use hard metadata contracts before semantic ranking when building a dining assistant over multiple menu feeds; this keeps retrieval quality measurable without spending the latency budget on records that can never be shown.
For a media platform aggregating restaurant listings, a plausible dish from the wrong city is still a bad result. So is a menu that expired at 2 p.m. The retrieval path should first enforce restaurant identity, market, publication state, service window, and source freshness. Only then should vector or lexical similarity sort the survivors. Dietary preferences and mood words can remain soft signals, provided the system has an explicit rule for relaxing them.
That ordering is the operational answer. Embeddings are not a policy engine.
What should metadata filters protect in a menu assistant?
Separate eligibility from relevance. Eligibility is binary: does this record belong to the requested restaurant, is it approved, and is it valid at the requested service time? Relevance is comparative: among eligible dishes, which one best matches “smoky, light, and shareable?” A single similarity score cannot safely carry both meanings.
Use stable IDs for hard constraints. Display names drift between feeds, and a restaurant group can reuse a name in two markets. Store an internal restaurant ID, source record ID, menu version, publication state, market, and source-observed timestamp. Keep the ingestion timestamp too; the two clocks answer different questions about feed lag.
Normalize timestamps to one comparison basis and retain the venue time zone for business rules. RFC 3339 can represent 2026-08-31T04:00:00Z; it cannot tell you whether that instant is brunch at a particular venue. Service windows belong in application logic, with tests around midnight and daylight-saving transitions.
The same identity fields make citations possible. A response can carry the exact source record and menu version into the generation step instead of asking a language model to infer provenance from a paragraph. Retrieval-augmented generation works because retrieved evidence accompanies generation; evidence without a stable version is difficult to audit.
Pick pre-, post-, or hybrid filtering from the failure budget
There are three useful placements, and each spends a different resource:
| Strategy | What happens | Main risk | Appropriate use |
|---|---|---|---|
| Pre-filter | Apply hard constraints before nearest-neighbor search | An over-specific filter can starve recall | Restaurant ID, market, publication, current service window |
| Post-filter | Retrieve broadly, then discard invalid records | Candidate budget is wasted; tail latency grows | Incomplete nutrition or optional preference fields |
| Hybrid | Pre-filter truth boundaries, post-filter soft preferences | More branches require stronger tests | Most production assistants |
If only 8 of the top 100 semantic candidates belong to the requested venue, post-filtering has already paid for 92 unusable records. Raising the candidate limit may recover recall, but it also increases reranking work and p99 latency. Capacity planning should ask how many candidates the narrowest normal filter needs, not how many vectors the index can store.
Post-filtering is reasonable for uncertain attributes. Nutrition data may be missing or stale, so treating “protein grams unknown” as an exclusion can erase a useful dish. Retrieve within the hard venue and availability boundary, rank records with matching nutrition metadata higher, and state when the field is absent.
The catch is selectivity. A threshold that works for a dense city can produce zero candidates for a small market. I'm not sure where your threshold belongs; the schema alone cannot answer it. Use a labeled set split by venue size, source, market, time of day, and metadata completeness, then choose a point on the measured quality/latency curve.
Preference relaxation must never relax tenancy, publication, restaurant identity, or current availability. That invariant belongs in a test, a review checklist, and an alert.
Make the candidate contract explicit in Go
The boundary should make hard constraints difficult to omit. A typed request gives validation and telemetry a stable shape while leaving the index implementation replaceable.
package retrieval
import (
"context"
"errors"
"sort"
"strings"
"time"
)
type Filters struct {
RestaurantID string
Market string
Published bool
ServiceAt time.Time
ObservedAfter time.Time
}
type MenuItem struct {
ID string
RestaurantID string
SourceRecordID string
MenuVersion string
Title string
Score float64
ObservedAt time.Time
}
type CandidateIndex interface {
Search(context.Context, string, Filters, int) ([]MenuItem, error)
}
type Retriever struct {
Index CandidateIndex
Now func() time.Time
}
func (r Retriever) Retrieve(ctx context.Context, query string, filters Filters, limit int) ([]MenuItem, error) {
if strings.TrimSpace(query) == "" {
return nil, errors.New("query is required")
}
if filters.RestaurantID == "" || filters.Market == "" {
return nil, errors.New("restaurant_id and market are required")
}
if !filters.Published {
return nil, errors.New("published must be true")
}
if filters.ServiceAt.IsZero() || filters.ServiceAt.After(r.Now().Add(24*time.Hour)) {
return nil, errors.New("service_at is outside the planning window")
}
if limit < 1 || limit > 100 {
return nil, errors.New("limit must be between 1 and 100")
}
items, err := r.Index.Search(ctx, query, filters, limit)
if err != nil {
return nil, err
}
sort.SliceStable(items, func(i, j int) bool {
if items[i].Score == items[j].Score {
return items[i].ID < items[j].ID
}
return items[i].Score > items[j].Score
})
return items, nil
}
Reject ambiguous values rather than silently broadening the query. An application can map validation failures to HTTP 422 and a superseded pinned menu version to HTTP 409; those are contract choices, not claims about a particular search product. Avoid free-form maps at the assistant boundary because misspelled fields look like legitimate zero-result searches and make authorization review harder.
The ingestion path should be just as strict: ingest, validate, normalize, version, index, pre-filter, retrieve, rerank, and cite. Deletion follows the source record ID through every stage, so an old index generation cannot resurrect a removed listing.
Verify retrieval quality and latency before release
An average relevance score is not a release gate. Build a labeled query set with exact dish names, vague cravings, dietary exclusions, closed service windows, duplicate listings, missing nutrition, changed prices, and deleted menu versions. Label relevance and eligibility separately; a semantically perfect but ineligible item must count as a failure.
Before choosing managed infrastructure or a self-hosted index, write down the operational trade-offs:
| Decision area | Managed option | Self-hosted option | Test evidence |
|---|---|---|---|
| Filter semantics | Provider operators and limits | Team owns schema and behavior | Contract tests for nulls, ranges, arrays, and time boundaries |
| Capacity | Elasticity under a service agreement | Team plans shards, replicas, and headroom | Load tests at realistic selectivity and update rates |
| Operations | Provider owns part of the data plane | Team owns upgrades, recovery, and paging | Recovery drills and on-call hours |
| Portability | Query syntax can bind the application | An abstraction can reduce coupling | Replay golden queries against another implementation |
Neither side is universally suitable. Managed infrastructure is a poor fit when policy requires full data-placement control or its operators cannot express your eligibility contract. Self-hosting is a poor fit when the team cannot staff upgrades, backups, repair, and capacity response. Stick with lexical or relational search when the catalog is small and semantic ranking does not improve a labeled evaluation enough to justify another dependency.
Track eligible-result rate, top-k relevance, stale-result rate, zero-result rate by controlled filter class, ingestion-to-search visibility lag, and p50/p95/p99 retrieval latency. A fast empty response is not success. OpenTelemetry supplies common metric and trace concepts, while SLO practice supplies the error-budget framing; the target values must come from your answer timeout and product risk.
Model bursts, not just averages. A useful test shape might include 12 source feeds, a 20-minute burst after menus publish, and candidate limits from 20 to 100. Those are test inputs, not benchmark claims; replace them with your feed count and traffic distribution before sizing production. Ship dark reads, compare eligibility and ranking without adding user latency, then canary a deterministic slice across a full menu-update cycle.
Slow down here.
Roll back while preserving evidence
Rollback should switch an immutable index alias or configuration generation to the last verified version. Do not rebuild data during the incident. Keep the previous generation readable until the canary observation window ends, and store the schema version with indexed records so the query service knows which filter contract it is applying.
Trigger rollback on an eligible-result SLO breach, stale-result growth beyond the error budget, or tail latency consuming the downstream answer budget. Preserve the failed generation and sampled trace IDs subject to retention and privacy rules. A compact replay fixture containing normalized filters, expected eligible IDs, menu versions, and timing metadata is enough to reproduce the decision without storing raw user queries by default.
The decision rule is compact: enforce truth boundaries first, rank surviving candidates second, and relax preferences only from measured evidence. Retrieval quality and latency then become observable engineering variables instead of arguments hidden inside a similarity score.
Top comments (0)