Facets
Counts, buckets and hierarchies over the metadata a collection indexes. Declare the shape once on the collection, then ask for the aggregations you want per query.
A facet is an aggregation over one indexed metadata field, the thing that turns
a result list into a filter sidebar reading "Published (42), Draft (15)". Both
built-in adapters support them. Values land in their own table at index time,
one row each, so a count is a GROUP BY and never a parse of your metadata.
Declare the fields
Facets read from metadata, so a field has to be in the projection before it
can be faceted. Facet keys should match your metadata keys.
collection("products").searchable({
metadata: (r) => ({ category: r.category, price: r.price, tags: r.tags }),
facets: {
category: true,
tags: { type: "array" },
price: {
type: "range",
buckets: [
{ label: "Under $10", max: 10 },
{ label: "$10 to $50", min: 10, max: 50 },
],
},
},
});| Config | What it indexes |
|---|---|
true | One row per record, the value as a string. |
{ type: "array" } | One row per item. Skips the field if the value is not an array. |
{ type: "range", buckets } | One row carrying the matching bucket label and the number itself. |
{ type: "hierarchy", separator } | One row per level of the path. separator defaults to " > ". |
A hierarchy value of "Electronics > Phones > iPhone" indexes three rows, one
for each prefix, so a count exists at every depth. A range value that matches no
bucket is dropped, and so is any field whose value is null or undefined.
Ask for them
const { facets } = await app.search.search({
query: "shoes",
facets: [{ field: "category", limit: 10, sortBy: "count" }],
});
// [{ field: "category", values: [{ value: "sneakers", count: 12 }, …], stats }]Each requested facet is { field, limit?, sortBy? }. limit defaults to 10
and sortBy to "count", descending. Pass "alpha" for alphabetical instead.
A range facet also comes back with stats, the min and max of the numeric
values behind it, which is what you need to place a price slider. Any other
facet type leaves stats undefined, because only a range indexes a number.
Facets are scoped by the same query
They are computed over the rows the current query matched, after the
collections, locale and filters narrowing, and over authorized rows only
when the request came through HTTP. Change the query and the counts change.
Browsing without a query
query: "" skips ranking entirely and orders by updatedAt descending, which
makes a facets-only request cheap. Pair it with limit: 0 when you want the
counts for an empty filter state and no documents.
const { facets } = await app.search.search({
query: "",
limit: 0,
facets: [{ field: "status" }],
});Related
- Search, the query options facets ride along with.
- Collections, the
.searchable()config.
Embeddings
The vectors behind semantic search. What the pgvector adapter embeds on its own, the two provider factories QUESTPIE ships, and how to override the vector for one collection.
Scaling
A live query that re-runs per subscriber is correct at every size and affordable only at some sizes. This page is the modeling that decides which one you have.