search: block to an entity to get BM25 ranking, live facet counts, and sort across millions of rows, with no separate search server such as Meilisearch or Elasticsearch. The index lives inside the same SQLite or Postgres database, in the same transaction as your writes.
Declaring a searchable entity
Searching from the client
db.useSearch is the React hook. It returns ranked hits, per-facet counts, a total, and timing.
Searching from a server function
ctx.db.search. Use it when you need to search inside a transaction or pre-aggregate before returning.
How it works
- Text matching: SQLite FTS5 or Postgres
tsvector, with BM25 ranking by default, configurable per field. - Facets: roaring bitmaps stored in a
_facet_bitmaptable, one bitmap per(entity, column, value). Intersection across active filters is bit-AND, which is faster thanWHEREclauses by 10–100× at scale. - Sort and paginate: when the planner needs to sort across the entire match set, it materializes hit ids into a temporary table, joins back to the row table, and applies
ORDER BYtogether withLIMITandOFFSET. This is the only way to paginate consistently across a sorted projection. - Aggregation safety: faceted search refuses to run on entities whose read policy depends on per-row data. Otherwise, facet counts would leak the existence of rows the caller cannot read. To opt in, make your read policy row-independent (for example,
auth.userId != null), or scope reads with a server function.
API
POST /api/search/:entity accepts:
Performance
Native search trades the operational cost of a separate index server for slightly higher per-query latency on very large datasets (over 10 million rows). For many B2B SaaS workloads, it is faster overall: no network hop, no asynchronous indexing lag, and no second system to monitor. Theexamples/store example runs a 10,000-product catalog with 3-facet search at sub-5ms p95 on a $5 VPS.
Next
Faceted search example
Walk through the store with code highlights.
Live queries
How search results stay up to date.