1#[derive(Clone, Debug, PartialEq)]
14pub enum QueryExpression {
15 Scoring(ScoringExpression),
17 Ranking(RankingExpression),
19}
20
21#[derive(Clone, Debug, PartialEq)]
25pub enum RankingExpression {
26 Fusion {
32 sources: Vec<QueryExpression>,
33 method: FusionMethod,
34 rank_constant: f32,
36 rank_window_size: Option<usize>,
39 weights: Option<Vec<f32>>,
42 },
43}
44
45#[derive(Clone, Debug, PartialEq)]
49pub enum FusionMethod {
50 ReciprocalRank,
53 Sum,
56 ArithmeticMean,
58 HarmonicMean,
60 GeometricMean,
62}
63
64impl QueryExpression {
65 pub fn as_scoring(&self) -> Option<&ScoringExpression> {
67 match self {
68 QueryExpression::Scoring(s) => Some(s),
69 QueryExpression::Ranking(_) => None,
70 }
71 }
72
73 pub fn scoring_expressions(&self) -> Vec<&ScoringExpression> {
77 match self {
78 QueryExpression::Scoring(s) => vec![s],
79 QueryExpression::Ranking(RankingExpression::Fusion { sources, .. }) => sources
80 .iter()
81 .flat_map(|s| s.scoring_expressions())
82 .collect(),
83 }
84 }
85}
86
87#[derive(Clone, Debug, PartialEq)]
90pub struct InnerHitsConfig {
91 pub name: Option<String>,
92 pub size: usize,
93 pub from: usize,
94}
95
96#[derive(Clone, Debug, PartialEq)]
98pub enum ScoringExpression {
99 Term { field: String, value: String },
101
102 Terms { field: String, values: Vec<String> },
104
105 Match {
107 field: String,
108 query: String,
109 analyzer: Option<String>,
110 },
111
112 MatchPhrase {
114 field: String,
115 query: String,
116 analyzer: Option<String>,
117 },
118
119 MatchBoolPrefix {
122 field: String,
123 query: String,
124 analyzer: Option<String>,
125 },
126
127 Bool {
136 must: Vec<ScoringExpression>,
137 should: Vec<ScoringExpression>,
138 must_not: Vec<ScoringExpression>,
139 filter: Vec<ScoringExpression>,
140 minimum_should_match: Option<u32>,
141 },
142
143 DisMax {
146 queries: Vec<ScoringExpression>,
147 tie_breaker: f32,
148 },
149
150 Exists { field: String },
152
153 Prefix { field: String, value: String },
155
156 ConstantScore {
158 query: Box<ScoringExpression>,
159 boost: f32,
160 },
161
162 Nested {
164 path: String,
165 query: Box<ScoringExpression>,
166 inner_hits: Option<InnerHitsConfig>,
167 },
168
169 GeoDistance {
171 field: String,
172 lat: f64,
173 lon: f64,
174 distance: String, },
176
177 GeoBoundingBox {
179 field: String,
180 top_left_lat: f64,
181 top_left_lon: f64,
182 bottom_right_lat: f64,
183 bottom_right_lon: f64,
184 },
185
186 GeoShape {
190 field: String,
191 shape: GeoShapeValue,
192 relation: SpatialRelation,
193 },
194
195 Range {
197 field: String,
198 gte: Option<f64>,
199 gt: Option<f64>,
200 lte: Option<f64>,
201 lt: Option<f64>,
202 },
203
204 Boost {
206 query: Box<ScoringExpression>,
207 boost: f32,
208 },
209
210 ScriptScore {
212 query: Box<ScoringExpression>,
213 script: String,
214 params: std::collections::HashMap<String, f64>,
215 },
216
217 FunctionScore {
219 query: Box<ScoringExpression>,
220 functions: Vec<ScoreFunction>,
221 score_mode: FunctionScoreMode,
222 boost_mode: FunctionBoostMode,
223 },
224
225 Boosting {
227 positive: Box<ScoringExpression>,
228 negative: Box<ScoringExpression>,
229 negative_boost: f32,
230 },
231
232 Fuzzy {
234 field: String,
235 value: String,
236 fuzziness: u32,
237 },
238
239 Regexp { field: String, value: String },
241
242 Wildcard { field: String, value: String },
244
245 MultiMatch {
248 fields: Vec<String>,
249 query: String,
250 analyzer: Option<String>,
251 tie_breaker: f32,
252 },
253
254 Span(SpanExpression),
259
260 Knn {
268 field: String,
269 query_vector: Vec<f32>,
270 k: usize,
271 num_candidates: usize,
272 threshold: Option<f32>,
274 },
275
276 MatchAll,
278
279 MatchNone,
281}
282
283#[derive(Clone, Debug, PartialEq)]
294pub enum SpanExpression {
295 SpanTerm { field: String, value: String },
297 SpanNear {
299 field: String,
300 terms: Vec<String>,
301 slop: u32,
302 in_order: bool,
303 },
304 SpanNot {
306 include: Box<SpanExpression>,
307 exclude: Box<SpanExpression>,
308 },
309 SpanFirst {
311 query: Box<SpanExpression>,
312 end: u32,
313 },
314}
315
316#[derive(Clone, Debug, PartialEq)]
318pub enum ScoreFunction {
319 Weight(f32),
321 FieldValueFactor {
323 field: String,
324 factor: f32,
325 modifier: FieldValueModifier,
326 missing: f64,
327 },
328 RandomScore { seed: u64 },
330}
331
332#[derive(Clone, Debug, PartialEq)]
334pub enum FieldValueModifier {
335 None,
336 Log1p,
337 Log2p,
338 Ln1p,
339 Ln2p,
340 Sqrt,
341 Square,
342 Reciprocal,
343}
344
345#[derive(Clone, Debug, PartialEq)]
347pub enum FunctionScoreMode {
348 Multiply,
349 Sum,
350 Avg,
351 First,
352 Max,
353 Min,
354}
355
356#[derive(Clone, Debug, PartialEq)]
358pub enum FunctionBoostMode {
359 Multiply,
360 Replace,
361 Sum,
362 Avg,
363 Max,
364 Min,
365}
366
367#[derive(Clone, Debug, PartialEq)]
371pub enum SpatialRelation {
372 Intersects,
374 Within,
376 Contains,
378 Disjoint,
380 Touches,
382 Crosses,
384 Overlaps,
386 Equals,
388 Covers,
390 CoveredBy,
392 ContainsProperly,
394}
395
396#[derive(Clone, Debug, PartialEq)]
398pub struct GeoShapeValue {
399 pub json: serde_json::Value,
401}
402
403impl ScoringExpression {
404 pub fn bool_query(
406 must: Vec<ScoringExpression>,
407 should: Vec<ScoringExpression>,
408 must_not: Vec<ScoringExpression>,
409 filter: Vec<ScoringExpression>,
410 ) -> Self {
411 Self::Bool {
412 must,
413 should,
414 must_not,
415 filter,
416 minimum_should_match: None,
417 }
418 }
419}
420
421impl super::Query for ScoringExpression {
422 fn bind(
430 &self,
431 searcher: &crate::search::searcher::Searcher,
432 score_mode: crate::core::ScoreMode,
433 ) -> crate::core::Result<Box<dyn super::BoundQuery>> {
434 match self {
435 ScoringExpression::Term { field, value } => crate::query::term::TermQuery {
437 field: field.clone(),
438 value: value.clone(),
439 }
440 .bind(searcher, score_mode),
441
442 ScoringExpression::Terms { field, values } => {
443 let should: Vec<ScoringExpression> = values
445 .iter()
446 .map(|v| ScoringExpression::Term {
447 field: field.clone(),
448 value: v.clone(),
449 })
450 .collect();
451 ScoringExpression::Bool {
452 must: vec![],
453 should,
454 must_not: vec![],
455 filter: vec![],
456 minimum_should_match: None,
457 }
458 .bind(searcher, score_mode)
459 }
460
461 ScoringExpression::Match {
463 field,
464 query,
465 analyzer,
466 } => crate::query::match_query::MatchQuery {
467 field: field.clone(),
468 query_text: query.clone(),
469 analyzer: analyzer.clone(),
470 }
471 .bind(searcher, score_mode),
472
473 ScoringExpression::MatchPhrase {
474 field,
475 query,
476 analyzer,
477 } => crate::query::phrase::MatchPhraseQuery {
478 field: field.clone(),
479 query_text: query.clone(),
480 analyzer: analyzer.clone(),
481 }
482 .bind(searcher, score_mode),
483
484 ScoringExpression::MatchBoolPrefix {
485 field,
486 query,
487 analyzer,
488 } => {
489 let analyzer_name = searcher.resolve_search_analyzer(field, analyzer.as_deref());
495 let analyzers = searcher.analyzers();
496 let analyzer_impl = analyzers.get(analyzer_name);
497 let analyzed = analyzer_impl.analyze(query);
498 let tokens: Vec<String> = analyzed.into_iter().map(|t| t.text).collect();
499 if tokens.is_empty() {
500 crate::query::convert::MatchNoneQuery.bind(searcher, score_mode)
501 } else {
502 let mut should: Vec<ScoringExpression> = Vec::new();
503 for (i, token) in tokens.iter().enumerate() {
504 if i == tokens.len() - 1 {
505 should.push(ScoringExpression::Prefix {
506 field: field.clone(),
507 value: token.clone(),
508 });
509 } else {
510 should.push(ScoringExpression::Term {
511 field: field.clone(),
512 value: token.clone(),
513 });
514 }
515 }
516 ScoringExpression::Bool {
517 must: vec![],
518 should,
519 must_not: vec![],
520 filter: vec![],
521 minimum_should_match: None,
522 }
523 .bind(searcher, score_mode)
524 }
525 }
526
527 ScoringExpression::MultiMatch {
528 fields,
529 query,
530 analyzer,
531 tie_breaker,
532 } => {
533 let queries: Vec<ScoringExpression> = fields
538 .iter()
539 .map(|f| ScoringExpression::Match {
540 field: f.clone(),
541 query: query.clone(),
542 analyzer: analyzer.clone(),
543 })
544 .collect();
545 ScoringExpression::DisMax {
546 queries,
547 tie_breaker: *tie_breaker,
548 }
549 .bind(searcher, score_mode)
550 }
551
552 ScoringExpression::Bool {
554 must,
555 should,
556 must_not,
557 filter,
558 minimum_should_match,
559 } => crate::query::boolean::BoolQuery {
560 must: must
561 .iter()
562 .map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
563 .collect(),
564 should: should
565 .iter()
566 .map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
567 .collect(),
568 must_not: must_not
569 .iter()
570 .map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
571 .collect(),
572 filter: filter
573 .iter()
574 .map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
575 .collect(),
576 minimum_should_match: *minimum_should_match,
577 }
578 .bind(searcher, score_mode),
579
580 ScoringExpression::DisMax {
581 queries,
582 tie_breaker,
583 } => crate::query::dis_max::DisMaxQuery {
584 queries: queries
585 .iter()
586 .map(|q| -> Box<dyn super::Query> { Box::new(q.clone()) })
587 .collect(),
588 tie_breaker: *tie_breaker,
589 }
590 .bind(searcher, score_mode),
591
592 ScoringExpression::ConstantScore { query, boost } => {
593 crate::query::constant_score::ConstantScoreQuery {
594 inner: Box::new(query.as_ref().clone()),
595 boost: *boost,
596 }
597 .bind(searcher, score_mode)
598 }
599
600 ScoringExpression::Boost { query, boost } => crate::query::boost::BoostQuery {
601 inner: Box::new(query.as_ref().clone()),
602 boost: *boost,
603 }
604 .bind(searcher, score_mode),
605
606 ScoringExpression::Boosting {
607 positive,
608 negative,
609 negative_boost,
610 } => crate::query::boosting::BoostingQuery {
611 positive: Box::new(positive.as_ref().clone()),
612 negative: Box::new(negative.as_ref().clone()),
613 negative_boost: *negative_boost,
614 }
615 .bind(searcher, score_mode),
616
617 ScoringExpression::ScriptScore {
618 query,
619 script,
620 params,
621 } => crate::query::script_score::ScriptScoreQuery {
622 query: Box::new(query.as_ref().clone()),
623 script: script.clone(),
624 params: params.clone(),
625 }
626 .bind(searcher, score_mode),
627
628 ScoringExpression::FunctionScore {
629 query,
630 functions,
631 score_mode: fn_score_mode,
632 boost_mode,
633 } => crate::query::function_score::FunctionScoreQuery {
634 query: Box::new(query.as_ref().clone()),
635 functions: functions.clone(),
636 score_mode: fn_score_mode.clone(),
637 boost_mode: boost_mode.clone(),
638 }
639 .bind(searcher, score_mode),
640
641 ScoringExpression::Nested {
642 path,
643 query,
644 inner_hits,
645 } => crate::query::nested::NestedQuery {
646 path: path.clone(),
647 inner: Box::new(query.as_ref().clone()),
648 inner_hits: inner_hits.clone(),
649 }
650 .bind(searcher, score_mode),
651
652 ScoringExpression::Exists { field } => crate::query::exists::ExistsQuery {
654 field: field.clone(),
655 }
656 .bind(searcher, score_mode),
657
658 ScoringExpression::Prefix { field, value } => crate::query::prefix::PrefixQuery {
659 field: field.clone(),
660 value: value.clone(),
661 }
662 .bind(searcher, score_mode),
663
664 ScoringExpression::Range {
665 field,
666 gte,
667 gt,
668 lte,
669 lt,
670 } => crate::query::range::RangeQuery {
671 field: field.clone(),
672 gte: *gte,
673 gt: *gt,
674 lte: *lte,
675 lt: *lt,
676 }
677 .bind(searcher, score_mode),
678
679 ScoringExpression::Fuzzy {
681 field,
682 value,
683 fuzziness,
684 } => crate::query::fuzzy::FuzzyQuery {
685 field: field.clone(),
686 value: value.clone(),
687 fuzziness: *fuzziness,
688 }
689 .bind(searcher, score_mode),
690
691 ScoringExpression::Wildcard { field, value } => crate::query::wildcard::WildcardQuery {
692 field: field.clone(),
693 pattern: value.clone(),
694 }
695 .bind(searcher, score_mode),
696
697 ScoringExpression::Regexp { field, value } => crate::query::regexp::RegexpQuery {
698 field: field.clone(),
699 pattern: value.clone(),
700 }
701 .bind(searcher, score_mode),
702
703 ScoringExpression::GeoDistance {
705 field,
706 lat,
707 lon,
708 distance,
709 } => {
710 let distance_km = crate::query::parser::parse_distance_km(distance);
711 crate::spatial::query::GeoDistanceQuery {
712 field: field.clone(),
713 center: crate::spatial::geo::GeoPoint::new(*lat, *lon),
714 distance_km,
715 }
716 .bind(searcher, score_mode)
717 }
718
719 ScoringExpression::GeoBoundingBox {
720 field,
721 top_left_lat,
722 top_left_lon,
723 bottom_right_lat,
724 bottom_right_lon,
725 } => crate::spatial::query::GeoBoundingBoxQuery {
726 field: field.clone(),
727 top_left: crate::spatial::geo::GeoPoint::new(*top_left_lat, *top_left_lon),
728 bottom_right: crate::spatial::geo::GeoPoint::new(
729 *bottom_right_lat,
730 *bottom_right_lon,
731 ),
732 }
733 .bind(searcher, score_mode),
734
735 ScoringExpression::GeoShape {
736 field,
737 shape,
738 relation,
739 } => {
740 let query_geom = crate::spatial::shape::parse_geojson(&shape.json)
741 .unwrap_or(::geo::Geometry::Point(::geo::Point::new(0.0, 0.0)));
742 let query_bbox = crate::spatial::shape::compute_bbox(&query_geom)
743 .unwrap_or((0.0, 0.0, 0.0, 0.0));
744 crate::spatial::query::GeoShapeQuery {
745 field: field.clone(),
746 query_shape: query_geom,
747 query_bbox,
748 relation: relation.clone(),
749 }
750 .bind(searcher, score_mode)
751 }
752
753 ScoringExpression::Span(span_ast) => {
757 Ok(crate::query::convert::ast_to_span_query(span_ast)
758 .bind_span(searcher, score_mode)?)
759 }
760
761 ScoringExpression::Knn {
763 field,
764 query_vector,
765 k,
766 num_candidates,
767 threshold,
768 } => crate::vector::query::KnnQuery {
769 field: field.clone(),
770 query_vector: query_vector.clone(),
771 k: *k,
772 num_candidates: *num_candidates,
773 threshold: *threshold,
774 }
775 .bind(searcher, score_mode),
776
777 ScoringExpression::MatchAll => {
779 crate::query::convert::MatchAllQuery.bind(searcher, score_mode)
780 }
781
782 ScoringExpression::MatchNone => {
783 crate::query::convert::MatchNoneQuery.bind(searcher, score_mode)
784 }
785 }
786 }
787}
788
789#[cfg(test)]
790mod tests {
791 use super::*;
792
793 #[test]
794 fn term_query() {
795 let q = ScoringExpression::Term {
796 field: "status".into(),
797 value: "active".into(),
798 };
799 assert_eq!(
800 q,
801 ScoringExpression::Term {
802 field: "status".into(),
803 value: "active".into()
804 }
805 );
806 }
807
808 #[test]
809 fn bool_query_construction() {
810 let q = ScoringExpression::bool_query(
811 vec![ScoringExpression::Term {
812 field: "f".into(),
813 value: "v".into(),
814 }],
815 vec![],
816 vec![],
817 vec![],
818 );
819 if let ScoringExpression::Bool { must, .. } = &q {
820 assert_eq!(must.len(), 1);
821 } else {
822 panic!("expected Bool");
823 }
824 }
825
826 #[test]
827 fn nested_bool() {
828 let inner = ScoringExpression::bool_query(
829 vec![ScoringExpression::MatchAll],
830 vec![],
831 vec![],
832 vec![],
833 );
834 let outer = ScoringExpression::bool_query(vec![inner], vec![], vec![], vec![]);
835 if let ScoringExpression::Bool { must, .. } = &outer {
836 assert!(matches!(&must[0], ScoringExpression::Bool { .. }));
837 }
838 }
839
840 #[test]
841 fn constant_score_wraps() {
842 let q = ScoringExpression::ConstantScore {
843 query: Box::new(ScoringExpression::Term {
844 field: "f".into(),
845 value: "v".into(),
846 }),
847 boost: 1.5,
848 };
849 if let ScoringExpression::ConstantScore { boost, .. } = q {
850 assert_eq!(boost, 1.5);
851 }
852 }
853
854 #[test]
855 fn match_query() {
856 let q = ScoringExpression::Match {
857 field: "body".into(),
858 query: "search engine".into(),
859 analyzer: None,
860 };
861 if let ScoringExpression::Match { field, query, .. } = &q {
862 assert_eq!(field, "body");
863 assert_eq!(query, "search engine");
864 }
865 }
866
867 #[test]
868 fn debug_format() {
869 let q = ScoringExpression::MatchAll;
870 let s = format!("{q:?}");
871 assert!(s.contains("MatchAll"));
872 }
873
874 #[test]
875 fn clone_works() {
876 let q = ScoringExpression::Term {
877 field: "f".into(),
878 value: "v".into(),
879 };
880 let q2 = q.clone();
881 assert_eq!(q, q2);
882 }
883
884 #[test]
885 fn terms_query() {
886 let q = ScoringExpression::Terms {
887 field: "tags".into(),
888 values: vec!["a".into(), "b".into(), "c".into()],
889 };
890 if let ScoringExpression::Terms { values, .. } = &q {
891 assert_eq!(values.len(), 3);
892 }
893 }
894}