1use std::fmt;
34
35#[derive(Debug, Clone)]
37pub enum Predicate {
38 Eq(String, Value),
43 Ne(String, Value),
45 Gt(String, Value),
47 Gte(String, Value),
49 Lt(String, Value),
51 Lte(String, Value),
53 Between(String, Value, Value),
55
56 And(Vec<Predicate>),
61 Or(Vec<Predicate>),
63 Not(Box<Predicate>),
65
66 TextSearch(String, String),
71 Prefix(String, String),
73 Suffix(String, String),
75 Infix(String, String),
77 Wildcard(String, String),
79 WildcardExact(String, String),
81 Fuzzy(String, String, u8),
83 Phrase(String, Vec<String>),
85 PhraseWithOptions {
87 field: String,
88 words: Vec<String>,
89 slop: Option<u32>,
90 inorder: Option<bool>,
91 },
92 Optional(Box<Predicate>),
94
95 Tag(String, String),
100 TagOr(String, Vec<String>),
102
103 MultiFieldSearch(Vec<String>, String),
108
109 GeoRadius(String, f64, f64, f64, String),
114 GeoPolygon {
116 field: String,
117 points: Vec<(f64, f64)>,
119 },
120
121 IsMissing(String),
126 IsNotMissing(String),
128
129 Boost(Box<Predicate>, f64),
134
135 VectorKnn {
140 field: String,
141 k: usize,
142 vector_param: String,
144 pre_filter: Option<Box<Predicate>>,
146 },
147 VectorRange {
149 field: String,
150 radius: f64,
151 vector_param: String,
152 },
153
154 Raw(String),
159}
160
161#[derive(Debug, Clone)]
163pub enum Value {
164 Int(i64),
165 Float(f64),
166 String(String),
167 Bool(bool),
168}
169
170impl fmt::Display for Value {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 match self {
173 Value::Int(n) => write!(f, "{}", n),
174 Value::Float(n) => write!(f, "{}", n),
175 Value::String(s) => write!(f, "{}", s),
176 Value::Bool(b) => write!(f, "{}", b),
177 }
178 }
179}
180
181impl Value {
182 pub fn is_numeric(&self) -> bool {
184 matches!(self, Value::Int(_) | Value::Float(_))
185 }
186}
187
188impl From<i64> for Value {
190 fn from(v: i64) -> Self {
191 Value::Int(v)
192 }
193}
194
195impl From<i32> for Value {
196 fn from(v: i32) -> Self {
197 Value::Int(v as i64)
198 }
199}
200
201impl From<f64> for Value {
202 fn from(v: f64) -> Self {
203 Value::Float(v)
204 }
205}
206
207impl From<&str> for Value {
208 fn from(v: &str) -> Self {
209 Value::String(v.to_string())
210 }
211}
212
213impl From<String> for Value {
214 fn from(v: String) -> Self {
215 Value::String(v)
216 }
217}
218
219impl From<bool> for Value {
220 fn from(v: bool) -> Self {
221 Value::Bool(v)
222 }
223}
224
225impl Predicate {
226 pub fn eq(field: impl Into<String>, value: impl Into<Value>) -> Self {
232 Predicate::Eq(field.into(), value.into())
233 }
234
235 pub fn ne(field: impl Into<String>, value: impl Into<Value>) -> Self {
237 Predicate::Ne(field.into(), value.into())
238 }
239
240 pub fn gt(field: impl Into<String>, value: impl Into<Value>) -> Self {
242 Predicate::Gt(field.into(), value.into())
243 }
244
245 pub fn gte(field: impl Into<String>, value: impl Into<Value>) -> Self {
247 Predicate::Gte(field.into(), value.into())
248 }
249
250 pub fn lt(field: impl Into<String>, value: impl Into<Value>) -> Self {
252 Predicate::Lt(field.into(), value.into())
253 }
254
255 pub fn lte(field: impl Into<String>, value: impl Into<Value>) -> Self {
257 Predicate::Lte(field.into(), value.into())
258 }
259
260 pub fn between(field: impl Into<String>, min: impl Into<Value>, max: impl Into<Value>) -> Self {
262 Predicate::Between(field.into(), min.into(), max.into())
263 }
264
265 pub fn text_search(field: impl Into<String>, term: impl Into<String>) -> Self {
271 Predicate::TextSearch(field.into(), term.into())
272 }
273
274 pub fn prefix(field: impl Into<String>, prefix: impl Into<String>) -> Self {
276 Predicate::Prefix(field.into(), prefix.into())
277 }
278
279 pub fn suffix(field: impl Into<String>, suffix: impl Into<String>) -> Self {
281 Predicate::Suffix(field.into(), suffix.into())
282 }
283
284 pub fn infix(field: impl Into<String>, substring: impl Into<String>) -> Self {
286 Predicate::Infix(field.into(), substring.into())
287 }
288
289 pub fn wildcard(field: impl Into<String>, pattern: impl Into<String>) -> Self {
291 Predicate::Wildcard(field.into(), pattern.into())
292 }
293
294 pub fn wildcard_exact(field: impl Into<String>, pattern: impl Into<String>) -> Self {
297 Predicate::WildcardExact(field.into(), pattern.into())
298 }
299
300 pub fn fuzzy(field: impl Into<String>, term: impl Into<String>, distance: u8) -> Self {
302 Predicate::Fuzzy(field.into(), term.into(), distance.clamp(1, 3))
303 }
304
305 pub fn phrase(field: impl Into<String>, words: Vec<impl Into<String>>) -> Self {
307 Predicate::Phrase(field.into(), words.into_iter().map(|w| w.into()).collect())
308 }
309
310 pub fn phrase_with_options(
318 field: impl Into<String>,
319 words: Vec<impl Into<String>>,
320 slop: Option<u32>,
321 inorder: Option<bool>,
322 ) -> Self {
323 Predicate::PhraseWithOptions {
324 field: field.into(),
325 words: words.into_iter().map(|w| w.into()).collect(),
326 slop,
327 inorder,
328 }
329 }
330
331 pub fn optional(self) -> Self {
334 Predicate::Optional(Box::new(self))
335 }
336
337 pub fn tag(field: impl Into<String>, tag: impl Into<String>) -> Self {
343 Predicate::Tag(field.into(), tag.into())
344 }
345
346 pub fn tag_or(field: impl Into<String>, tags: Vec<impl Into<String>>) -> Self {
348 Predicate::TagOr(field.into(), tags.into_iter().map(|t| t.into()).collect())
349 }
350
351 pub fn multi_field_search(fields: Vec<impl Into<String>>, term: impl Into<String>) -> Self {
357 Predicate::MultiFieldSearch(fields.into_iter().map(|f| f.into()).collect(), term.into())
358 }
359
360 pub fn geo_radius(
366 field: impl Into<String>,
367 lon: f64,
368 lat: f64,
369 radius: f64,
370 unit: impl Into<String>,
371 ) -> Self {
372 Predicate::GeoRadius(field.into(), lon, lat, radius, unit.into())
373 }
374
375 pub fn geo_polygon(field: impl Into<String>, points: Vec<(f64, f64)>) -> Self {
378 Predicate::GeoPolygon {
379 field: field.into(),
380 points,
381 }
382 }
383
384 pub fn vector_knn(field: impl Into<String>, k: usize, vector_param: impl Into<String>) -> Self {
395 Predicate::VectorKnn {
396 field: field.into(),
397 k,
398 vector_param: vector_param.into(),
399 pre_filter: None,
400 }
401 }
402
403 pub fn vector_knn_with_filter(
405 field: impl Into<String>,
406 k: usize,
407 vector_param: impl Into<String>,
408 pre_filter: Predicate,
409 ) -> Self {
410 Predicate::VectorKnn {
411 field: field.into(),
412 k,
413 vector_param: vector_param.into(),
414 pre_filter: Some(Box::new(pre_filter)),
415 }
416 }
417
418 pub fn vector_range(
420 field: impl Into<String>,
421 radius: f64,
422 vector_param: impl Into<String>,
423 ) -> Self {
424 Predicate::VectorRange {
425 field: field.into(),
426 radius,
427 vector_param: vector_param.into(),
428 }
429 }
430
431 pub fn is_missing(field: impl Into<String>) -> Self {
437 Predicate::IsMissing(field.into())
438 }
439
440 pub fn is_not_missing(field: impl Into<String>) -> Self {
442 Predicate::IsNotMissing(field.into())
443 }
444
445 pub fn raw(query: impl Into<String>) -> Self {
451 Predicate::Raw(query.into())
452 }
453
454 pub fn and(self, other: Predicate) -> Self {
460 match self {
461 Predicate::And(mut preds) => {
462 preds.push(other);
463 Predicate::And(preds)
464 }
465 _ => Predicate::And(vec![self, other]),
466 }
467 }
468
469 pub fn or(self, other: Predicate) -> Self {
471 match self {
472 Predicate::Or(mut preds) => {
473 preds.push(other);
474 Predicate::Or(preds)
475 }
476 _ => Predicate::Or(vec![self, other]),
477 }
478 }
479
480 pub fn negate(self) -> Self {
482 Predicate::Not(Box::new(self))
483 }
484
485 pub fn boost(self, weight: f64) -> Self {
487 Predicate::Boost(Box::new(self), weight)
488 }
489
490 pub fn get_params(&self) -> Vec<(String, String)> {
497 let mut params = Vec::new();
498 self.collect_params(&mut params);
499 params
500 }
501
502 fn collect_params(&self, params: &mut Vec<(String, String)>) {
504 match self {
505 Predicate::GeoPolygon { points, .. } => {
506 let coords: Vec<String> = points
507 .iter()
508 .map(|(lon, lat)| format!("{} {}", lon, lat))
509 .collect();
510 let wkt = format!("POLYGON(({}))", coords.join(", "));
511 params.push(("poly".to_string(), wkt));
512 }
513 Predicate::VectorKnn {
514 vector_param,
515 pre_filter,
516 ..
517 } => {
518 params.push((vector_param.clone(), String::new()));
521 if let Some(filter) = pre_filter {
522 filter.collect_params(params);
523 }
524 }
525 Predicate::VectorRange { vector_param, .. } => {
526 params.push((vector_param.clone(), String::new()));
527 }
528 Predicate::And(preds) | Predicate::Or(preds) => {
529 for p in preds {
530 p.collect_params(params);
531 }
532 }
533 Predicate::Not(inner) | Predicate::Optional(inner) | Predicate::Boost(inner, _) => {
534 inner.collect_params(params);
535 }
536 _ => {}
537 }
538 }
539
540 pub fn to_query(&self) -> String {
542 match self {
543 Predicate::Eq(field, value) => {
545 if value.is_numeric() {
546 format!("@{}:[{} {}]", field, value, value)
547 } else {
548 format!("@{}:{{{}}}", field, escape_tag_value(&value.to_string()))
549 }
550 }
551 Predicate::Ne(field, value) => {
552 if value.is_numeric() {
553 format!("-@{}:[{} {}]", field, value, value)
554 } else {
555 format!("-@{}:{{{}}}", field, escape_tag_value(&value.to_string()))
556 }
557 }
558 Predicate::Gt(field, value) => {
559 format!("@{}:[({} +inf]", field, value)
560 }
561 Predicate::Gte(field, value) => {
562 format!("@{}:[{} +inf]", field, value)
563 }
564 Predicate::Lt(field, value) => {
565 format!("@{}:[-inf ({}]", field, value)
566 }
567 Predicate::Lte(field, value) => {
568 format!("@{}:[-inf {}]", field, value)
569 }
570 Predicate::Between(field, min, max) => {
571 format!("@{}:[{} {}]", field, min, max)
572 }
573
574 Predicate::And(preds) => {
576 if preds.is_empty() {
577 "*".to_string()
578 } else {
579 preds
580 .iter()
581 .map(|p| {
582 let q = p.to_query();
583 if matches!(p, Predicate::Or(_)) {
584 format!("({})", q)
585 } else {
586 q
587 }
588 })
589 .collect::<Vec<_>>()
590 .join(" ")
591 }
592 }
593 Predicate::Or(preds) => {
594 if preds.is_empty() {
595 "*".to_string()
596 } else {
597 preds
598 .iter()
599 .map(|p| {
600 let q = p.to_query();
601 if matches!(p, Predicate::And(_)) {
602 format!("({})", q)
603 } else {
604 q
605 }
606 })
607 .collect::<Vec<_>>()
608 .join(" | ")
609 }
610 }
611 Predicate::Not(inner) => {
612 format!("-({})", inner.to_query())
613 }
614
615 Predicate::TextSearch(field, term) => {
617 format!("@{}:{}", field, escape_text_value(term))
618 }
619 Predicate::Prefix(field, prefix) => {
620 format!("@{}:{}*", field, escape_text_value(prefix))
621 }
622 Predicate::Suffix(field, suffix) => {
623 format!("@{}:*{}", field, escape_text_value(suffix))
624 }
625 Predicate::Infix(field, substring) => {
626 format!("@{}:*{}*", field, escape_text_value(substring))
627 }
628 Predicate::Wildcard(field, pattern) => {
629 format!("@{}:{}", field, pattern)
630 }
631 Predicate::WildcardExact(field, pattern) => {
632 format!("@{}:\"w'{}\"", field, pattern)
633 }
634 Predicate::Fuzzy(field, term, distance) => {
635 let pct = "%".repeat(*distance as usize);
636 format!("@{}:{}{}{}", field, pct, escape_text_value(term), pct)
637 }
638 Predicate::Phrase(field, words) => {
639 format!("@{}:({})", field, words.join(" "))
640 }
641 Predicate::PhraseWithOptions {
642 field,
643 words,
644 slop,
645 inorder,
646 } => {
647 let phrase = words.join(" ");
648 let mut attrs = Vec::new();
649 if let Some(s) = slop {
650 attrs.push(format!("$slop: {}", s));
651 }
652 if let Some(io) = inorder {
653 attrs.push(format!("$inorder: {}", io));
654 }
655 if attrs.is_empty() {
656 format!("@{}:({})", field, phrase)
657 } else {
658 format!("@{}:({}) => {{ {}; }}", field, phrase, attrs.join("; "))
659 }
660 }
661 Predicate::Optional(inner) => {
662 format!("~{}", inner.to_query())
663 }
664
665 Predicate::Tag(field, tag) => {
667 format!("@{}:{{{}}}", field, escape_tag_value(tag))
668 }
669 Predicate::TagOr(field, tags) => {
670 let escaped: Vec<String> = tags.iter().map(|t| escape_tag_value(t)).collect();
671 format!("@{}:{{{}}}", field, escaped.join("|"))
672 }
673
674 Predicate::MultiFieldSearch(fields, term) => {
676 format!("@{}:{}", fields.join("|"), escape_text_value(term))
677 }
678
679 Predicate::GeoRadius(field, lon, lat, radius, unit) => {
681 format!("@{}:[{} {} {} {}]", field, lon, lat, radius, unit)
682 }
683 Predicate::GeoPolygon { field, points } => {
684 let _coords: Vec<String> = points
689 .iter()
690 .map(|(lon, lat)| format!("{} {}", lon, lat))
691 .collect();
692 format!("@{}:[WITHIN $poly]", field)
694 }
695
696 Predicate::IsMissing(field) => {
698 format!("ismissing(@{})", field)
699 }
700 Predicate::IsNotMissing(field) => {
701 format!("-ismissing(@{})", field)
702 }
703
704 Predicate::Boost(inner, weight) => {
706 format!("({}) => {{ $weight: {}; }}", inner.to_query(), weight)
707 }
708
709 Predicate::VectorKnn {
711 field,
712 k,
713 vector_param,
714 pre_filter,
715 } => {
716 let filter = pre_filter
717 .as_ref()
718 .map(|p| p.to_query())
719 .unwrap_or_else(|| "*".to_string());
720 format!("{}=>[KNN {} @{} ${}]", filter, k, field, vector_param)
721 }
722 Predicate::VectorRange {
723 field,
724 radius,
725 vector_param,
726 } => {
727 format!("@{}:[VECTOR_RANGE {} ${}]", field, radius, vector_param)
728 }
729
730 Predicate::Raw(query) => query.clone(),
732 }
733 }
734}
735
736fn escape_tag_value(s: &str) -> String {
738 let mut result = String::with_capacity(s.len());
739 for c in s.chars() {
740 match c {
741 ',' | '.' | '<' | '>' | '{' | '}' | '[' | ']' | '"' | '\'' | ':' | ';' | '!' | '@'
742 | '#' | '$' | '%' | '^' | '&' | '*' | '(' | ')' | '-' | '+' | '=' | '~' | ' ' => {
743 result.push('\\');
744 result.push(c);
745 }
746 _ => result.push(c),
747 }
748 }
749 result
750}
751
752fn escape_text_value(s: &str) -> String {
754 let mut result = String::with_capacity(s.len());
755 for c in s.chars() {
756 match c {
757 '@' | '{' | '}' | '[' | ']' | '(' | ')' | '|' | '-' | '~' => {
758 result.push('\\');
759 result.push(c);
760 }
761 _ => result.push(c),
762 }
763 }
764 result
765}
766
767#[derive(Debug, Clone, Default)]
769pub struct PredicateBuilder {
770 predicates: Vec<Predicate>,
771}
772
773impl PredicateBuilder {
774 pub fn new() -> Self {
776 Self::default()
777 }
778
779 pub fn and(mut self, predicate: Predicate) -> Self {
781 self.predicates.push(predicate);
782 self
783 }
784
785 pub fn build(self) -> String {
787 if self.predicates.is_empty() {
788 "*".to_string()
789 } else if self.predicates.len() == 1 {
790 self.predicates[0].to_query()
791 } else {
792 Predicate::And(self.predicates).to_query()
793 }
794 }
795
796 pub fn build_predicate(self) -> Predicate {
798 if self.predicates.is_empty() {
799 Predicate::Raw("*".to_string())
800 } else if self.predicates.len() == 1 {
801 self.predicates.into_iter().next().unwrap()
802 } else {
803 Predicate::And(self.predicates)
804 }
805 }
806}
807
808#[cfg(test)]
809mod tests {
810 use super::*;
811
812 #[test]
814 fn test_eq_numeric() {
815 let pred = Predicate::eq("age", 30);
816 assert_eq!(pred.to_query(), "@age:[30 30]");
817 }
818
819 #[test]
820 fn test_eq_string() {
821 let pred = Predicate::eq("status", "active");
822 assert_eq!(pred.to_query(), "@status:{active}");
823 }
824
825 #[test]
826 fn test_gt() {
827 let pred = Predicate::gt("age", 30);
828 assert_eq!(pred.to_query(), "@age:[(30 +inf]");
829 }
830
831 #[test]
832 fn test_gte() {
833 let pred = Predicate::gte("age", 30);
834 assert_eq!(pred.to_query(), "@age:[30 +inf]");
835 }
836
837 #[test]
838 fn test_lt() {
839 let pred = Predicate::lt("age", 30);
840 assert_eq!(pred.to_query(), "@age:[-inf (30]");
841 }
842
843 #[test]
844 fn test_lte() {
845 let pred = Predicate::lte("age", 30);
846 assert_eq!(pred.to_query(), "@age:[-inf 30]");
847 }
848
849 #[test]
850 fn test_between() {
851 let pred = Predicate::between("age", 20, 40);
852 assert_eq!(pred.to_query(), "@age:[20 40]");
853 }
854
855 #[test]
856 fn test_ne() {
857 let pred = Predicate::ne("status", "deleted");
858 assert_eq!(pred.to_query(), "-@status:{deleted}");
859 }
860
861 #[test]
863 fn test_and() {
864 let pred = Predicate::gt("age", 30).and(Predicate::eq("status", "active"));
865 assert_eq!(pred.to_query(), "@age:[(30 +inf] @status:{active}");
866 }
867
868 #[test]
869 fn test_or() {
870 let pred = Predicate::eq("status", "active").or(Predicate::eq("status", "pending"));
871 assert_eq!(pred.to_query(), "@status:{active} | @status:{pending}");
872 }
873
874 #[test]
875 fn test_not() {
876 let pred = Predicate::eq("status", "deleted").negate();
877 assert_eq!(pred.to_query(), "-(@status:{deleted})");
878 }
879
880 #[test]
881 fn test_complex_and_or() {
882 let pred = Predicate::gt("age", 30)
883 .and(Predicate::eq("status", "active"))
884 .or(Predicate::lt("age", 20));
885 let query = pred.to_query();
886 assert!(query.contains("@age:[(30 +inf]"));
887 assert!(query.contains("@status:{active}"));
888 assert!(query.contains("|"));
889 }
890
891 #[test]
893 fn test_text_search() {
894 let pred = Predicate::text_search("title", "python");
895 assert_eq!(pred.to_query(), "@title:python");
896 }
897
898 #[test]
899 fn test_prefix() {
900 let pred = Predicate::prefix("name", "jo");
901 assert_eq!(pred.to_query(), "@name:jo*");
902 }
903
904 #[test]
905 fn test_suffix() {
906 let pred = Predicate::suffix("name", "son");
907 assert_eq!(pred.to_query(), "@name:*son");
908 }
909
910 #[test]
911 fn test_fuzzy() {
912 let pred = Predicate::fuzzy("name", "john", 1);
913 assert_eq!(pred.to_query(), "@name:%john%");
914
915 let pred2 = Predicate::fuzzy("name", "john", 2);
916 assert_eq!(pred2.to_query(), "@name:%%john%%");
917 }
918
919 #[test]
920 fn test_phrase() {
921 let pred = Predicate::phrase("title", vec!["hello", "world"]);
922 assert_eq!(pred.to_query(), "@title:(hello world)");
923 }
924
925 #[test]
927 fn test_tag() {
928 let pred = Predicate::tag("category", "science");
929 assert_eq!(pred.to_query(), "@category:{science}");
930 }
931
932 #[test]
933 fn test_tag_or() {
934 let pred = Predicate::tag_or("tags", vec!["urgent", "important"]);
935 assert_eq!(pred.to_query(), "@tags:{urgent|important}");
936 }
937
938 #[test]
940 fn test_geo_radius() {
941 let pred = Predicate::geo_radius("location", -122.4, 37.7, 10.0, "km");
942 assert_eq!(pred.to_query(), "@location:[-122.4 37.7 10 km]");
943 }
944
945 #[test]
947 fn test_is_missing() {
948 let pred = Predicate::is_missing("email");
949 assert_eq!(pred.to_query(), "ismissing(@email)");
950 }
951
952 #[test]
953 fn test_is_not_missing() {
954 let pred = Predicate::is_not_missing("email");
955 assert_eq!(pred.to_query(), "-ismissing(@email)");
956 }
957
958 #[test]
960 fn test_boost() {
961 let pred = Predicate::text_search("title", "python").boost(2.0);
962 assert_eq!(pred.to_query(), "(@title:python) => { $weight: 2; }");
963 }
964
965 #[test]
967 fn test_builder() {
968 let query = PredicateBuilder::new()
969 .and(Predicate::gt("age", 30))
970 .and(Predicate::tag("status", "active"))
971 .build();
972 assert_eq!(query, "@age:[(30 +inf] @status:{active}");
973 }
974
975 #[test]
976 fn test_builder_empty() {
977 let query = PredicateBuilder::new().build();
978 assert_eq!(query, "*");
979 }
980
981 #[test]
983 fn test_escape_tag_value() {
984 let pred = Predicate::tag("email", "user@example.com");
985 assert_eq!(pred.to_query(), r"@email:{user\@example\.com}");
986 }
987
988 #[test]
989 fn test_float_values() {
990 let pred = Predicate::gt("score", 3.5);
991 assert_eq!(pred.to_query(), "@score:[(3.5 +inf]");
992 }
993
994 #[test]
1000 fn test_infix() {
1001 let pred = Predicate::infix("name", "sun");
1002 assert_eq!(pred.to_query(), "@name:*sun*");
1003 }
1004
1005 #[test]
1007 fn test_wildcard_exact() {
1008 let pred = Predicate::wildcard_exact("name", "foo*bar?");
1009 assert_eq!(pred.to_query(), "@name:\"w'foo*bar?\"");
1010 }
1011
1012 #[test]
1014 fn test_phrase_with_slop() {
1015 let pred = Predicate::phrase_with_options("title", vec!["hello", "world"], Some(2), None);
1016 assert_eq!(pred.to_query(), "@title:(hello world) => { $slop: 2; }");
1017 }
1018
1019 #[test]
1020 fn test_phrase_with_inorder() {
1021 let pred =
1022 Predicate::phrase_with_options("title", vec!["hello", "world"], None, Some(true));
1023 assert_eq!(
1024 pred.to_query(),
1025 "@title:(hello world) => { $inorder: true; }"
1026 );
1027 }
1028
1029 #[test]
1030 fn test_phrase_with_slop_and_inorder() {
1031 let pred =
1032 Predicate::phrase_with_options("title", vec!["hello", "world"], Some(2), Some(true));
1033 assert_eq!(
1034 pred.to_query(),
1035 "@title:(hello world) => { $slop: 2; $inorder: true; }"
1036 );
1037 }
1038
1039 #[test]
1041 fn test_optional() {
1042 let pred = Predicate::text_search("title", "python").optional();
1043 assert_eq!(pred.to_query(), "~@title:python");
1044 }
1045
1046 #[test]
1047 fn test_optional_combined() {
1048 let required = Predicate::text_search("title", "redis");
1049 let optional = Predicate::text_search("title", "tutorial").optional();
1050 let pred = required.and(optional);
1051 assert_eq!(pred.to_query(), "@title:redis ~@title:tutorial");
1052 }
1053
1054 #[test]
1056 fn test_multi_field_search() {
1057 let pred = Predicate::multi_field_search(vec!["title", "body"], "python");
1058 assert_eq!(pred.to_query(), "@title|body:python");
1059 }
1060
1061 #[test]
1062 fn test_multi_field_search_three_fields() {
1063 let pred = Predicate::multi_field_search(vec!["title", "body", "summary"], "redis");
1064 assert_eq!(pred.to_query(), "@title|body|summary:redis");
1065 }
1066
1067 #[test]
1069 fn test_geo_polygon() {
1070 let points = vec![
1071 (0.0, 0.0),
1072 (0.0, 10.0),
1073 (10.0, 10.0),
1074 (10.0, 0.0),
1075 (0.0, 0.0),
1076 ];
1077 let pred = Predicate::geo_polygon("location", points);
1078 assert_eq!(pred.to_query(), "@location:[WITHIN $poly]");
1079 }
1080
1081 #[test]
1082 fn test_geo_polygon_params() {
1083 let points = vec![
1084 (0.0, 0.0),
1085 (0.0, 10.0),
1086 (10.0, 10.0),
1087 (10.0, 0.0),
1088 (0.0, 0.0),
1089 ];
1090 let pred = Predicate::geo_polygon("location", points);
1091 let params = pred.get_params();
1092 assert_eq!(params.len(), 1);
1093 assert_eq!(params[0].0, "poly");
1094 assert!(params[0].1.starts_with("POLYGON(("));
1095 }
1096
1097 #[test]
1099 fn test_vector_knn() {
1100 let pred = Predicate::vector_knn("embedding", 10, "query_vec");
1101 assert_eq!(pred.to_query(), "*=>[KNN 10 @embedding $query_vec]");
1102 }
1103
1104 #[test]
1105 fn test_vector_knn_with_filter() {
1106 let filter = Predicate::eq("category", "science");
1107 let pred = Predicate::vector_knn_with_filter("embedding", 10, "query_vec", filter);
1108 assert_eq!(
1109 pred.to_query(),
1110 "@category:{science}=>[KNN 10 @embedding $query_vec]"
1111 );
1112 }
1113
1114 #[test]
1116 fn test_vector_range() {
1117 let pred = Predicate::vector_range("embedding", 0.5, "query_vec");
1118 assert_eq!(pred.to_query(), "@embedding:[VECTOR_RANGE 0.5 $query_vec]");
1119 }
1120}