How visual event search works
Every query typed into the Events page runs ONE filtered HNSW pass over per-frame visual embeddings and ranks events by similarity to your intent. A natural-language parser sits in front of the engine, translating “white truck at the loading dock yesterday afternoon” into the structured fields the engine consumes — chip filters, camera or site references, a time window, an event category, and the semantic terms that drive the visual rank.
The Phase 4 rebuild (May 2026) replaced the older multi-lane RRF model with a single filtered HNSW path because the new MobileCLIP2-B embedding space is precise enough that fusing additional lanes added complexity without measurable recall. The retired three-lane design (Tag + Text + Vector with Reciprocal Rank Fusion) is documented as historical context in docs/arch-search.md §§1-9; the canonical implementation is in docs/arch-search.md §10.
Why this works this way
The Events page was originally split from a Search page, and the split confused users. R6 records the merge: one page, one input, with the search box treated as an optional filter on top of the chronological feed. When the input is empty you see a time-ordered stream. When you type, the engine re-sorts the feed by visual similarity to your query.
The visual ranking pipeline is:
- Parser. Gemini 3.1 Flash-Lite (default
gemini-3.1-flash-lite, temperature 0.1, seecloud/config/search_config.json) translates the prose into structured filters and a semantic query. A rule-based parser is the fallback when Gemini is unavailable or the request opts out. - Query embedding. The semantic query is encoded by MobileCLIP2-B via the local AI Runtime sidecar (
cloud/cmd/server/main.go:2206). The 512-dim, L2-normalized vector shares the embedding space withframe_embeddings.embedding, which is the only reason the cosine search returns meaningful results. - Filtered HNSW. ONE
pgvectorHNSW query againstframe_embeddingswith the chip filters compiled into the WHERE clause (camera, site, date range, event category, source, AI status, plusEXISTS event_objectsfor object chips andEXISTS event_facesfor face profile chips). The query runs inside a read-only tx withSET LOCAL hnsw.iterative_scan = strict_orderso the planner keeps exploring candidates if early ones get filtered out by the org-scoped predicate. - Aggregation. Best-frame per event via
MIN(distance); the per-frame index returns the best-matching frame and the engine groups byevent_id. - Go-side rerank. A bulk lookup (one query per
Search()call) populates three boolean axes per result —object_match,canonical_tag_match,face_profile_match— by intersecting the rendered hits with the explicit filter values. The current production code applies a single+0.15rerank bonus when either an object filter OR a face filter matched; tag-only matches do not add a bonus (the boost is reserved for explicit class/face filters perengine.go:300-321). The v5 search-ranking PRD §7 prescribes a granular+0.05 / +0.05 / +0.10per-axis bonus — that’s a known PRD-vs-code divergence tracked indocs/audits/2026-05-21-manual-alignment/gaps-by-phase/phase-2.mdGAP-2-12; the operator-visible behaviour today is binary. - Fallback. When the planner estimates fewer than ~100 rows would match the EXISTS predicates (typical for very selective object/face filters), an
ExplainGate(cloud/internal/search/explain_gate.go) drops the EXISTS predicates from the HNSW query, runs it with 4× oversample, and post-filters in Go viaEventObjectLookup. This is the only fallback in EngineV2; the pre-Phase-4 “tag-strict relaxation” was retired alongside the multi-lane model.
The natural-language parser is what makes “white truck yesterday afternoon at the loading dock” work. A small LLM call (gemini-3.1-flash-lite, temperature 0.1) parses the user’s prose into the structured fields above. The parsed query then feeds the engine. If the parser fails or the LLM is offline, the rule parser still drives the visual lane on the raw query string — search degrades, it does not break.
The Ask AI cohort chat
The Ask AI sheet on the Events page (event list, NOT event detail — frontend/src/pages/EventsPage.tsx:880-889) is a separate surface on top of the same data. It POSTs the visible cohort’s event IDs to POST /api/v1/events/chat and the LLM returns:
- a freeform
answer, - a
confidencescore, - a
sourceslist of the most-relevant event IDs, - a
matching_event_idsarray — the canonical set of events the LLM claims match the question. For count questions the backend (cloud/internal/search/count_grounding.go, post-FE2E-QA commitfb10d16b) rewrites any stated count inanswertolen(matching_event_ids)so the user-visible count is always grounded in the enumeration.
Per-event chat (the POST /api/v1/events/{id}/chat endpoint described in older docs) was retired in L12; only the cohort endpoint remains.
What this means for you
- An empty search box shows the chronological feed; typing turns it into a ranked result list.
- Chip filters (object class, recognised face, canonical tag, camera, site, time, event category) are the explicit predicates the rerank bonus reads from. Without a chip filter, the rank is pure visual similarity.
- Prose queries like “someone loitering near the back door last night” parse to a time window plus a semantic query; v5 cannot detect
loiteringdirectly (it’s on the unsupported-keyword list, seecloud/internal/events/object_categories.go:152-158), so the parser emits a warning and the query falls back to pure visual similarity. - Multilingual queries work through Gemini, but the launch gate is bilingual English + one CJK pair top-5 recall ≥ 80% (v5 search-ranking PRD §10). Other languages are not benchmarked.
- The score panel on each hit (open it via the score chip) breaks the match into
base_scoreplus three rerank axes — Object / Tag / Face. The finalsimilarity_scoreisbase_score + 0.15when Object OR Face matched, otherwise equal tobase_score.
Where to go next
- Filter events by camera, date, and tag — the chip-filter how-to
- Search with natural language — the prose-query how-to
- Universal ONVIF event taxonomy — the D95 event_category vocabulary chip filters target
- Canonical AI tags — compiled/search vocabulary and YOLO-derived event tags