summa_core/query/
prefix.rs1use crate::dsl::Field;
7use crate::segment::SegmentReader;
8
9#[cfg(test)]
10use super::term_union::materialize_union;
11use super::term_union::{TermUnionScorer, reject_chunked};
12use super::traits::{CountFuture, Query, Scorer, ScorerFuture};
13
14#[derive(Debug, Clone)]
16pub struct PrefixQuery {
17 pub field: Field,
18 pub prefix: Vec<u8>,
19}
20
21impl std::fmt::Display for PrefixQuery {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 write!(
24 f,
25 "Prefix({}:\"{}*\")",
26 self.field.0,
27 String::from_utf8_lossy(&self.prefix)
28 )
29 }
30}
31
32impl PrefixQuery {
33 pub fn new(field: Field, prefix: impl Into<Vec<u8>>) -> Self {
35 Self {
36 field,
37 prefix: prefix.into(),
38 }
39 }
40
41 pub fn text(field: Field, text: &str) -> Self {
43 Self {
44 field,
45 prefix: text.to_lowercase().into_bytes(),
46 }
47 }
48}
49
50impl Query for PrefixQuery {
51 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
52 let field = self.field;
53 let prefix = self.prefix.clone();
54 Box::pin(async move {
55 reject_chunked(reader, field, "PrefixQuery")?;
56 let postings = reader.get_prefix_expansion(field, &prefix).await?;
57 Ok(Box::new(TermUnionScorer::from_expanded(
58 postings,
59 reader.num_docs(),
60 reader.chunk_map(field),
61 limit,
62 )) as Box<dyn Scorer>)
63 })
64 }
65
66 #[cfg(feature = "sync")]
67 fn scorer_sync<'a>(
68 &self,
69 reader: &'a SegmentReader,
70 limit: usize,
71 ) -> crate::Result<Box<dyn Scorer + 'a>> {
72 reject_chunked(reader, self.field, "PrefixQuery")?;
73 let postings = reader.get_prefix_expansion_sync(self.field, &self.prefix)?;
74 Ok(Box::new(TermUnionScorer::from_expanded(
75 postings,
76 reader.num_docs(),
77 reader.chunk_map(self.field),
78 limit,
79 )))
80 }
81
82 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
83 let field = self.field;
84 let prefix = self.prefix.clone();
85 Box::pin(async move {
86 let postings = reader.get_prefix_expansion(field, &prefix).await?;
87 Ok(postings
88 .iter()
89 .fold(0u32, |sum, posting| sum.saturating_add(posting.doc_count()))
90 .min(reader.num_docs()))
91 })
92 }
93
94 fn is_filter(&self) -> bool {
95 true
96 }
97
98 #[cfg(feature = "sync")]
99 fn as_doc_predicate<'a>(&self, reader: &'a SegmentReader) -> Option<super::DocPredicate<'a>> {
100 let bitset = self.as_doc_bitset(reader)?;
101 Some(Box::new(move |doc_id: crate::DocId| {
102 bitset.contains(doc_id)
103 }))
104 }
105
106 #[cfg(feature = "sync")]
107 fn as_doc_bitset(&self, reader: &SegmentReader) -> Option<super::DocBitset> {
108 if reader.is_chunked_field(self.field) {
109 return None;
110 }
111 let postings = reader
112 .get_prefix_postings_sync(self.field, &self.prefix)
113 .ok()?;
114 let mut bitset = super::DocBitset::new(reader.num_docs());
115 for posting in &postings {
116 let mut iter = posting.iterator();
117 loop {
118 let d = iter.doc();
119 if d == crate::structures::TERMINATED {
120 break;
121 }
122 bitset.set(reader.chunk_map(self.field).map_or(d, |map| map.doc_id(d)));
123 iter.advance();
124 }
125 }
126 Some(bitset)
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::query::DocSet;
134 use crate::structures::{BlockPostingList, TERMINATED};
135
136 #[test]
137 fn test_materialize_union_empty() {
138 let docs = materialize_union(&[], 0, None);
139 assert!(docs.is_empty());
140 }
141
142 #[test]
143 fn test_materialize_union_deduplicates() {
144 let mut left = crate::structures::PostingList::new();
145 left.push(1, 1);
146 left.push(5, 1);
147 left.push(9, 1);
148 let mut right = crate::structures::PostingList::new();
149 right.push(2, 1);
150 right.push(5, 1);
151 right.push(10, 1);
152 let postings = vec![
153 BlockPostingList::from_posting_list(&left).unwrap(),
154 BlockPostingList::from_posting_list(&right).unwrap(),
155 ];
156
157 assert_eq!(materialize_union(&postings, 11, None), vec![1, 2, 5, 9, 10]);
158 assert_eq!(
161 materialize_union(&postings[..1], 1_000_000_000, None),
162 vec![1, 5, 9]
163 );
164 }
165
166 #[test]
167 fn test_prefix_scorer_basic() {
168 let mut scorer = TermUnionScorer::new(vec![1, 5, 10, 20]);
169 assert_eq!(scorer.doc(), 1);
170 assert_eq!(scorer.score(), 1.0);
171 assert_eq!(scorer.advance(), 5);
172 assert_eq!(scorer.seek(10), 10);
173 assert_eq!(scorer.advance(), 20);
174 assert_eq!(scorer.advance(), TERMINATED);
175 }
176
177 #[test]
178 fn test_prefix_scorer_seek_past() {
179 let mut scorer = TermUnionScorer::new(vec![1, 5, 10, 20]);
180 assert_eq!(scorer.seek(7), 10);
181 assert_eq!(scorer.seek(100), TERMINATED);
182 }
183
184 #[test]
185 fn test_prefix_query_display() {
186 let q = PrefixQuery::text(Field(0), "abc");
187 assert_eq!(format!("{}", q), "Prefix(0:\"abc*\")");
188 }
189
190 #[test]
191 fn test_prefix_query_is_filter() {
192 let q = PrefixQuery::text(Field(0), "test");
193 assert!(q.is_filter());
194 }
195}