Skip to content

Commit a432e67

Browse files
committed
TermSet fast fields (#69, #70, #75)
* perf: Implement a TermSet variant which uses fast fields (#69) * fix: Add support for `bool` to the fast field `TermSet` implementation (#70) * perf: Optimize `TermSet` for very large sets of terms. (#75)
1 parent 8250cad commit a432e67

5 files changed

Lines changed: 591 additions & 22 deletions

File tree

src/query/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ mod range_query;
2222
mod regex_query;
2323
mod reqopt_scorer;
2424
mod scorer;
25-
mod set_query;
2625
mod size_hint;
2726
mod term_query;
27+
mod term_set_query;
2828
mod union;
2929
mod weight;
3030

@@ -64,8 +64,8 @@ pub use self::regex_query::RegexQuery;
6464
pub use self::reqopt_scorer::RequiredOptionalScorer;
6565
pub use self::score_combiner::{DisjunctionMaxCombiner, ScoreCombiner, SumCombiner};
6666
pub use self::scorer::Scorer;
67-
pub use self::set_query::TermSetQuery;
6867
pub use self::term_query::TermQuery;
68+
pub use self::term_set_query::*;
6969
pub use self::union::{BufferedUnionScorer, SimpleUnion};
7070
#[cfg(test)]
7171
pub use self::vec_docset::VecDocSet;

src/query/term_set_query/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod term_set_query;
2+
mod term_set_query_fastfield;
3+
4+
pub use self::term_set_query::{InvertedIndexTermSetQuery, TermSetQuery};
5+
pub use self::term_set_query_fastfield::FastFieldTermSetQuery;

src/query/set_query.rs renamed to src/query/term_set_query/term_set_query.rs

Lines changed: 105 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ use std::collections::HashMap;
33
use tantivy_fst::raw::CompiledAddr;
44
use tantivy_fst::{Automaton, Map};
55

6+
use super::term_set_query_fastfield::FastFieldTermSetWeight;
67
use crate::query::score_combiner::DoNothingCombiner;
78
use crate::query::{AutomatonWeight, BooleanWeight, EnableScoring, Occur, Query, Weight};
8-
use crate::schema::{Field, Schema};
9+
use crate::schema::{Field, Schema, Type};
910
use crate::{SegmentReader, Term};
1011

12+
/// The term set query will use the fast field implementation if the number of terms is larger than
13+
/// this threshold.
14+
const TERM_SET_FAST_FIELD_CARDINALITY_THRESHOLD: usize = 1024;
15+
1116
/// A Term Set Query matches all of the documents containing any of the Term provided
1217
#[derive(Debug, Clone)]
1318
pub struct TermSetQuery {
@@ -22,11 +27,6 @@ impl TermSetQuery {
2227
terms_map.entry(term.field()).or_default().push(term);
2328
}
2429

25-
for terms in terms_map.values_mut() {
26-
terms.sort_unstable();
27-
terms.dedup();
28-
}
29-
3030
TermSetQuery { terms_map }
3131
}
3232

@@ -36,28 +36,60 @@ impl TermSetQuery {
3636
) -> crate::Result<BooleanWeight<DoNothingCombiner>> {
3737
let mut sub_queries: Vec<(_, Box<dyn Weight>)> = Vec::with_capacity(self.terms_map.len());
3838

39-
for (&field, sorted_terms) in self.terms_map.iter() {
39+
for (&field, terms) in self.terms_map.iter() {
4040
let field_entry = schema.get_field_entry(field);
4141
let field_type = field_entry.field_type();
4242
if !field_type.is_indexed() {
4343
let error_msg = format!("Field {:?} is not indexed.", field_entry.name());
4444
return Err(crate::TantivyError::SchemaError(error_msg));
4545
}
4646

47-
// In practice this won't fail because:
48-
// - we are writing to memory, so no IoError
49-
// - Terms are ordered
50-
let map = Map::from_iter(
51-
sorted_terms
47+
let supported_for_ff = match field_type.value_type() {
48+
Type::U64 | Type::I64 | Type::F64 | Type::Date | Type::IpAddr => {
49+
// NOTE: Keep in sync with `FastFieldTermSetWeight::scorer`.
50+
true
51+
}
52+
Type::Bool => {
53+
// Guaranteed to be low cardinality, so always more efficient to use posting
54+
// lists.
55+
false
56+
}
57+
Type::Json | Type::Str => {
58+
// Explicitly not supported yet: see `term_set_query_fastfield.rs`.
59+
false
60+
}
61+
_ => false,
62+
};
63+
64+
// NOTE: At this point, the terms have not been deduped, and so this threshold may not
65+
// be perfectly accurate. But in the case of very large input sets, it's worth avoiding
66+
// sorting/deduping the terms until after we've determined their type.
67+
if field_type.is_fast()
68+
&& supported_for_ff
69+
&& terms.len() > TERM_SET_FAST_FIELD_CARDINALITY_THRESHOLD
70+
{
71+
sub_queries.push((
72+
Occur::Should,
73+
Box::new(FastFieldTermSetWeight::new(field, terms.iter())?),
74+
));
75+
} else {
76+
let mut sorted_terms: Vec<(&[u8], u64)> = terms
5277
.iter()
53-
.map(|key| (key.serialized_value_bytes(), 0)),
54-
)
55-
.map_err(std::io::Error::other)?;
56-
57-
sub_queries.push((
58-
Occur::Should,
59-
Box::new(AutomatonWeight::new(field, SetDfaWrapper(map))),
60-
));
78+
.map(|key| (key.serialized_value_bytes(), 0))
79+
.collect::<Vec<_>>();
80+
sorted_terms.sort_unstable();
81+
sorted_terms.dedup();
82+
// In practice this won't fail because:
83+
// - we are writing to memory, so no IoError
84+
// - `sorted_terms` are ordered
85+
let map = Map::from_iter(sorted_terms.into_iter())
86+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
87+
88+
sub_queries.push((
89+
Occur::Should,
90+
Box::new(AutomatonWeight::new(field, SetDfaWrapper(map))),
91+
));
92+
}
6193
}
6294

6395
Ok(BooleanWeight::new(
@@ -87,6 +119,59 @@ impl Query for TermSetQuery {
87119
}
88120
}
89121

122+
/// `InvertedIndexTermSetQuery` is the same as [TermSetQuery] but only uses the inverted index.
123+
#[derive(Debug, Clone)]
124+
pub struct InvertedIndexTermSetQuery {
125+
terms_map: HashMap<Field, Vec<Term>>,
126+
}
127+
128+
impl InvertedIndexTermSetQuery {
129+
/// Create a new `InvertedIndexTermSetQuery`.
130+
pub fn new<T: IntoIterator<Item = Term>>(terms: T) -> Self {
131+
let mut terms_map: HashMap<_, Vec<_>> = HashMap::new();
132+
for term in terms {
133+
terms_map.entry(term.field()).or_default().push(term);
134+
}
135+
136+
for terms in terms_map.values_mut() {
137+
terms.sort_unstable();
138+
terms.dedup();
139+
}
140+
141+
InvertedIndexTermSetQuery { terms_map }
142+
}
143+
}
144+
145+
impl Query for InvertedIndexTermSetQuery {
146+
fn weight(&self, enable_scoring: EnableScoring<'_>) -> crate::Result<Box<dyn Weight>> {
147+
let mut sub_queries: Vec<(_, Box<dyn Weight>)> = Vec::with_capacity(self.terms_map.len());
148+
for (&field, sorted_terms) in &self.terms_map {
149+
let schema = enable_scoring.schema();
150+
let field_entry = schema.get_field_entry(field);
151+
if !field_entry.field_type().is_indexed() {
152+
let error_msg = format!("Field {:?} is not indexed.", field_entry.name());
153+
return Err(crate::TantivyError::SchemaError(error_msg));
154+
}
155+
let map = Map::from_iter(
156+
sorted_terms
157+
.iter()
158+
.map(|key| (key.serialized_value_bytes(), 0)),
159+
)
160+
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
161+
162+
sub_queries.push((
163+
Occur::Should,
164+
Box::new(AutomatonWeight::new(field, SetDfaWrapper(map))),
165+
));
166+
}
167+
Ok(Box::new(BooleanWeight::new(
168+
sub_queries,
169+
false,
170+
Box::new(DoNothingCombiner::default),
171+
)))
172+
}
173+
}
174+
90175
struct SetDfaWrapper(Map<Vec<u8>>);
91176

92177
impl Automaton for SetDfaWrapper {

0 commit comments

Comments
 (0)