11use std:: fmt:: Debug ;
22
3+ use common:: BitSet ;
34use serde:: { Deserialize , Deserializer , Serialize , Serializer } ;
45
56use crate :: aggregation:: agg_req_with_accessor:: AggregationsWithAccessor ;
@@ -10,10 +11,10 @@ use crate::aggregation::segment_agg_result::{
1011 build_segment_agg_collector_with_reader, CollectorClone , SegmentAggregationCollector ,
1112} ;
1213use crate :: docset:: { DocSet , COLLECT_BLOCK_BUFFER_LEN } ;
13- use crate :: query:: { Query , QueryParser , Scorer } ;
14+ use crate :: query:: { EnableScoring , Query , QueryParser } ;
1415use crate :: schema:: Schema ;
1516use crate :: tokenizer:: TokenizerManager ;
16- use crate :: { DocId , SegmentReader , TantivyError , TERMINATED } ;
17+ use crate :: { DocId , SegmentReader , TantivyError } ;
1718
1819/// Filter aggregation creates a single bucket containing documents that match a query.
1920///
@@ -143,9 +144,7 @@ impl FilterAggregation {
143144// Custom serialization implementation
144145impl Serialize for FilterAggregation {
145146 fn serialize < S > ( & self , serializer : S ) -> Result < S :: Ok , S :: Error >
146- where
147- S : Serializer ,
148- {
147+ where S : Serializer {
149148 match & self . query {
150149 FilterQuery :: QueryString ( query_string) => {
151150 // Serialize the query string directly
@@ -164,9 +163,7 @@ impl Serialize for FilterAggregation {
164163
165164impl < ' de > Deserialize < ' de > for FilterAggregation {
166165 fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
167- where
168- D : Deserializer < ' de > ,
169- {
166+ where D : Deserializer < ' de > {
170167 // Deserialize as query string
171168 let query_string = String :: deserialize ( deserializer) ?;
172169 Ok ( FilterAggregation :: new ( query_string) )
@@ -185,76 +182,65 @@ impl PartialEq for FilterAggregation {
185182 }
186183}
187184
188- /// Document evaluator for filter queries
189- /// This avoids running separate query executions and instead evaluates queries per document
185+ /// Document evaluator for filter queries using BitSet
190186struct DocumentQueryEvaluator {
191- /// The scorer for document matching
192- /// We create this once per segment and reuse it for all document checks.
193- /// This is critical for performance.
194- scorer : Box < dyn Scorer > ,
187+ /// BitSet containing all matching documents for this segment
188+ bitset : BitSet ,
195189}
196190
197191impl DocumentQueryEvaluator {
198192 /// Create and initialize a document query evaluator for a segment
193+ /// This executes the query upfront and collects results into a BitSet
199194 fn new (
200195 query : Box < dyn Query > ,
201196 schema : Schema ,
202197 segment_reader : & SegmentReader ,
203198 ) -> crate :: Result < Self > {
204- use crate :: query:: EnableScoring ;
199+ // Get the weight for the query
205200 let weight = query. weight ( EnableScoring :: disabled_from_schema ( & schema) ) ?;
206- let scorer = weight. scorer ( segment_reader, 1.0 ) ?;
207- Ok ( Self { scorer } )
208- }
209201
210- /// Evaluate if a document matches the filter query
211- /// This is the core performance-critical method
212- pub fn matches_document ( & mut self , doc : DocId ) -> crate :: Result < bool > {
213- let scorer = & mut self . scorer ;
202+ // Get a scorer that iterates over matching documents
203+ let mut scorer = weight. scorer ( segment_reader, 1.0 ) ?;
214204
215- // Use the same pattern as Weight::explain to handle seek ordering correctly
216- // The scorer maintains its position, so we can efficiently check if doc matches
217- Ok ( !( scorer. doc ( ) > doc || scorer. seek ( doc) != doc) )
218- }
205+ // Create a BitSet to hold all matching documents
206+ let max_doc = segment_reader. max_doc ( ) ;
207+ let mut bitset = BitSet :: with_max_value ( max_doc) ;
219208
220- /// Filter a batch of documents efficiently using intersection
221- /// Returns matching documents from the input batch
222- # [ inline ]
223- pub fn filter_batch ( & mut self , docs : & [ DocId ] , output : & mut Vec < DocId > ) -> crate :: Result < ( ) > {
224- if docs . is_empty ( ) {
225- return Ok ( ( ) ) ;
209+ // Collect all matching documents into the BitSet
210+ // This is the upfront cost, but then lookups are O(1)
211+ let mut doc = scorer . doc ( ) ;
212+ while doc != crate :: TERMINATED {
213+ bitset . insert ( doc ) ;
214+ doc = scorer . advance ( ) ;
226215 }
227216
228- let scorer = & mut self . scorer ;
217+ Ok ( Self { bitset } )
218+ }
229219
230- // Efficient intersection: advance scorer and check against input docs
231- let mut scorer_doc = scorer. doc ( ) ;
232- if scorer_doc == TERMINATED {
233- return Ok ( ( ) ) ;
234- }
220+ /// Evaluate if a document matches the filter query
221+ /// O(1) lookup in the precomputed BitSet
222+ #[ inline]
223+ pub fn matches_document ( & self , doc : DocId ) -> bool {
224+ self . bitset . contains ( doc)
225+ }
235226
227+ /// Filter a batch of documents
228+ /// Returns matching documents from the input batch
229+ #[ inline]
230+ pub fn filter_batch ( & self , docs : & [ DocId ] , output : & mut Vec < DocId > ) {
236231 for & doc in docs {
237- // Advance scorer to at least doc
238- if scorer_doc < doc {
239- scorer_doc = scorer. seek ( doc) ;
240- }
241-
242- // If scorer matches this doc, include it
243- if scorer_doc == doc {
232+ if self . bitset . contains ( doc) {
244233 output. push ( doc) ;
245- scorer_doc = scorer. advance ( ) ;
246- if scorer_doc == TERMINATED {
247- break ;
248- }
249234 }
250235 }
251-
252- Ok ( ( ) )
253236 }
254237}
238+
255239impl Debug for DocumentQueryEvaluator {
256240 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
257- f. debug_struct ( "DocumentQueryEvaluator" ) . finish ( )
241+ f. debug_struct ( "DocumentQueryEvaluator" )
242+ . field ( "num_matches" , & self . bitset . len ( ) )
243+ . finish ( )
258244 }
259245}
260246
@@ -372,8 +358,8 @@ impl SegmentAggregationCollector for FilterSegmentCollector {
372358 doc : DocId ,
373359 agg_with_accessor : & mut AggregationsWithAccessor ,
374360 ) -> crate :: Result < ( ) > {
375- // This is the core efficiency: evaluate filter on document already matched by main query
376- if self . evaluator . matches_document ( doc) ? {
361+ // O(1) BitSet lookup to check if document matches filter
362+ if self . evaluator . matches_document ( doc) {
377363 self . doc_count += 1 ;
378364
379365 // If we have sub-aggregations, collect on them for this filtered document
@@ -395,10 +381,10 @@ impl SegmentAggregationCollector for FilterSegmentCollector {
395381 return Ok ( ( ) ) ;
396382 }
397383
398- // Use batch filtering for better performance
384+ // Use batch filtering with O(1) BitSet lookups
399385 self . matching_docs_buffer . clear ( ) ;
400386 self . evaluator
401- . filter_batch ( docs, & mut self . matching_docs_buffer ) ? ;
387+ . filter_batch ( docs, & mut self . matching_docs_buffer ) ;
402388
403389 self . doc_count += self . matching_docs_buffer . len ( ) as u64 ;
404390
0 commit comments