---
id: p1-03
title: Dashboard search - unified cross-surface ranking (messages + mail + calendar)
priority: p1
status: done
scope: mac-sync
tags: [search, unified, cross-surface, ranking, rrf]
owner: search
updated_at: 2026-04-24
evidence:
  - "src/server/src/features/search/service.ts:unifiedSearch — parallel fan-out to messages+mail+calendar, RRF fusion with env-configurable per-surface weights"
  - "src/server/src/surfaces/my/search.ts:GET /my/search — validated q param (1-256 chars), returns UnifiedSearchResponse"
  - "src/server/src/entities/event/schema.ts:2026-04-24_event_fts — GIN-indexed search_tsv on events (title+notes+location)"
  - "src/server/src/entities/event/repo.ts:searchEvents — FTS via websearch_to_tsquery + ts_headline with «» markers"
  - "web/src/components/GlobalSearch.tsx:GlobalSearch — global search box in AppShell sidebar above nav, results grouped by surface"
  - web/src/layout/AppShell.tsx — GlobalSearch mounted between Brand and NavList
  - "web/src/hooks/useSearch.ts:useUnifiedSearch — React Query hook for /my/search"
  - src/server/src/test/unified-search.seed-queries.md — 10 seed queries baseline for regression
blocked_by: [p1-02]
---
## Summary

A single `GET /my/search?q=…` endpoint that fans out to per-surface candidate generators (FTS + vector top-K), then fuses results via reciprocal rank fusion with configurable per-surface weights. Photos deferred to p2-01 (blocked on @imajin classifier readiness).

## Approach

- Surface-agnostic embedding store keyed by `(surface, entity_id)` — same model version across surfaces so cosine distances are directly comparable.
- Fan-out query: each surface produces (FTS top-K) + (vector top-K), merged via RRF with `env`-configurable weights.
- UI: global search box in the app shell (above tab navigation), results grouped by surface with an "All" tab using the fused ranking.

## Acceptance criteria

- [ ] `GET /my/search?q=…` returns results from messages + mail + calendar with a stable fused score
- [ ] Per-surface candidate generation p95 < 400ms; fused endpoint p95 < 800ms
- [ ] Result quality spot-check: 10 seed queries each have a documented top-3 for regression
- [ ] Global search UI lives above the tab navigation (not inside a single tab)
- [ ] Photos surface omitted (covered by p2-01 when @imajin classifier is production-ready)
