Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
e3012b7
Initial impl
mdashti Sep 25, 2025
1e0346a
Added `Filter` impl in `build_single_agg_segment_collector_with_reade…
mdashti Sep 25, 2025
80dd75e
Added `Filter(FilterBucketResult)` + Made tests work.
mdashti Sep 25, 2025
a77343a
Fixed type issues.
mdashti Sep 25, 2025
7c93dd1
Fixed a test.
mdashti Sep 25, 2025
1cf56c0
8a7a73a: Pass `segment_reader`
mdashti Sep 25, 2025
499d753
Added more tests.
mdashti Sep 25, 2025
15baaa7
Improved parsing + tests
mdashti Sep 25, 2025
be46f6e
refactoring
mdashti Sep 25, 2025
953b419
Added more tests.
mdashti Sep 25, 2025
71edc1d
refactoring: moved parsing code under QueryParser
mdashti Sep 25, 2025
bca4a6b
Use Tantivy syntax instead of ES
mdashti Sep 25, 2025
f1e2e6b
Added a sanity check test.
mdashti Sep 26, 2025
5f7d714
Simplified impl + tests
mdashti Sep 26, 2025
49585c8
Added back tests in a more maintable way
mdashti Sep 26, 2025
aa6296d
nitz.
mdashti Sep 26, 2025
d534d72
nitz
mdashti Sep 26, 2025
725c829
implemented very simple fast-path
mdashti Sep 26, 2025
9ce3425
improved a comment
mdashti Sep 26, 2025
22950df
implemented fast field support
mdashti Sep 26, 2025
bf3d3f6
Used `BoundsRange`
mdashti Sep 26, 2025
621933a
Improved fast field impl + tests
mdashti Sep 26, 2025
a82877e
Simplified execution.
mdashti Sep 26, 2025
22735f9
Fixed exports + nitz
mdashti Sep 28, 2025
622c9c5
Improved the tests to check to the expected result.
mdashti Sep 29, 2025
564ee32
Improved test by checking the whole result JSON
mdashti Sep 29, 2025
c6cb527
Removed brittle perf checks.
mdashti Sep 29, 2025
d249699
Added efficiency verification tests.
mdashti Sep 29, 2025
cb5cce3
Added one more efficiency check test.
mdashti Sep 29, 2025
420c9d1
Improved the efficiency tests.
mdashti Sep 29, 2025
fecbe98
Removed unnecessary parsing code + added direct Query obj
mdashti Sep 29, 2025
410d091
Fixed tests.
mdashti Sep 29, 2025
767ec67
Improved tests
mdashti Oct 1, 2025
576fd4f
Fixed code structure
mdashti Oct 1, 2025
7aeba9e
Merge branch 'main' into paradedb-current-main/filter-impl
mdashti Oct 2, 2025
b19ede5
Fixed lint issues
mdashti Oct 2, 2025
a3712c1
nitz.
mdashti Oct 2, 2025
271e5e1
nitz
mdashti Oct 2, 2025
1ac2c6f
nitz.
mdashti Oct 2, 2025
c15cfa5
nitz.
mdashti Oct 2, 2025
567ca87
nitz.
mdashti Oct 2, 2025
4deca43
Added an example
mdashti Oct 2, 2025
dc100d3
Fixed PR comments.
mdashti Oct 3, 2025
c973235
Applied PR comments + nitz
mdashti Oct 3, 2025
2f550d8
nitz.
mdashti Oct 3, 2025
319d2b3
Improved the code.
mdashti Oct 3, 2025
04eb526
Fixed a perf issue.
mdashti Oct 7, 2025
c8accb6
Added batch processing.
mdashti Oct 7, 2025
0b84fb2
Made the example more interesting
mdashti Oct 7, 2025
2c69c15
Fixed bucket count
mdashti Oct 7, 2025
b75a477
Renamed Direct to CustomQuery
mdashti Oct 7, 2025
807b7de
Fixed lint issues.
mdashti Oct 7, 2025
b6305e9
No need for scorer to be an `Option`
mdashti Oct 8, 2025
5660916
nitz
mdashti Oct 8, 2025
5faf5e1
Used BitSet
mdashti Oct 8, 2025
fa374da
Added an optimization for AllQuery
mdashti Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions examples/filter_aggregation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// # Filter Aggregation Example
//
// This example demonstrates filter aggregations - creating buckets of documents
// matching specific queries, with nested aggregations computed on each bucket.
//
// Filter aggregations are useful for computing metrics on different subsets of
// your data in a single query, like "average price overall + average price for
// electronics + count of in-stock items".

use serde_json::json;
use tantivy::aggregation::agg_req::Aggregations;
use tantivy::aggregation::AggregationCollector;
use tantivy::query::AllQuery;
use tantivy::schema::{Schema, FAST, INDEXED, TEXT};
use tantivy::{doc, Index};

fn main() -> tantivy::Result<()> {
// Create a simple product schema
let mut schema_builder = Schema::builder();
schema_builder.add_text_field("category", TEXT | FAST);
schema_builder.add_text_field("brand", TEXT | FAST);
schema_builder.add_u64_field("price", FAST);
schema_builder.add_f64_field("rating", FAST);
schema_builder.add_bool_field("in_stock", FAST | INDEXED);
let schema = schema_builder.build();

// Create index and add sample products
let index = Index::create_in_ram(schema.clone());
let mut writer = index.writer(50_000_000)?;

writer.add_document(doc!(
schema.get_field("category")? => "electronics",
schema.get_field("brand")? => "apple",
schema.get_field("price")? => 999u64,
schema.get_field("rating")? => 4.5f64,
schema.get_field("in_stock")? => true
))?;
writer.add_document(doc!(
schema.get_field("category")? => "electronics",
schema.get_field("brand")? => "samsung",
schema.get_field("price")? => 799u64,
schema.get_field("rating")? => 4.2f64,
schema.get_field("in_stock")? => true
))?;
writer.add_document(doc!(
schema.get_field("category")? => "clothing",
schema.get_field("brand")? => "nike",
schema.get_field("price")? => 120u64,
schema.get_field("rating")? => 4.1f64,
schema.get_field("in_stock")? => false
))?;
writer.add_document(doc!(
schema.get_field("category")? => "books",
schema.get_field("brand")? => "penguin",
schema.get_field("price")? => 25u64,
schema.get_field("rating")? => 4.8f64,
schema.get_field("in_stock")? => true
))?;

writer.commit()?;

let reader = index.reader()?;
let searcher = reader.searcher();

// Example 1: Basic filter with metric aggregation
println!("=== Example 1: Electronics average price ===");
let agg_req = json!({
"electronics": {
"filter": "category:electronics",
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
});

let agg: Aggregations = serde_json::from_value(agg_req)?;
let collector = AggregationCollector::from_aggs(agg, Default::default());
let result = searcher.search(&AllQuery, &collector)?;
println!("{}\n", serde_json::to_string_pretty(&result)?);

// Example 2: Multiple independent filters
println!("=== Example 2: Multiple filters in one query ===");
let agg_req = json!({
"electronics": {
"filter": "category:electronics",
"aggs": { "avg_price": { "avg": { "field": "price" } } }
},
"in_stock": {
"filter": "in_stock:true",
"aggs": { "count": { "value_count": { "field": "brand" } } }
},
"high_rated": {
"filter": "rating:[4.5 TO *]",
"aggs": { "count": { "value_count": { "field": "brand" } } }
}
});

let agg: Aggregations = serde_json::from_value(agg_req)?;
let collector = AggregationCollector::from_aggs(agg, Default::default());
let result = searcher.search(&AllQuery, &collector)?;
println!("{}\n", serde_json::to_string_pretty(&result)?);
Comment thread
stuhood marked this conversation as resolved.

// Example 3: Nested filters
println!("=== Example 3: Nested filters ===");
let agg_req = json!({
"all_products": {
"filter": "*",
"aggs": {
"electronics": {
"filter": "category:electronics",
"aggs": {
"expensive": {
"filter": "price:[800 TO *]",
"aggs": {
"avg_rating": { "avg": { "field": "rating" } }
}
}
}
}
}
}
});

let agg: Aggregations = serde_json::from_value(agg_req)?;
let collector = AggregationCollector::from_aggs(agg, Default::default());
let result = searcher.search(&AllQuery, &collector)?;
println!("{}\n", serde_json::to_string_pretty(&result)?);

// Example 4: Filter with sub-aggregation (terms)
println!("=== Example 4: Filter with terms sub-aggregation ===");
let agg_req = json!({
"electronics": {
"filter": "category:electronics",
"aggs": {
"by_brand": {
"terms": { "field": "brand" },
"aggs": {
"avg_price": { "avg": { "field": "price" } }
}
}
}
}
});

let agg: Aggregations = serde_json::from_value(agg_req)?;
let collector = AggregationCollector::from_aggs(agg, Default::default());
let result = searcher.search(&AllQuery, &collector)?;
println!("{}", serde_json::to_string_pretty(&result)?);

Ok(())
}
7 changes: 6 additions & 1 deletion src/aggregation/agg_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ use std::collections::{HashMap, HashSet};
use serde::{Deserialize, Serialize};

use super::bucket::{
DateHistogramAggregationReq, HistogramAggregation, RangeAggregation, TermsAggregation,
DateHistogramAggregationReq, FilterAggregation, HistogramAggregation, RangeAggregation,
TermsAggregation,
};
use super::metric::{
AverageAggregation, CardinalityAggregationReq, CountAggregation, ExtendedStatsAggregation,
Expand Down Expand Up @@ -129,6 +130,9 @@ pub enum AggregationVariants {
/// Put data into buckets of terms.
#[serde(rename = "terms")]
Terms(TermsAggregation),
/// Filter documents into a single bucket.
#[serde(rename = "filter")]
Filter(FilterAggregation),

// Metric aggregation types
/// Computes the average of the extracted values.
Expand Down Expand Up @@ -174,6 +178,7 @@ impl AggregationVariants {
AggregationVariants::Range(range) => vec![range.field.as_str()],
AggregationVariants::Histogram(histogram) => vec![histogram.field.as_str()],
AggregationVariants::DateHistogram(histogram) => vec![histogram.field.as_str()],
AggregationVariants::Filter(filter) => filter.get_fast_field_names(),
AggregationVariants::Average(avg) => vec![avg.field_name()],
AggregationVariants::Count(count) => vec![count.field_name()],
AggregationVariants::Max(max) => vec![max.field_name()],
Expand Down
11 changes: 11 additions & 0 deletions src/aggregation/agg_req_with_accessor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,17 @@ impl AggregationWithAccessor {

add_agg_with_accessors(&agg, accessors, &mut res, value_accessors)?;
}
Filter(ref _filter_req) => {
// Filter aggregations don't need a specific field accessor like other aggregations
// (terms, histogram, stats) because they:
// 1. Access fast fields dynamically during query evaluation (fast path
// optimization)
// 2. Sub-aggregations get their own real accessors through the normal framework
// We provide a dummy accessor to satisfy the framework's accessor requirement
use columnar::Column;
let dummy_accessor = Column::build_empty_column(reader.num_docs());
add_agg_with_accessor(&agg, dummy_accessor, ColumnType::U64, &mut res)?;
}
};

Ok(res)
Expand Down
28 changes: 28 additions & 0 deletions src/aggregation/agg_result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ pub enum BucketResult {
/// The upper bound error for the doc count of each term.
doc_count_error_upper_bound: Option<u64>,
},
/// This is the filter result - a single bucket with sub-aggregations
Filter(FilterBucketResult),
}

impl BucketResult {
Expand All @@ -172,6 +174,10 @@ impl BucketResult {
sum_other_doc_count: _,
doc_count_error_upper_bound: _,
} => buckets.iter().map(|bucket| bucket.get_bucket_count()).sum(),
BucketResult::Filter(filter_result) => {
// add one for the filter bucket itself
1 + filter_result.sub_aggregations.get_bucket_count()
}
}
}
}
Expand Down Expand Up @@ -308,3 +314,25 @@ impl RangeBucketEntry {
1 + self.sub_aggregation.get_bucket_count()
}
}

/// This is the filter bucket result, which contains the document count and sub-aggregations.
///
/// # JSON Format
/// ```json
/// {
/// "electronics_only": {
/// "doc_count": 2,
/// "avg_price": {
/// "value": 150.0
/// }
/// }
/// }
/// ```
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FilterBucketResult {
/// Number of documents in the filter bucket
pub doc_count: u64,
/// Sub-aggregation results
#[serde(flatten)]
pub sub_aggregations: AggregationResults,
}
Loading
Loading