Home / Filtering / Faceted Search With Semantic Results
TutorialFaceted Search With Semantic Results
Faceted semantic search combines meaning-based ranking with facets, the clickable lists of brands, colours, price ranges or categories that show counts and let users narrow results. The difficulty is that facets count a set of matching items, while vector search ranks every item by distance and excludes none. A faceted semantic search must therefore decide which results the counts cover, usually the top N results or everything within a distance threshold, and use the same cut-off for the result list and every facet. Facet counts should also be disjunctive: each facet ignores its own selection, so users can see how many items other brands or colours would add. Weaviate’s documentation calls this a faceted vector search, and it rejects an aggregation over a vector search unless an objectLimit or a distance is set. Aggregations with group_by then return the counts per value.
Faceted semantic search in four facts
- Vector search ranks every object, so facet counts need a top-N limit or a distance cut-off.
- Weaviate’s aggregate over a near search fails unless objectLimit or a distance is given.
- Disjunctive facets drop their own filter when counting, so other options stay visible.
- Facet values work best as field-tokenized properties with Weaviate’s bitmap filter index.
What is faceted semantic search?
Faceted semantic search is a search interface where results are ranked by meaning and a panel of facets shows counts per attribute value, letting users filter the results with a click. It joins two well-known patterns.
Facets are familiar from shopping and library sites. A search for shoes shows a panel with brands, sizes, colours and price bands, each with a count, such as “red (42)”. Clicking a value filters the results, and the counts update. Facets help users understand what is available and narrow a large result set without retyping.
Semantic search changes how results are found. Instead of matching keywords, it ranks items by how close their embeddings are to the query, so “comfortable shoes for long walks” finds items described in other words. Combining the two gives users both the flexibility of meaning-based queries and the control of structured filters, as described in metadata filtering.
The combination is not automatic. Facets were designed for keyword search, where the set of matching documents is clear. With vector search, that set has to be defined first.
Why are facets harder with semantic search?
Facets are harder with semantic search because vector search returns a ranking of all items rather than a set of matches, so there is no natural group of results to count. Counting everything would count the whole catalogue.
In keyword search, a query for “red shoes” matches the documents containing those terms, and everything else is out. Facet counts describe that match set. Weaviate’s documentation makes the contrast explicit: vector searches rank objects by similarity but do not exclude any objects. Without a limit, a facet over a vector search would count every product in the collection.
Weaviate therefore requires a bound. Its aggregate documentation states that an aggregation combined with a near search must set objectLimit or a distance (or certainty), and the request fails otherwise. Internally the process has two steps: the vector search finds the objects, then the aggregation counts them. The difference between ranking and filtering is covered in searching vs filtering.
How do you choose the result set that facets count?
You choose the result set that facets count by capping it at the top N results or by keeping only results within a distance threshold, then applying the same rule to the result list. Each option fits a different interface.
An object limit counts the top N results. Weaviate’s documentation describes this as useful when you know in advance how many results you will serve, such as 100 recommendations. It gives predictable counts and matches what users can actually scroll through. Its weakness is that the counts depend on N rather than on how many items truly fit the query.
A distance threshold counts every result closer than a cut-off. Weaviate’s documentation suggests this for user-facing search such as ecommerce, where a user wants all items similar to a query like “apple iphone” and facets over them. The number of results varies by query, which feels natural, but the right threshold depends on the embedding model and must be tuned. Scores and their meaning are discussed in relevance scores.
Whatever the rule, use it everywhere on the page. If the result list shows the top 50 but facets count the top 200, a user who clicks “red (18)” may see only 5 red items. Consistency matters more than the exact number.
How should facet counts react to selections?
Facet counts should be disjunctive: each facet is counted with every other active filter applied but not its own, so users can still see and add other values. The result list applies all filters.
Suppose a user picks the brand Acme. If the brand facet were counted with the brand filter applied, it would show only “Acme” and every other brand would disappear, leaving no way to add a second brand. Counting the brand facet without its own filter keeps the other brands visible with their counts. Within one facet, values usually combine with OR; across facets, with AND.
This costs extra requests. A page with two facets needs one search for the results and one aggregation per facet, each with a different filter set. Weaviate’s filters run before ranking, so every request applies its filters efficiently, as explained in pre-filtering vs post-filtering.
How do you build faceted semantic search in Weaviate?
You build faceted semantic search in Weaviate by running a filtered vector or hybrid query for the results and an aggregation with group_by per facet, all sharing one distance cut-off or object limit. Facet properties use field tokenization.
from weaviate.classes.query import Filter
from weaviate.classes.aggregate import GroupByAggregate
products = client.collections.use("Product") # name, brand, color (field tokenized), price
brand_f = Filter.by_property("brand").equal("acme")
color_f = Filter.by_property("color").equal("red")
CUT = dict(distance=0.5) # or object_limit=200, same everywhere
results = products.query.near_vector(q_vec, limit=24, filters=brand_f & color_f, **CUT)
def facet(prop, filters):
agg = products.aggregate.near_vector(q_vec, group_by=GroupByAggregate(prop=prop),
filters=filters, total_count=True, **CUT)
return {g.grouped_by.value: g.total_count for g in agg.groups}
brand_counts = facet("brand", color_f) # brand facet ignores the brand filter
color_counts = facet("color", brand_f) # color facet ignores the color filter
The pattern was run on Embedded Weaviate 1.30.5 with five toy products as an API check. With a red filter, the results and the brand facet both showed one Acme and one Peak product; the colour facet, counted without its own filter, showed red 2 and blue 1. Swapping the distance for an object limit of 4 changed the brand counts to 2 each, which shows why the cut-off must be shared.
Real queries usually come from a vectorizer, so near_text replaces near_vector. Facet values that users can choose several of at once combine with Filter.any_of or contains_any.
How do facets stay fast?
Facets stay fast when their properties are indexed for filtering with bitmaps, use field tokenization so each value is one token, and keep the number of distinct values manageable. Counting is then a set operation.
Weaviate’s filterable index uses Roaring bitmaps, one per value, which makes filtering and counting cheap, as described in bitmap filters. Field tokenization stores “Running Shoes” as one value instead of two words, so a facet counts whole values. Free-text fields make poor facets.
The search side sets the main cost. Each aggregation repeats the vector search, so a page with many facets multiplies the work. Keeping the cut-off modest, limiting facets to those users actually use, and caching counts for common queries keep latency down. Very selective filter combinations have their own costs, covered in filter selectivity and filtered ANN search.
Which facets are most useful in semantic search?
The most useful facets are attributes they think in, such as brand, size, price band or document type, with a small number of values sorted by count. Too many facets or values overwhelm the panel.
Numeric attributes work best as ranges. Prices and dates become bands, such as under 50, 50 to 100 and over 100, each counted with a range filter. Weaviate’s range filter index speeds up those comparisons. Long-tail values can be collapsed into “more” lists, with the top values shown by count.
Semantic queries change which facets matter. A meaning-based query such as “gift for a gardener” may match items across many categories, so a category facet becomes a guide to what the system found. Permissions and tenants also shape facets: counts must only include items the user may see, as described in permission-aware search and multi-tenant semantic search. The choice of database matters too, as compared in vector databases with metadata filtering.
How do you test faceted semantic search?
You test faceted semantic search by checking that counts match what users see after clicking, and that the cut-off keeps relevant items while excluding unrelated ones. Both errors are visible to users.
Start with consistency. For a sample of queries and filter combinations, click each facet value and compare the number of results with the count shown. Any mismatch points to different cut-offs or filters between the search and the aggregation. Automated tests can run the same comparison on every release.
Then check the cut-off. For common queries, look at the last results inside the threshold or limit. If they are clearly unrelated, the facets are counting noise and the cut-off is too loose; if obviously relevant items fall outside, it is too tight. Tune the distance per model, and revisit it when the embedding model changes, because typical distances shift with the model.
Faceted semantic search pairs meaning-based ranking with facet counts, but vector search ranks every item, so facets need a defined result set. Count the top N results or everything within a distance threshold, and use the same cut-off for the result list and every facet. Make facets disjunctive, with each facet ignoring its own selection. In Weaviate, run a filtered vector or hybrid query for results and an aggregation with group_by per facet, which requires an objectLimit or distance. Store facet values with field tokenization so bitmap filters keep counting fast, and offer a few facets users actually think in.