Hybrid search with post-filtering
Introduced 2.13
You can perform post-filtering on hybrid search results by providing the post_filter parameter in your query.
The post_filter clause is applied after the search results have been retrieved. Post-filtering is useful for applying additional filters to the search results without impacting the scoring or the order of the results.
Post-filtering does not impact aggregation results.
To filter all subqueries during query execution instead of filtering the final results, use a common filter. For more information, see Hybrid search with pre-filtering.
Example: Faceted search with post-filtering
Post-filtering is commonly used in faceted search, in which the UI displays aggregation counts (such as brand, color, and size filters) alongside search results. Using a post_filter keeps the aggregation counts based on the full unfiltered query while filtering only the displayed hits.
Consider an index containing product documents:
{
"name": "Nike Air Max",
"brand": "Nike",
"color": "Red",
"size": 10,
"price": 120,
"category": "Running Shoes"
}
A user searches for “running shoes”, and the application constructs a query containing aggregations for brand, color, and size:
POST /products/_search
{
"query": {
"match": {
"category": "running shoes"
}
},
"aggs": {
"brands": {
"terms": { "field": "brand.keyword" }
},
"colors": {
"terms": { "field": "color.keyword" }
},
"sizes": {
"terms": { "field": "size" }
}
}
}
The response returns hits from all brands:
Nike Air Max
Nike Pegasus
Adidas Adizero
Puma Velocity
...
The response also returns aggregations that include counts for every brand, color, and size:
Brands: Nike (120), Adidas (80), Puma (45)
Colors: Black (90), White (70), Red (55)
Sizes: 8 (40), 9 (60), 10 (85)
The aggregations are typically displayed as facet filters in the UI. When a user selects a specific brand (for example, Nike) to filter results, using a pre-filter would exclude non-Nike documents before aggregations are computed, causing other brands to disappear from the facet counts.
With post_filter, the query and aggregations run on the full result set. The filter is applied only to the displayed hits:
POST /products/_search
{
"query": {
"match": {
"category": "running shoes"
}
},
"aggs": {
"brands": {
"terms": { "field": "brand.keyword" }
},
"colors": {
"terms": { "field": "color.keyword" }
}
},
"post_filter": {
"term": { "brand.keyword": "Nike" }
}
}
The hits contain only Nike products, but the aggregations still reflect the full unfiltered query:
Brands: Nike (120), Adidas (80), Puma (45)
Colors: Black (90), White (70), Red (55)
All brand options remain visible in the facet, allowing the user to switch brands or compare counts without removing the filter.
How post-filtering affects search results and scoring
Post-filtering can significantly change the final search results and document scores. Consider the following scenarios.
Single-query scenario
Consider a query that returns the following results:
- Query results before normalization:
[d2: 5.0, d4: 3.0, d1: 2.0] - Normalized scores:
[d2: 1.0, d4: 0.33, d1: 0.0]
After applying a post-filter to the initial query results, the results are as follows:
- Post-filter matches
[d2, d4] - Resulting scores:
[d2: 1.0, d4: 0.0]
Note how document d4’s score changes from 0.33 to 0.0 after applying the post-filter.
Multiple-query scenario
Consider a query with two subqueries:
- Query 1 results:
[d2: 5.0, d4: 3.0, d1: 2.0] - Query 2 results:
[d1: 1.0, d5: 0.5, d4: 0.25] - Normalized scores:
- Query 1:
[d2: 1.0, d4: 0.33, d1: 0.0] - Query 2:
[d1: 1.0, d5: 0.33, d4: 0.0]
- Query 1:
- Combined initial scores:
[d2: 1.0, d1: 0.5, d5: 0.33, d4: 0.165]
After applying a post-filter to the initial query results, the results are as follows:
- Post-filter matches
[d2, d4] - Resulting scores:
- Query 1:
[d2: 5.0, d4: 3.0] - Query 2:
[d4: 0.25]
- Query 1:
- Normalized scores:
- Query 1:
[d2: 1.0, d4: 0.0] - Query 2:
[d4: 1.0]
- Query 1:
- Combined final scores:
[d2: 1.0, d4: 0.5]
Observe that:
- Document
d2’s score remains unchanged. - Document
d4’s score has changed.