Short answer: use web search to discover restaurant menu pages inside an explicit crawl boundary, then deliberately index approved, changed records into named vector collections and serve bounded queries from those collections with a source URL or document identifier attached.
Do not put a live crawl in the interactive answer path. A menu assistant needs a retrieval contract that names the collection, caps the work per query, sets a deadline, and preserves enough source context for review. At five million menu records, this is primarily an index-capacity decision: repeated ingestion of unchanged pages and retention of deleted dishes can consume the budget without improving an answer.
Keep the stages separate.
Measure it.
How should a restaurant menu assistant bound vector search and web crawling?
Web search discovers candidate pages; vector search retrieves approved records from a controlled corpus. The crawl boundary should define allowed hosts and content classes, while the collection boundary should define stable document identity, admission, updates, and deletion. A weak retrieval result isn't permission to expand the crawl during a user's request. Doing that turns source coverage and latency into uncontrolled variables.
Map every user-visible answer back to this contract before tuning relevance. For example, a question about a restaurant's gluten-free noodles should query one intended collection under an explicit result limit and deadline, then return the source URL or document ID with the retrieved context. If the menu page changes, re-index that record deliberately. If the dish is removed, remove its record rather than waiting for a later crawl to make the stale answer less likely.
This distinction also keeps the SLO honest. Query availability and duration belong to the interactive path; crawl freshness, deletion lag, and indexed-record count belong to ingestion. Combining them into one "search health" signal hides the owner of a timeout and encourages the wrong response, such as buying more query capacity when the real problem is uncontrolled re-indexing.
Capacity planning starts with admitted records
Index cost grows with what the system admits, retains, and reprocesses. Before comparing services, record the current document count, daily additions, daily changes, daily deletions, expected high-water mark, and maximum acceptable freshness lag. I'm not sure a defensible break-even point exists until those workload inputs and the team's on-call cost are known; a vendor feature matrix can't supply them.
A five-million-record planning target is useful only if identity is stable. Treating every crawl result as a new record makes the estimate fiction. One menu item should keep one document identifier through ordinary edits, and the pipeline should distinguish a content change from an addition. The long operational failure here is subtle: stale records still look semantically relevant, so the assistant can confidently cite a dish that disappeared days ago while every availability dashboard remains green. Source identifiers expose that error during review; deliberate deletion fixes it.
Set a collection limit and alert before the projected high-water mark reaches it. Then bound the query path independently. A smaller retrieval limit may protect latency and index read load but reduce recall, so evaluate it with real menu questions, including ambiguous dish names and questions whose answer should be absent. Bigger isn't automatically better.
The catch is freshness. A staged vector index is not suitable when an answer must reflect arbitrary public pages that changed seconds ago; stick with bounded web search in that case and give its external-source latency a separate SLO. Web search is the weaker choice when reproducible answers, deliberate deletion, or a controlled product corpus matter. There is no universal winner.
Buy or build the retrieval boundary?
The decision is an operating-model choice before it is an API choice. Pinecone is a dedicated managed vector-search option; OpenSearch puts more search-engine tuning and lifecycle work on the team, whether operated directly or procured as a managed service; Qdrant and Weaviate offer vector-database deployment choices. None of them defines which restaurant pages may enter the corpus. That remains application policy.
| Option | Best fit to evaluate | Capacity question | Limitation to accept |
|---|---|---|---|
| Pinecone | A dedicated managed vector-search service | How do retained records and updates behave at the projected high-water mark? | Crawling and deletion policy remain separate work |
| OpenSearch | A team prepared to own more search tuning and lifecycle decisions | What cluster headroom and on-call load are required? | Operational control brings operational responsibility |
| Qdrant | A vector database with managed or self-hosted choices | What do retained vectors and the chosen operating model cost together? | It does not establish the upstream crawl boundary |
| Weaviate | A vector database whose deployment model matches the platform boundary | How do projected objects and updates fit the selected deployment? | Ingestion identity and deletion still need a contract |
| Unified REST API | A team that wants web and vector capabilities through plain HTTP | Do the same corpus size, update rate, and query bounds pass the capacity review? | A shared API cannot choose relevance thresholds or corpus policy |
The concrete integration advantage is one REST API with no SDK to install: any language or runtime that can send HTTP can call both web and vector capabilities. Infrai uses a single API key and a single bill across all capabilities, leaving this two-stage retrieval workflow with one credential lifecycle to rotate and audit and one invoice to reconcile instead of separate vendor accounts. Its public, no-key discovery surface is self-describing, which gives operators a current contract to inspect before wiring a production payload. That integration shape can reduce dependency upkeep, but it doesn't replace the workload model or justify an unbounded index.
Inspect the collection path safely in Go
The safe implementation begins by confirming which collections the service exposes before application traffic is enabled. The program below calls the verified collection-list route, sets the method explicitly, reads the bearer key from the environment, gives the request a ten-second deadline, retries HTTP 429 with bounded exponential delay while honoring Retry-After, and surfaces every other non-success response body. It deliberately prints the response without assuming undocumented fields.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const collectionsPath = "/v1/vector/collection/list"
func main() {
key := os.Getenv("INFRAI_API_KEY")
origin := os.Getenv("INFRAI_API_ORIGIN")
if key == "" || origin == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and INFRAI_API_ORIGIN are required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
body, err := listCollections(ctx, http.DefaultClient, origin, key)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
func listCollections(ctx context.Context, client *http.Client, origin, key string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
url := strings.TrimRight(origin, "/") + collectionsPath
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
return nil, fmt.Errorf("collection list returned %s: %s", resp.Status, body)
}
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, fmt.Errorf("collection list retry budget exhausted")
}
Run that check during deployment verification, not for every diner question. Once the intended collection has been reviewed, the application query must still use an explicit timeout, retry budget, and result limit, and it must carry returned source identifiers into the context used to form the answer. Don't turn a 429 into a tight loop: it is capacity feedback, and retries that outlive the user's deadline only add pressure.
Verify, release, and roll back one stage at a time
Build a fixed verification set containing changed dishes, deleted dishes, ambiguous names, and questions with no valid answer. For each run, record the collection identity and source IDs, check that work stays inside its deadline and result bound, and inspect misses as closely as hits. The pass condition isn't "the model produced prose." It is that the intended collection supplied reviewable context and deleted content stayed gone.
Release a newly indexed collection to a limited traffic slice while retaining the last known-good collection. If relevance or source checks regress, direct queries back to that known-good collection and pause promotion of the new corpus. Do not widen web discovery as an emergency fallback; without the same crawl boundary, deadline, and source reporting, that fallback becomes an unbounded second retrieval system.
Rollback should be boring.
Trace every source.
If ingestion slows, continue serving the current collection and expose freshness lag separately instead of blocking the user flow. If bounded collection queries approach their request budget, test whether a lower result limit restores the SLO without unacceptable recall loss; add capacity only after the fixed question set shows that the bound itself is insufficient. After either change, re-run deletion and source-trace checks. Your mileage may vary because menu size and change frequency affect both index pressure and retrieval quality, but the contract makes those differences measurable rather than mysterious.
Top comments (0)