Skip to content

Commit 5faf5e1

Browse files
committed
Used BitSet
1 parent 5660916 commit 5faf5e1

1 file changed

Lines changed: 42 additions & 56 deletions

File tree

src/aggregation/bucket/filter.rs

Lines changed: 42 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::fmt::Debug;
22

3+
use common::BitSet;
34
use serde::{Deserialize, Deserializer, Serialize, Serializer};
45

56
use 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
};
1213
use crate::docset::{DocSet, COLLECT_BLOCK_BUFFER_LEN};
13-
use crate::query::{Query, QueryParser, Scorer};
14+
use crate::query::{EnableScoring, Query, QueryParser};
1415
use crate::schema::Schema;
1516
use 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
144145
impl 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

165164
impl<'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
190186
struct 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

197191
impl 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+
255239
impl 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

Comments
 (0)