1use crate::dsl::Field;
4use crate::query::{MatchedPositions, ScoredPosition};
5use crate::segment::SegmentReader;
6use crate::{DocId, Score, TERMINATED};
7
8use super::combiner::MultiValueCombiner;
9use crate::query::traits::{CountFuture, Query, Scorer, ScorerFuture};
10
11const DEFAULT_SPARSE_OVER_FETCH_FACTOR: f32 = crate::query::MAX_CANDIDATE_OVERSUBSCRIPTION as f32;
12
13enum SparseQueryInfos {
14 Local(Vec<crate::query::SparseTermQueryInfo>),
15 Shared(std::sync::Arc<[crate::query::SparseTermQueryInfo]>),
16}
17
18impl SparseQueryInfos {
19 fn as_slice(&self) -> &[crate::query::SparseTermQueryInfo] {
20 match self {
21 Self::Local(infos) => infos,
22 Self::Shared(infos) => infos,
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub struct SparseVectorQuery {
30 pub field: Field,
32 pub vector: Vec<(u32, f32)>,
34 pub combiner: MultiValueCombiner,
36 pub heap_factor: f32,
37 pub over_fetch_factor: f32,
38 pub lsp_gamma: Option<usize>,
39 pub weight_threshold: f32,
43 pub max_query_dims: Option<usize>,
47 pub pruning: Option<f32>,
52 pub min_query_dims: usize,
56 pub seismic_cut: usize,
58 pub seismic_factor: f32,
60 pub exhaustive: bool,
62 pruned: Option<Vec<(u32, f32)>>,
64}
65
66impl std::fmt::Display for SparseVectorQuery {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 let dims = self.pruned_dims();
69 write!(f, "Sparse({}, dims={}", self.field.0, dims.len())?;
70 if self.vector.len() != dims.len() {
71 write!(f, ", orig={}", self.vector.len())?;
72 }
73 write!(f, ")")
74 }
75}
76
77impl SparseVectorQuery {
78 pub fn new(field: Field, vector: Vec<(u32, f32)>) -> Self {
85 let defaults = crate::structures::SparseQueryConfig::default();
86 let mut q = Self {
87 field,
88 vector,
89 combiner: MultiValueCombiner::default(),
90 heap_factor: 1.0,
91 over_fetch_factor: DEFAULT_SPARSE_OVER_FETCH_FACTOR,
92 lsp_gamma: None,
93 weight_threshold: 0.0,
94 max_query_dims: Some(crate::query::MAX_QUERY_TERMS),
95 pruning: None,
96 min_query_dims: 4,
97 seismic_cut: defaults.seismic_cut,
98 seismic_factor: defaults.seismic_factor,
99 exhaustive: defaults.exhaustive,
100 pruned: None,
101 };
102 q.pruned = q.compute_pruned_vector();
103 q
104 }
105
106 pub(crate) fn pruned_dims(&self) -> &[(u32, f32)] {
108 self.pruned.as_deref().unwrap_or(&self.vector)
109 }
110
111 fn validate(&self, reader: &SegmentReader) -> crate::Result<()> {
112 let entry = reader
113 .schema()
114 .get_field_entry(self.field)
115 .ok_or_else(|| crate::Error::FieldNotFound(self.field.0.to_string()))?;
116 if entry.field_type != crate::dsl::FieldType::SparseVector {
117 return Err(crate::Error::InvalidFieldType {
118 expected: "sparse_vector".to_string(),
119 got: format!("{:?}", entry.field_type),
120 });
121 }
122 if self.vector.iter().any(|(_, weight)| !weight.is_finite()) {
123 return Err(crate::Error::Query(
124 "sparse query contains a non-finite weight".to_string(),
125 ));
126 }
127 if self.pruned_dims().len() > crate::query::MAX_QUERY_TERMS {
128 return Err(crate::Error::Query(format!(
129 "sparse query contains more than {} effective dimensions",
130 crate::query::MAX_QUERY_TERMS
131 )));
132 }
133
134 if !self.heap_factor.is_finite() || !(0.0..=1.0).contains(&self.heap_factor) {
135 return Err(crate::Error::Query(format!(
136 "sparse heap_factor must be finite and in [0, 1], got {}",
137 self.heap_factor
138 )));
139 }
140 if !self.over_fetch_factor.is_finite()
141 || !(1.0..=DEFAULT_SPARSE_OVER_FETCH_FACTOR).contains(&self.over_fetch_factor)
142 {
143 return Err(crate::Error::Query(format!(
144 "sparse over_fetch_factor must be finite and in [1, {DEFAULT_SPARSE_OVER_FETCH_FACTOR}], got {}",
145 self.over_fetch_factor
146 )));
147 }
148 crate::query::seismic::validate_options(self.seismic_cut, self.seismic_factor)?;
149 self.combiner.validate().map_err(crate::Error::Query)
150 }
151
152 pub fn with_heap_factor(mut self, heap_factor: f32) -> Self {
154 self.heap_factor = heap_factor.clamp(0.0, 1.0);
155 self
156 }
157
158 pub fn with_over_fetch_factor(mut self, factor: f32) -> Self {
159 self.over_fetch_factor = factor.clamp(1.0, DEFAULT_SPARSE_OVER_FETCH_FACTOR);
160 self
161 }
162
163 pub fn with_lsp_gamma(mut self, gamma: usize) -> Self {
164 self.lsp_gamma = Some(gamma);
165 self
166 }
167
168 pub fn with_seismic_cut(mut self, cut: usize) -> Self {
169 self.seismic_cut = cut;
170 self
171 }
172 pub fn with_seismic_factor(mut self, factor: f32) -> Self {
173 self.seismic_factor = factor;
174 self
175 }
176 pub fn with_exhaustive(mut self, exhaustive: bool) -> Self {
177 self.exhaustive = exhaustive;
178 self
179 }
180
181 pub fn with_combiner(mut self, combiner: MultiValueCombiner) -> Self {
183 self.combiner = combiner;
184 self
185 }
186
187 pub fn with_weight_threshold(mut self, threshold: f32) -> Self {
190 self.weight_threshold = threshold;
191 self.pruned = self.compute_pruned_vector();
192 self
193 }
194
195 pub fn with_max_query_dims(mut self, max_dims: usize) -> Self {
197 self.max_query_dims = Some(max_dims.min(crate::query::MAX_QUERY_TERMS));
200 self.pruned = self.compute_pruned_vector();
201 self
202 }
203
204 pub fn with_pruning(mut self, fraction: f32) -> Self {
207 self.pruning = Some(fraction.clamp(0.0, 1.0));
208 self.pruned = self.compute_pruned_vector();
209 self
210 }
211
212 pub fn with_min_query_dims(mut self, min_dims: usize) -> Self {
215 self.min_query_dims = min_dims;
216 self.pruned = self.compute_pruned_vector();
217 self
218 }
219
220 fn compute_pruned_vector(&self) -> Option<Vec<(u32, f32)>> {
224 let original_len = self.vector.len();
225 let max_dims = self
226 .max_query_dims
227 .unwrap_or(crate::query::MAX_QUERY_TERMS)
228 .min(crate::query::MAX_QUERY_TERMS);
229 let filtering_enabled = self.weight_threshold > 0.0 && original_len > self.min_query_dims;
230 let pruning_enabled = self
231 .pruning
232 .is_some_and(|fraction| fraction < 1.0 && original_len > self.min_query_dims);
233 if !filtering_enabled && !pruning_enabled && original_len <= max_dims {
234 return None;
235 }
236
237 let mut v: Vec<(u32, f32)> = if filtering_enabled {
240 self.vector
241 .iter()
242 .copied()
243 .filter(|(_, w)| w.abs() >= self.weight_threshold)
244 .collect()
245 } else {
246 self.vector.clone()
247 };
248 let after_threshold = v.len();
249
250 let mut sorted_by_weight = false;
253 if let Some(fraction) = self.pruning
254 && fraction < 1.0
255 && v.len() > self.min_query_dims
256 {
257 v.sort_unstable_by(|a, b| b.1.abs().total_cmp(&a.1.abs()).then_with(|| a.0.cmp(&b.0)));
258 sorted_by_weight = true;
259 let keep = ((v.len() as f64 * fraction as f64).ceil() as usize).max(1);
260 v.truncate(keep);
261 }
262 let after_pruning = v.len();
263
264 if v.len() > max_dims {
268 if !sorted_by_weight {
269 v.sort_unstable_by(|a, b| {
270 b.1.abs().total_cmp(&a.1.abs()).then_with(|| a.0.cmp(&b.0))
271 });
272 }
273 v.truncate(max_dims);
274 }
275
276 if v.len() < original_len && log::log_enabled!(log::Level::Debug) {
277 let src: Vec<_> = self
278 .vector
279 .iter()
280 .map(|(d, w)| format!("({},{:.4})", d, w))
281 .collect();
282 let pruned_fmt: Vec<_> = v.iter().map(|(d, w)| format!("({},{:.4})", d, w)).collect();
283 log::debug!(
284 "[sparse query] field={}: pruned {}->{} dims \
285 (threshold: {}->{}, pruning: {}->{}, max_dims: {}->{}), \
286 source=[{}], pruned=[{}]",
287 self.field.0,
288 original_len,
289 v.len(),
290 original_len,
291 after_threshold,
292 after_threshold,
293 after_pruning,
294 after_pruning,
295 v.len(),
296 src.join(", "),
297 pruned_fmt.join(", "),
298 );
299 }
300
301 Some(v)
302 }
303
304 pub fn from_indices_weights(field: Field, indices: Vec<u32>, weights: Vec<f32>) -> Self {
306 let vector: Vec<(u32, f32)> = indices.into_iter().zip(weights).collect();
307 Self::new(field, vector)
308 }
309
310 #[cfg(feature = "native")]
322 pub fn from_text(
323 field: Field,
324 text: &str,
325 tokenizer_name: &str,
326 weighting: crate::structures::QueryWeighting,
327 sparse_index: Option<&crate::segment::SparseIndex>,
328 ) -> crate::Result<Self> {
329 use crate::structures::QueryWeighting;
330 use crate::tokenizer::tokenizer_cache;
331
332 let tokenizer = tokenizer_cache().get_or_load(tokenizer_name)?;
333 let token_ids = tokenizer.tokenize_unique(text)?;
334
335 let weights: Vec<f32> = match weighting {
336 QueryWeighting::One => vec![1.0f32; token_ids.len()],
337 QueryWeighting::Idf => {
338 if let Some(index) = sparse_index {
339 index.idf_weights(&token_ids)
340 } else {
341 vec![1.0f32; token_ids.len()]
342 }
343 }
344 QueryWeighting::IdfFile => {
345 use crate::tokenizer::idf_weights_cache;
346 if let Some(idf) = idf_weights_cache().get_or_load(tokenizer_name, None) {
347 token_ids.iter().map(|&id| idf.get(id)).collect()
348 } else {
349 vec![1.0f32; token_ids.len()]
350 }
351 }
352 };
353
354 let vector: Vec<(u32, f32)> = token_ids.into_iter().zip(weights).collect();
355 Ok(Self::new(field, vector))
356 }
357
358 #[cfg(feature = "native")]
370 pub fn from_text_with_stats(
371 field: Field,
372 text: &str,
373 tokenizer: &crate::tokenizer::HfTokenizer,
374 weighting: crate::structures::QueryWeighting,
375 global_stats: Option<&crate::query::GlobalStats>,
376 ) -> crate::Result<Self> {
377 use crate::structures::QueryWeighting;
378
379 let token_ids = tokenizer.tokenize_unique(text)?;
380
381 let weights: Vec<f32> = match weighting {
382 QueryWeighting::One => vec![1.0f32; token_ids.len()],
383 QueryWeighting::Idf => {
384 if let Some(stats) = global_stats {
385 stats
387 .sparse_idf_weights(field, &token_ids)
388 .into_iter()
389 .map(|w| w.max(0.0))
390 .collect()
391 } else {
392 vec![1.0f32; token_ids.len()]
393 }
394 }
395 QueryWeighting::IdfFile => {
396 vec![1.0f32; token_ids.len()]
399 }
400 };
401
402 let vector: Vec<(u32, f32)> = token_ids.into_iter().zip(weights).collect();
403 Ok(Self::new(field, vector))
404 }
405
406 #[cfg(feature = "native")]
418 pub fn from_text_with_tokenizer_bytes(
419 field: Field,
420 text: &str,
421 tokenizer_bytes: &[u8],
422 weighting: crate::structures::QueryWeighting,
423 global_stats: Option<&crate::query::GlobalStats>,
424 ) -> crate::Result<Self> {
425 use crate::structures::QueryWeighting;
426 use crate::tokenizer::HfTokenizer;
427
428 let tokenizer = HfTokenizer::from_bytes(tokenizer_bytes)?;
429 let token_ids = tokenizer.tokenize_unique(text)?;
430
431 let weights: Vec<f32> = match weighting {
432 QueryWeighting::One => vec![1.0f32; token_ids.len()],
433 QueryWeighting::Idf => {
434 if let Some(stats) = global_stats {
435 stats
437 .sparse_idf_weights(field, &token_ids)
438 .into_iter()
439 .map(|w| w.max(0.0))
440 .collect()
441 } else {
442 vec![1.0f32; token_ids.len()]
443 }
444 }
445 QueryWeighting::IdfFile => {
446 vec![1.0f32; token_ids.len()]
449 }
450 };
451
452 let vector: Vec<(u32, f32)> = token_ids.into_iter().zip(weights).collect();
453 Ok(Self::new(field, vector))
454 }
455}
456
457impl SparseVectorQuery {
458 fn sparse_infos_for_plan(
459 &self,
460 plan: Option<&std::sync::Arc<crate::query::bmp::LspSegmentPlan>>,
461 ) -> SparseQueryInfos {
462 match plan {
463 Some(plan) => SparseQueryInfos::Shared(std::sync::Arc::clone(&plan.infos)),
464 None => SparseQueryInfos::Local(self.sparse_infos()),
465 }
466 }
467
468 fn sparse_infos(&self) -> Vec<crate::query::SparseTermQueryInfo> {
472 let candidate_dims: Option<rustc_hash::FxHashSet<u32>> = self
473 .pruned
474 .as_ref()
475 .map(|dimensions| dimensions.iter().map(|&(dimension, _)| dimension).collect());
476 let make_info = |(dim_id, weight)| crate::query::SparseTermQueryInfo {
477 field: self.field,
478 dim_id,
479 weight,
480 candidate: candidate_dims
481 .as_ref()
482 .is_none_or(|dimensions| dimensions.contains(&dim_id)),
483 combiner: self.combiner,
484 heap_factor: if self.exhaustive {
485 1.0
486 } else {
487 self.heap_factor
488 },
489 over_fetch_factor: self.over_fetch_factor,
490 lsp_gamma: if self.exhaustive {
491 Some(0)
492 } else {
493 self.lsp_gamma
494 },
495 seismic_cut: self.seismic_cut,
496 seismic_factor: self.seismic_factor,
497 exhaustive: self.exhaustive,
498 };
499 if self.vector.len() <= crate::query::MAX_QUERY_TERMS {
500 return self.vector.iter().copied().map(make_info).collect();
501 }
502
503 let mut scoring_dims = self.vector.clone();
504 scoring_dims.sort_unstable_by(|left, right| {
505 right
506 .1
507 .abs()
508 .total_cmp(&left.1.abs())
509 .then_with(|| left.0.cmp(&right.0))
510 });
511 scoring_dims.truncate(crate::query::MAX_QUERY_TERMS);
512 scoring_dims.into_iter().map(make_info).collect()
513 }
514}
515
516impl Query for SparseVectorQuery {
517 fn as_doc_bitset_with_options(
518 &self,
519 reader: &SegmentReader,
520 options: &crate::query::ScorerOptions,
521 ) -> Option<crate::query::DocBitset> {
522 self.validate(reader).ok()?;
523 let index = reader.seismic_index(self.field)?;
524 let terms: Vec<_> = self
525 .sparse_infos()
526 .iter()
527 .map(|info| (info.dim_id, info.weight))
528 .collect();
529 crate::query::seismic::membership(index, reader.num_docs(), &terms, options)
530 }
531 fn candidate_query(&self) -> crate::Result<crate::query::CandidateQuery> {
532 Ok(crate::query::CandidateQuery::new(
533 self.field,
534 crate::query::candidate_scoring::ScoreComponent::Sparse(
535 self.sparse_infos()
536 .into_iter()
537 .map(|info| (info.dim_id, info.weight))
538 .collect(),
539 ),
540 )
541 .with_combiner(self.combiner))
542 }
543 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
544 self.scorer_with_options(reader, limit, crate::query::ScorerOptions::with_positions())
545 }
546
547 fn scorer_with_options<'a>(
548 &self,
549 reader: &'a SegmentReader,
550 limit: usize,
551 options: crate::query::ScorerOptions,
552 ) -> ScorerFuture<'a> {
553 let validation = self.validate(reader);
554 let infos = self.sparse_infos_for_plan(options.lsp_plan.as_ref());
555
556 Box::pin(async move {
557 validation?;
558 let infos = infos.as_slice();
559 if let Some(scorer) =
560 crate::query::planner::build_sparse_memory_scorer(infos, reader, limit, &options)?
561 {
562 return Ok(scorer);
563 }
564 if let Some((executor, info)) = crate::query::planner::build_sparse_maxscore_executor(
565 infos, reader, limit, None, &options,
566 ) {
567 let raw = executor.execute().await?;
568 return Ok(crate::query::planner::combine_sparse_results(
569 raw,
570 info.combiner,
571 info.field,
572 limit,
573 ));
574 }
575 Ok(Box::new(crate::query::EmptyScorer) as Box<dyn Scorer>)
576 })
577 }
578
579 #[cfg(feature = "sync")]
580 fn scorer_sync<'a>(
581 &self,
582 reader: &'a SegmentReader,
583 limit: usize,
584 ) -> crate::Result<Box<dyn Scorer + 'a>> {
585 self.scorer_sync_with_options(reader, limit, crate::query::ScorerOptions::with_positions())
586 }
587
588 #[cfg(feature = "sync")]
589 fn scorer_sync_with_options<'a>(
590 &self,
591 reader: &'a SegmentReader,
592 limit: usize,
593 options: crate::query::ScorerOptions,
594 ) -> crate::Result<Box<dyn Scorer + 'a>> {
595 self.validate(reader)?;
596 let infos = self.sparse_infos_for_plan(options.lsp_plan.as_ref());
597 let infos = infos.as_slice();
598 if let Some(scorer) =
599 crate::query::planner::build_sparse_memory_scorer(infos, reader, limit, &options)?
600 {
601 return Ok(scorer);
602 }
603 if let Some((executor, info)) = crate::query::planner::build_sparse_maxscore_executor(
604 infos, reader, limit, None, &options,
605 ) {
606 let raw = executor.execute_sync()?;
607 return Ok(crate::query::planner::combine_sparse_results(
608 raw,
609 info.combiner,
610 info.field,
611 limit,
612 ));
613 }
614 Ok(Box::new(crate::query::EmptyScorer) as Box<dyn Scorer + 'a>)
615 }
616
617 fn count_estimate<'a>(&self, _reader: &'a SegmentReader) -> CountFuture<'a> {
618 Box::pin(async move { Ok(u32::MAX) })
619 }
620
621 fn decompose(&self) -> crate::query::QueryDecomposition {
622 let infos = self.sparse_infos();
623 if infos.is_empty() {
624 crate::query::QueryDecomposition::Opaque
625 } else {
626 crate::query::QueryDecomposition::SparseTerms(infos)
627 }
628 }
629}
630
631#[derive(Debug, Clone)]
639pub struct SparseTermQuery {
640 pub field: Field,
641 pub dim_id: u32,
642 pub weight: f32,
643 pub combiner: MultiValueCombiner,
645 pub heap_factor: f32,
646 pub over_fetch_factor: f32,
647 pub lsp_gamma: Option<usize>,
648 pub seismic_cut: usize,
649 pub seismic_factor: f32,
650 pub exhaustive: bool,
651}
652
653impl std::fmt::Display for SparseTermQuery {
654 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655 write!(
656 f,
657 "SparseTerm({}, dim={}, w={:.3})",
658 self.field.0, self.dim_id, self.weight
659 )
660 }
661}
662
663impl SparseTermQuery {
664 pub fn new(field: Field, dim_id: u32, weight: f32) -> Self {
665 let defaults = crate::structures::SparseQueryConfig::default();
666 Self {
667 field,
668 dim_id,
669 weight,
670 combiner: MultiValueCombiner::default(),
671 heap_factor: 1.0,
672 over_fetch_factor: DEFAULT_SPARSE_OVER_FETCH_FACTOR,
673 lsp_gamma: None,
674 seismic_cut: defaults.seismic_cut,
675 seismic_factor: defaults.seismic_factor,
676 exhaustive: defaults.exhaustive,
677 }
678 }
679
680 pub fn with_heap_factor(mut self, heap_factor: f32) -> Self {
681 self.heap_factor = heap_factor.clamp(0.0, 1.0);
682 self
683 }
684
685 pub fn with_over_fetch_factor(mut self, factor: f32) -> Self {
686 self.over_fetch_factor = factor.clamp(1.0, DEFAULT_SPARSE_OVER_FETCH_FACTOR);
687 self
688 }
689
690 pub fn with_lsp_gamma(mut self, gamma: usize) -> Self {
691 self.lsp_gamma = Some(gamma);
692 self
693 }
694
695 pub fn with_seismic_cut(mut self, cut: usize) -> Self {
696 self.seismic_cut = cut;
697 self
698 }
699 pub fn with_seismic_factor(mut self, factor: f32) -> Self {
700 self.seismic_factor = factor;
701 self
702 }
703 pub fn with_exhaustive(mut self, exhaustive: bool) -> Self {
704 self.exhaustive = exhaustive;
705 self
706 }
707
708 pub fn with_combiner(mut self, combiner: MultiValueCombiner) -> Self {
709 self.combiner = combiner;
710 self
711 }
712
713 fn validate(&self, reader: &SegmentReader) -> crate::Result<()> {
714 let entry = reader
715 .schema()
716 .get_field_entry(self.field)
717 .ok_or_else(|| crate::Error::FieldNotFound(self.field.0.to_string()))?;
718 if entry.field_type != crate::dsl::FieldType::SparseVector {
719 return Err(crate::Error::InvalidFieldType {
720 expected: "sparse_vector".to_string(),
721 got: format!("{:?}", entry.field_type),
722 });
723 }
724 if !self.weight.is_finite() {
725 return Err(crate::Error::Query(
726 "sparse term query weight must be finite".to_string(),
727 ));
728 }
729
730 if !self.heap_factor.is_finite() || !(0.0..=1.0).contains(&self.heap_factor) {
731 return Err(crate::Error::Query(format!(
732 "sparse heap_factor must be finite and in [0, 1], got {}",
733 self.heap_factor
734 )));
735 }
736 if !self.over_fetch_factor.is_finite()
737 || !(1.0..=DEFAULT_SPARSE_OVER_FETCH_FACTOR).contains(&self.over_fetch_factor)
738 {
739 return Err(crate::Error::Query(format!(
740 "sparse over_fetch_factor must be finite and in [1, {DEFAULT_SPARSE_OVER_FETCH_FACTOR}], got {}",
741 self.over_fetch_factor
742 )));
743 }
744 crate::query::seismic::validate_options(self.seismic_cut, self.seismic_factor)?;
745 self.combiner.validate().map_err(crate::Error::Query)
746 }
747
748 fn sparse_info(&self) -> crate::query::SparseTermQueryInfo {
749 crate::query::SparseTermQueryInfo {
750 field: self.field,
751 dim_id: self.dim_id,
752 weight: self.weight,
753 candidate: true,
754 combiner: self.combiner,
755 heap_factor: if self.exhaustive {
756 1.0
757 } else {
758 self.heap_factor
759 },
760 over_fetch_factor: self.over_fetch_factor,
761 lsp_gamma: if self.exhaustive {
762 Some(0)
763 } else {
764 self.lsp_gamma
765 },
766 seismic_cut: self.seismic_cut,
767 seismic_factor: self.seismic_factor,
768 exhaustive: self.exhaustive,
769 }
770 }
771
772 fn make_scorer<'a>(
774 &self,
775 reader: &'a SegmentReader,
776 limit: usize,
777 options: &crate::query::ScorerOptions,
778 ) -> crate::Result<Box<dyn Scorer + 'a>> {
779 let infos = [self.sparse_info()];
780 Ok(
781 crate::query::planner::build_sparse_memory_scorer(&infos, reader, limit, options)?
782 .unwrap_or_else(|| Box::new(crate::query::EmptyScorer)),
783 )
784 }
785
786 fn make_maxscore_scorer<'a>(
787 &self,
788 reader: &'a SegmentReader,
789 ) -> crate::Result<Option<SparseTermScorer<'a>>> {
790 let si = match reader.sparse_index(self.field) {
791 Some(si) => si,
792 None => return Ok(None),
793 };
794 let (skip_start, skip_count, global_max, block_data_offset) =
795 match si.get_skip_range_full(self.dim_id) {
796 Some(v) => v,
797 None => return Ok(None),
798 };
799 let cursor = crate::query::TermCursor::sparse(
800 si,
801 self.weight,
802 skip_start,
803 skip_count,
804 global_max,
805 block_data_offset,
806 );
807 Ok(Some(SparseTermScorer {
808 cursor,
809 field_id: self.field.0,
810 }))
811 }
812}
813
814impl Query for SparseTermQuery {
815 fn as_doc_bitset_with_options(
816 &self,
817 reader: &SegmentReader,
818 options: &crate::query::ScorerOptions,
819 ) -> Option<crate::query::DocBitset> {
820 self.validate(reader).ok()?;
821 let index = reader.seismic_index(self.field)?;
822 crate::query::seismic::membership(
823 index,
824 reader.num_docs(),
825 &[(self.dim_id, self.weight)],
826 options,
827 )
828 }
829 fn scorer<'a>(&self, reader: &'a SegmentReader, limit: usize) -> ScorerFuture<'a> {
830 self.scorer_with_options(reader, limit, crate::query::ScorerOptions::with_positions())
831 }
832
833 fn scorer_with_options<'a>(
834 &self,
835 reader: &'a SegmentReader,
836 limit: usize,
837 options: crate::query::ScorerOptions,
838 ) -> ScorerFuture<'a> {
839 let query = self.clone();
840 Box::pin(async move {
841 query.validate(reader)?;
842 if let Some(mut scorer) = query.make_maxscore_scorer(reader)? {
843 scorer.cursor.ensure_block_loaded().await?;
844 return Ok(Box::new(scorer) as Box<dyn Scorer + 'a>);
845 }
846 query.make_scorer(reader, limit, &options)
847 })
848 }
849
850 #[cfg(feature = "sync")]
851 fn scorer_sync<'a>(
852 &self,
853 reader: &'a SegmentReader,
854 limit: usize,
855 ) -> crate::Result<Box<dyn Scorer + 'a>> {
856 self.scorer_sync_with_options(reader, limit, crate::query::ScorerOptions::with_positions())
857 }
858
859 #[cfg(feature = "sync")]
860 fn scorer_sync_with_options<'a>(
861 &self,
862 reader: &'a SegmentReader,
863 limit: usize,
864 options: crate::query::ScorerOptions,
865 ) -> crate::Result<Box<dyn Scorer + 'a>> {
866 self.validate(reader)?;
867 if let Some(mut scorer) = self.make_maxscore_scorer(reader)? {
868 scorer.cursor.ensure_block_loaded_sync()?;
869 return Ok(Box::new(scorer) as Box<dyn Scorer + 'a>);
870 }
871 self.make_scorer(reader, limit, &options)
872 }
873
874 fn count_estimate<'a>(&self, reader: &'a SegmentReader) -> CountFuture<'a> {
875 let count = reader.seismic_index(self.field).map_or_else(
876 || {
877 reader.sparse_index(self.field).map_or_else(
878 || {
879 reader
880 .bmp_index(self.field)
881 .map_or(0, |_| reader.num_docs())
882 },
883 |index| index.doc_count(self.dim_id).min(reader.num_docs()),
884 )
885 },
886 |index| index.len().min(reader.num_docs()),
887 );
888 Box::pin(async move { Ok(count) })
889 }
890
891 fn decompose(&self) -> crate::query::QueryDecomposition {
892 crate::query::QueryDecomposition::SparseTerms(vec![self.sparse_info()])
893 }
894}
895
896struct SparseTermScorer<'a> {
901 cursor: crate::query::TermCursor<'a>,
902 field_id: u32,
903}
904
905impl crate::query::docset::DocSet for SparseTermScorer<'_> {
906 fn doc(&self) -> DocId {
907 let d = self.cursor.doc();
908 if d == u32::MAX { TERMINATED } else { d }
909 }
910
911 fn advance(&mut self) -> DocId {
912 match self.cursor.advance_sync() {
913 Ok(d) if d == u32::MAX => TERMINATED,
914 Ok(d) => d,
915 Err(_) => TERMINATED,
916 }
917 }
918
919 fn seek(&mut self, target: DocId) -> DocId {
920 match self.cursor.seek_sync(target) {
921 Ok(d) if d == u32::MAX => TERMINATED,
922 Ok(d) => d,
923 Err(_) => TERMINATED,
924 }
925 }
926
927 fn size_hint(&self) -> u32 {
928 0
929 }
930}
931
932impl Scorer for SparseTermScorer<'_> {
933 fn score(&self) -> Score {
934 self.cursor.score()
935 }
936
937 fn matched_positions(&self) -> Option<MatchedPositions> {
938 let ordinal = self.cursor.ordinal();
939 let score = self.cursor.score();
940 if score == 0.0 {
941 return None;
942 }
943 Some(vec![(
944 self.field_id,
945 vec![ScoredPosition::new(ordinal as u32, score)],
946 )])
947 }
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953 use crate::dsl::Field;
954
955 #[test]
956 fn programmatic_sparse_queries_share_schema_seismic_defaults() {
957 let defaults = crate::structures::SparseQueryConfig::default();
958 let omitted: crate::structures::SparseQueryConfig = serde_json::from_str("{}").unwrap();
959 assert_eq!(omitted, defaults);
960 let vector = SparseVectorQuery::new(Field(0), vec![(7, 0.5)]);
961 let term = SparseTermQuery::new(Field(0), 7, 0.5);
962 let expected = (
963 defaults.seismic_cut,
964 defaults.seismic_factor,
965 defaults.exhaustive,
966 );
967 assert_eq!(
968 (vector.seismic_cut, vector.seismic_factor, vector.exhaustive),
969 expected
970 );
971 assert_eq!(
972 (term.seismic_cut, term.seismic_factor, term.exhaustive),
973 expected
974 );
975 assert_eq!(vector.max_query_dims, Some(crate::query::MAX_QUERY_TERMS));
978 assert_eq!(defaults.max_query_dims, None);
979 for decomposition in [vector.decompose(), term.decompose()] {
980 let crate::query::QueryDecomposition::SparseTerms(infos) = decomposition else {
981 panic!("sparse queries must expose sparse scoring terms");
982 };
983 assert_eq!(infos.len(), 1);
984 assert_eq!(
985 (
986 infos[0].seismic_cut,
987 infos[0].seismic_factor,
988 infos[0].exhaustive
989 ),
990 expected
991 );
992 }
993 }
994
995 #[test]
996 fn test_sparse_vector_query_new() {
997 let sparse = vec![(1, 0.5), (5, 0.3), (10, 0.2)];
998 let query = SparseVectorQuery::new(Field(0), sparse.clone());
999
1000 assert_eq!(query.field, Field(0));
1001 assert_eq!(query.vector, sparse);
1002 assert!(
1003 query.pruned.is_none(),
1004 "the default path must alias the source vector instead of cloning it"
1005 );
1006 }
1007
1008 #[test]
1009 fn test_sparse_vector_query_from_indices_weights() {
1010 let query =
1011 SparseVectorQuery::from_indices_weights(Field(0), vec![1, 5, 10], vec![0.5, 0.3, 0.2]);
1012
1013 assert_eq!(query.vector, vec![(1, 0.5), (5, 0.3), (10, 0.2)]);
1014 }
1015
1016 #[test]
1017 fn max_query_dims_cannot_exceed_query_work_budget() {
1018 let vector: Vec<(u32, f32)> = (0..100).map(|dim| (dim, dim as f32 + 1.0)).collect();
1019 let query = SparseVectorQuery::new(Field(0), vector).with_max_query_dims(usize::MAX);
1020
1021 assert_eq!(query.pruned_dims().len(), crate::query::MAX_QUERY_TERMS);
1022 assert!(query.pruned_dims().iter().all(|(dim, _)| *dim >= 36));
1024 }
1025
1026 #[test]
1027 fn decomposition_keeps_full_scores_and_marks_pruned_candidates() {
1028 let query = SparseVectorQuery::new(Field(0), vec![(3, 1.0), (7, 0.8), (11, 0.2)])
1029 .with_min_query_dims(0)
1030 .with_pruning(0.34);
1031 let infos = query.sparse_infos();
1032
1033 assert_eq!(infos.len(), 3);
1034 assert_eq!(
1035 infos
1036 .iter()
1037 .filter(|info| info.candidate)
1038 .map(|info| info.dim_id)
1039 .collect::<Vec<_>>(),
1040 vec![3, 7]
1041 );
1042 assert_eq!(
1043 infos
1044 .iter()
1045 .map(|info| (info.dim_id, info.weight))
1046 .collect::<Vec<_>>(),
1047 vec![(3, 1.0), (7, 0.8), (11, 0.2)]
1048 );
1049 }
1050}