1use crate::scalar::inverted::document_tokenizer::DocType;
5use crate::scalar::inverted::tokenizer::document_tokenizer::LanceTokenizer;
6use lance_core::{Error, Result};
7use serde::ser::SerializeMap;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FtsSearchParams {
13 pub limit: Option<usize>,
20 pub wand_factor: f32,
21 pub fuzziness: Option<u32>,
22 pub max_expansions: usize,
23 pub phrase_slop: Option<u32>,
26 pub prefix_length: u32,
28}
29
30impl FtsSearchParams {
31 pub fn new() -> Self {
32 Self {
33 limit: None,
34 wand_factor: 1.0,
35 fuzziness: Some(0),
36 max_expansions: 50,
37 phrase_slop: None,
38 prefix_length: 0,
39 }
40 }
41
42 pub fn with_limit(mut self, limit: Option<usize>) -> Self {
43 self.limit = limit;
44 self
45 }
46
47 pub fn with_wand_factor(mut self, factor: f32) -> Self {
48 self.wand_factor = factor;
49 self
50 }
51
52 pub fn with_fuzziness(mut self, fuzziness: Option<u32>) -> Self {
53 self.fuzziness = fuzziness;
54 self
55 }
56
57 pub fn with_max_expansions(mut self, max_expansions: usize) -> Self {
58 self.max_expansions = max_expansions;
59 self
60 }
61
62 pub fn with_phrase_slop(mut self, phrase_slop: Option<u32>) -> Self {
63 self.phrase_slop = phrase_slop;
64 self
65 }
66
67 pub fn with_prefix_length(mut self, prefix_length: u32) -> Self {
68 self.prefix_length = prefix_length;
69 self
70 }
71}
72
73impl Default for FtsSearchParams {
74 fn default() -> Self {
75 Self::new()
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
80pub enum Operator {
81 And,
82 #[default]
83 Or,
84}
85
86impl TryFrom<&str> for Operator {
87 type Error = Error;
88 fn try_from(value: &str) -> Result<Self> {
89 match value.to_ascii_uppercase().as_str() {
90 "AND" => Ok(Self::And),
91 "OR" => Ok(Self::Or),
92 _ => Err(Error::invalid_input(format!("Invalid operator: {}", value))),
93 }
94 }
95}
96
97impl From<Operator> for &'static str {
98 fn from(operator: Operator) -> Self {
99 match operator {
100 Operator::And => "AND",
101 Operator::Or => "OR",
102 }
103 }
104}
105
106pub trait FtsQueryNode {
107 fn columns(&self) -> HashSet<String>;
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "snake_case")]
112pub enum FtsQuery {
113 Match(MatchQuery),
115 Phrase(PhraseQuery),
116
117 Boost(BoostQuery),
119 MultiMatch(MultiMatchQuery),
120 Boolean(BooleanQuery),
121}
122
123impl std::fmt::Display for FtsQuery {
124 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
125 match self {
126 Self::Match(query) => write!(f, "Match({:?})", query),
127 Self::Phrase(query) => write!(f, "Phrase({:?})", query),
128 Self::Boost(query) => write!(
129 f,
130 "Boosting(positive={}, negative={}, negative_boost={})",
131 query.positive, query.negative, query.negative_boost
132 ),
133 Self::MultiMatch(query) => write!(f, "MultiMatch({:?})", query),
134 Self::Boolean(query) => {
135 write!(
136 f,
137 "Boolean(must={:?}, should={:?})",
138 query.must, query.should
139 )
140 }
141 }
142 }
143}
144
145impl FtsQueryNode for FtsQuery {
146 fn columns(&self) -> HashSet<String> {
147 match self {
148 Self::Match(query) => query.columns(),
149 Self::Phrase(query) => query.columns(),
150 Self::Boost(query) => {
151 let mut columns = query.positive.columns();
152 columns.extend(query.negative.columns());
153 columns
154 }
155 Self::MultiMatch(query) => {
156 let mut columns = HashSet::new();
157 for match_query in &query.match_queries {
158 columns.extend(match_query.columns());
159 }
160 columns
161 }
162 Self::Boolean(query) => {
163 let mut columns = HashSet::new();
164 for query in &query.must {
165 columns.extend(query.columns());
166 }
167 for query in &query.should {
168 columns.extend(query.columns());
169 }
170 columns
171 }
172 }
173 }
174}
175
176impl FtsQuery {
177 pub fn query(&self) -> String {
178 match self {
179 Self::Match(query) => query.terms.clone(),
180 Self::Phrase(query) => format!("\"{}\"", query.terms), Self::Boost(query) => query.positive.query(),
182 Self::MultiMatch(query) => query.match_queries[0].terms.clone(),
183 Self::Boolean(_) => {
184 String::new()
186 }
187 }
188 }
189
190 pub fn is_missing_column(&self) -> bool {
191 match self {
192 Self::Match(query) => query.column.is_none(),
193 Self::Phrase(query) => query.column.is_none(),
194 Self::Boost(query) => {
195 query.positive.is_missing_column() || query.negative.is_missing_column()
196 }
197 Self::MultiMatch(query) => query.match_queries.iter().any(|q| q.column.is_none()),
198 Self::Boolean(query) => {
199 query.must.iter().any(|q| q.is_missing_column())
200 || query.should.iter().any(|q| q.is_missing_column())
201 }
202 }
203 }
204
205 pub fn with_column(self, column: String) -> Self {
206 match self {
207 Self::Match(query) => Self::Match(query.with_column(Some(column))),
208 Self::Phrase(query) => Self::Phrase(query.with_column(Some(column))),
209 Self::Boost(query) => {
210 let positive = query.positive.with_column(column.clone());
211 let negative = query.negative.with_column(column);
212 Self::Boost(BoostQuery {
213 positive: Box::new(positive),
214 negative: Box::new(negative),
215 negative_boost: query.negative_boost,
216 })
217 }
218 Self::MultiMatch(query) => {
219 let match_queries = query
220 .match_queries
221 .into_iter()
222 .map(|q| q.with_column(Some(column.clone())))
223 .collect();
224 Self::MultiMatch(MultiMatchQuery { match_queries })
225 }
226 Self::Boolean(query) => {
227 let must = query
228 .must
229 .into_iter()
230 .map(|q| q.with_column(column.clone()))
231 .collect();
232 let should = query
233 .should
234 .into_iter()
235 .map(|q| q.with_column(column.clone()))
236 .collect();
237 let must_not = query
238 .must_not
239 .into_iter()
240 .map(|q| q.with_column(column.clone()))
241 .collect();
242 Self::Boolean(BooleanQuery {
243 must,
244 should,
245 must_not,
246 })
247 }
248 }
249 }
250}
251
252impl From<MatchQuery> for FtsQuery {
253 fn from(query: MatchQuery) -> Self {
254 Self::Match(query)
255 }
256}
257
258impl From<PhraseQuery> for FtsQuery {
259 fn from(query: PhraseQuery) -> Self {
260 Self::Phrase(query)
261 }
262}
263
264impl From<BoostQuery> for FtsQuery {
265 fn from(query: BoostQuery) -> Self {
266 Self::Boost(query)
267 }
268}
269
270impl From<MultiMatchQuery> for FtsQuery {
271 fn from(query: MultiMatchQuery) -> Self {
272 Self::MultiMatch(query)
273 }
274}
275
276impl From<BooleanQuery> for FtsQuery {
277 fn from(query: BooleanQuery) -> Self {
278 Self::Boolean(query)
279 }
280}
281
282#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
283pub struct MatchQuery {
284 pub column: Option<String>,
287 pub terms: String,
288
289 #[serde(default = "MatchQuery::default_boost")]
291 pub boost: f32,
292
293 pub fuzziness: Option<u32>,
300
301 #[serde(default = "MatchQuery::default_max_expansions")]
304 pub max_expansions: usize,
305
306 #[serde(default)]
311 pub operator: Operator,
312
313 #[serde(default)]
316 pub prefix_length: u32,
317}
318
319impl MatchQuery {
320 pub fn new(terms: String) -> Self {
321 Self {
322 column: None,
323 terms,
324 boost: 1.0,
325 fuzziness: Some(0),
326 max_expansions: 50,
327 operator: Operator::Or,
328 prefix_length: 0,
329 }
330 }
331
332 pub(crate) fn default_boost() -> f32 {
333 1.0
334 }
335
336 pub(crate) fn default_max_expansions() -> usize {
337 50
338 }
339
340 pub fn with_column(mut self, column: Option<String>) -> Self {
341 self.column = column;
342 self
343 }
344
345 pub fn with_boost(mut self, boost: f32) -> Self {
346 self.boost = boost;
347 self
348 }
349
350 pub fn with_fuzziness(mut self, fuzziness: Option<u32>) -> Self {
351 self.fuzziness = fuzziness;
352 self
353 }
354
355 pub fn with_max_expansions(mut self, max_expansions: usize) -> Self {
356 self.max_expansions = max_expansions;
357 self
358 }
359
360 pub fn with_operator(mut self, operator: Operator) -> Self {
361 self.operator = operator;
362 self
363 }
364
365 pub fn with_prefix_length(mut self, prefix_length: u32) -> Self {
366 self.prefix_length = prefix_length;
367 self
368 }
369
370 pub fn auto_fuzziness(token: &str) -> u32 {
371 match token.len() {
372 0..=2 => 0,
373 3..=5 => 1,
374 _ => 2,
375 }
376 }
377}
378
379impl FtsQueryNode for MatchQuery {
380 fn columns(&self) -> HashSet<String> {
381 let mut columns = HashSet::new();
382 if let Some(column) = &self.column {
383 columns.insert(column.clone());
384 }
385 columns
386 }
387}
388
389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
390pub struct PhraseQuery {
391 pub column: Option<String>,
394 pub terms: String,
395 #[serde(default = "u32::default")]
396 pub slop: u32,
397}
398
399impl PhraseQuery {
400 pub fn new(terms: String) -> Self {
401 Self {
402 column: None,
403 terms,
404 slop: 0,
405 }
406 }
407
408 pub fn with_column(mut self, column: Option<String>) -> Self {
409 self.column = column;
410 self
411 }
412
413 pub fn with_slop(mut self, slop: u32) -> Self {
414 self.slop = slop;
415 self
416 }
417}
418
419impl FtsQueryNode for PhraseQuery {
420 fn columns(&self) -> HashSet<String> {
421 let mut columns = HashSet::new();
422 if let Some(column) = &self.column {
423 columns.insert(column.clone());
424 }
425 columns
426 }
427}
428
429#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
430pub struct BoostQuery {
431 pub positive: Box<FtsQuery>,
432 pub negative: Box<FtsQuery>,
433 #[serde(default = "BoostQuery::default_negative_boost")]
434 pub negative_boost: f32,
435}
436
437impl BoostQuery {
438 pub fn new(positive: FtsQuery, negative: FtsQuery, negative_boost: Option<f32>) -> Self {
439 Self {
440 positive: Box::new(positive),
441 negative: Box::new(negative),
442 negative_boost: negative_boost.unwrap_or(0.5),
443 }
444 }
445
446 fn default_negative_boost() -> f32 {
447 0.5
448 }
449}
450
451impl FtsQueryNode for BoostQuery {
452 fn columns(&self) -> HashSet<String> {
453 let mut columns = self.positive.columns();
454 columns.extend(self.negative.columns());
455 columns
456 }
457}
458
459#[derive(Debug, Clone, PartialEq)]
460pub struct MultiMatchQuery {
461 pub match_queries: Vec<MatchQuery>,
463}
464
465impl Serialize for MultiMatchQuery {
466 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
467 where
468 S: serde::Serializer,
469 {
470 let mut map = serializer.serialize_map(Some(3))?;
471
472 let query = self.match_queries.first().ok_or(serde::ser::Error::custom(
473 "MultiMatchQuery must have at least one MatchQuery".to_string(),
474 ))?;
475 map.serialize_entry("query", &query.terms)?;
476 let columns = self
477 .match_queries
478 .iter()
479 .map(|q| q.column.as_ref().unwrap().clone())
480 .collect::<Vec<String>>();
481 map.serialize_entry("columns", &columns)?;
482 let boosts = self
483 .match_queries
484 .iter()
485 .map(|q| q.boost)
486 .collect::<Vec<f32>>();
487 map.serialize_entry("boost", &boosts)?;
488 map.end()
489 }
490}
491
492impl<'de> Deserialize<'de> for MultiMatchQuery {
493 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
494 where
495 D: serde::Deserializer<'de>,
496 {
497 #[derive(Deserialize)]
498 struct MultiMatchQueryData {
499 query: String,
500 columns: Vec<String>,
501 boost: Option<Vec<f32>>,
502 }
503
504 let data = MultiMatchQueryData::deserialize(deserializer)?;
505 let boosts = data.boost.unwrap_or(vec![1.0; data.columns.len()]);
506
507 Self::try_new(data.query, data.columns)
508 .map_err(serde::de::Error::custom)?
509 .try_with_boosts(boosts)
510 .map_err(serde::de::Error::custom)
511 }
512}
513
514impl MultiMatchQuery {
515 pub fn try_new(query: String, columns: Vec<String>) -> Result<Self> {
516 if columns.is_empty() {
517 return Err(Error::invalid_input(
518 "Cannot create MultiMatchQuery with no columns".to_string(),
519 ));
520 }
521
522 let match_queries = columns
523 .into_iter()
524 .map(|column| MatchQuery::new(query.clone()).with_column(Some(column)))
525 .collect();
526 Ok(Self { match_queries })
527 }
528
529 pub fn try_with_boosts(mut self, boosts: Vec<f32>) -> Result<Self> {
530 if boosts.len() != self.match_queries.len() {
531 return Err(Error::invalid_input(
532 "The number of boosts must match the number of queries".to_string(),
533 ));
534 }
535
536 for (query, boost) in self.match_queries.iter_mut().zip(boosts) {
537 query.boost = boost;
538 }
539 Ok(self)
540 }
541
542 pub fn with_operator(mut self, operator: Operator) -> Self {
543 for query in &mut self.match_queries {
544 query.operator = operator;
545 }
546 self
547 }
548}
549
550impl FtsQueryNode for MultiMatchQuery {
551 fn columns(&self) -> HashSet<String> {
552 let mut columns = HashSet::with_capacity(self.match_queries.len());
553 for query in &self.match_queries {
554 columns.extend(query.columns());
555 }
556 columns
557 }
558}
559
560pub enum Occur {
561 Should,
562 Must,
563 MustNot,
564}
565
566impl TryFrom<&str> for Occur {
567 type Error = Error;
568 fn try_from(value: &str) -> Result<Self> {
569 match value.to_ascii_uppercase().as_str() {
570 "SHOULD" => Ok(Self::Should),
571 "MUST" => Ok(Self::Must),
572 "MUST_NOT" => Ok(Self::MustNot),
573 _ => Err(Error::invalid_input(format!(
574 "Invalid occur value: {}",
575 value
576 ))),
577 }
578 }
579}
580
581impl From<Occur> for &'static str {
582 fn from(occur: Occur) -> Self {
583 match occur {
584 Occur::Should => "SHOULD",
585 Occur::Must => "MUST",
586 Occur::MustNot => "MUST_NOT",
587 }
588 }
589}
590
591#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
592pub struct BooleanQuery {
593 pub should: Vec<FtsQuery>,
594 pub must: Vec<FtsQuery>,
595 pub must_not: Vec<FtsQuery>,
596}
597
598impl BooleanQuery {
599 pub fn new(iter: impl IntoIterator<Item = (Occur, FtsQuery)>) -> Self {
600 let mut should = Vec::new();
601 let mut must = Vec::new();
602 let mut must_not = Vec::new();
603 for (occur, query) in iter {
604 match occur {
605 Occur::Should => should.push(query),
606 Occur::Must => must.push(query),
607 Occur::MustNot => must_not.push(query),
608 }
609 }
610 Self {
611 should,
612 must,
613 must_not,
614 }
615 }
616
617 pub fn with_should(mut self, query: FtsQuery) -> Self {
618 self.should.push(query);
619 self
620 }
621
622 pub fn with_must(mut self, query: FtsQuery) -> Self {
623 self.must.push(query);
624 self
625 }
626
627 pub fn with_must_not(mut self, query: FtsQuery) -> Self {
628 self.must_not.push(query);
629 self
630 }
631}
632
633#[derive(Debug, Clone, PartialEq)]
634#[cfg(test)]
635pub(crate) struct BooleanMatchPlan {
636 pub column: String,
637 pub should: Vec<MatchQuery>,
638 pub must: Vec<MatchQuery>,
639 pub must_not: Vec<MatchQuery>,
640}
641
642#[cfg(test)]
643impl BooleanMatchPlan {
644 pub(crate) fn try_build(query: &FtsQuery) -> Option<Self> {
645 match query {
646 FtsQuery::Match(match_query) => {
647 let mut column = None;
648 let mut should = Vec::new();
649 Self::push_match(&mut should, &mut column, match_query)?;
650 Some(Self {
651 column: column?,
652 should,
653 must: Vec::new(),
654 must_not: Vec::new(),
655 })
656 }
657 FtsQuery::Boolean(bool_query) => {
658 let mut column = None;
659 let should = Self::collect_matches(&bool_query.should, &mut column)?;
660 let must = Self::collect_matches(&bool_query.must, &mut column)?;
661 let must_not = Self::collect_matches(&bool_query.must_not, &mut column)?;
662
663 if should.is_empty() && must.is_empty() {
664 return None;
665 }
666 Some(Self {
667 column: column?,
668 should,
669 must,
670 must_not,
671 })
672 }
673 _ => None,
674 }
675 }
676
677 fn push_match(
678 dest: &mut Vec<MatchQuery>,
679 column: &mut Option<String>,
680 query: &MatchQuery,
681 ) -> Option<()> {
682 let query_column = query.column.as_ref()?;
683 if let Some(existing) = column.as_ref() {
684 if existing != query_column {
685 return None;
686 }
687 } else {
688 *column = Some(query_column.clone());
689 }
690 dest.push(query.clone());
691 Some(())
692 }
693
694 fn collect_matches(
695 queries: &[FtsQuery],
696 column: &mut Option<String>,
697 ) -> Option<Vec<MatchQuery>> {
698 let mut matches = Vec::with_capacity(queries.len());
699 for query in queries {
700 let FtsQuery::Match(match_query) = query else {
701 return None;
702 };
703 Self::push_match(&mut matches, column, match_query)?;
704 }
705 Some(matches)
706 }
707}
708
709impl FtsQueryNode for BooleanQuery {
710 fn columns(&self) -> HashSet<String> {
711 let mut columns = HashSet::new();
712 for query in &self.should {
713 columns.extend(query.columns());
714 }
715 for query in &self.must {
716 columns.extend(query.columns());
717 }
718 for query in &self.must_not {
719 columns.extend(query.columns());
720 }
721 columns
722 }
723}
724
725#[derive(Clone)]
726pub struct Tokens {
727 tokens: Vec<String>,
728 positions: Vec<u32>,
729 tokens_map: HashMap<String, usize>,
730 token_type: DocType,
731}
732
733impl Tokens {
734 pub fn new(tokens: Vec<String>, token_type: DocType) -> Self {
735 let positions = (0..tokens.len() as u32).collect();
736 Self::with_positions(tokens, positions, token_type)
737 }
738
739 pub fn with_positions(tokens: Vec<String>, positions: Vec<u32>, token_type: DocType) -> Self {
740 debug_assert_eq!(tokens.len(), positions.len());
741 let mut tokens_vec = vec![];
742 let mut tokens_map = HashMap::new();
743 for (idx, token) in tokens.into_iter().enumerate() {
744 tokens_vec.push(token.clone());
745 tokens_map.insert(token, idx);
746 }
747
748 Self {
749 tokens: tokens_vec,
750 positions,
751 tokens_map,
752 token_type,
753 }
754 }
755
756 pub fn len(&self) -> usize {
757 self.tokens.len()
758 }
759
760 pub fn is_empty(&self) -> bool {
761 self.tokens.is_empty()
762 }
763
764 pub fn token_type(&self) -> &DocType {
765 &self.token_type
766 }
767
768 pub fn contains(&self, token: &str) -> bool {
769 self.tokens_map.contains_key(token)
770 }
771
772 pub fn token_index(&self, token: &str) -> Option<usize> {
773 self.tokens_map.get(token).copied()
774 }
775
776 pub fn get_token(&self, index: usize) -> &str {
777 &self.tokens[index]
778 }
779
780 pub fn position(&self, index: usize) -> u32 {
781 self.positions[index]
782 }
783}
784
785impl IntoIterator for Tokens {
786 type Item = String;
787 type IntoIter = std::vec::IntoIter<String>;
788
789 fn into_iter(self) -> Self::IntoIter {
790 self.tokens.into_iter()
791 }
792}
793
794impl<'a> IntoIterator for &'a Tokens {
795 type Item = &'a String;
796 type IntoIter = std::slice::Iter<'a, String>;
797
798 fn into_iter(self) -> Self::IntoIter {
799 self.tokens.iter()
800 }
801}
802
803pub fn collect_query_tokens(text: &str, tokenizer: &mut Box<dyn LanceTokenizer>) -> Tokens {
804 let token_type = tokenizer.doc_type();
805 let mut stream = tokenizer.token_stream_for_search(text);
806 let mut tokens = Vec::new();
807 let mut positions = Vec::new();
808 while let Some(token) = stream.next() {
809 tokens.push(token.text.clone());
810 positions.push(token.position as u32);
811 }
812 Tokens::with_positions(tokens, positions, token_type)
813}
814
815pub fn has_query_token(
816 text: &str,
817 tokenizer: &mut Box<dyn LanceTokenizer>,
818 query_tokens: &Tokens,
819) -> bool {
820 let mut stream = tokenizer.token_stream_for_doc(text);
821 while let Some(token) = stream.next() {
822 if query_tokens.contains(&token.text) {
823 return true;
824 }
825 }
826 false
827}
828
829pub fn fill_fts_query_column(
830 query: &FtsQuery,
831 columns: &[String],
832 replace: bool,
833) -> Result<FtsQuery> {
834 if !query.is_missing_column() && !replace {
835 return Ok(query.clone());
836 }
837 match query {
838 FtsQuery::Match(match_query) => {
839 match columns.len() {
840 0 => {
841 Err(Error::invalid_input("Cannot perform full text search unless an INVERTED index has been created on at least one column".to_string()))
842 }
843 1 => {
844 let column = columns[0].clone();
845 let query = match_query.clone().with_column(Some(column));
846 Ok(FtsQuery::Match(query))
847 }
848 _ => {
849 let multi_match_query =
851 MultiMatchQuery::try_new(match_query.terms.clone(), columns.to_vec())?;
852 Ok(FtsQuery::MultiMatch(multi_match_query))
853 }
854 }
855 }
856 FtsQuery::Phrase(phrase_query) => {
857 match columns.len() {
858 0 => {
859 Err(Error::invalid_input("Cannot perform full text search unless an INVERTED index has been created on at least one column".to_string()))
860 }
861 1 => {
862 let column = columns[0].clone();
863 let query = phrase_query.clone().with_column(Some(column));
864 Ok(FtsQuery::Phrase(query))
865 }
866 _ => {
867 Err(Error::invalid_input("the column must be specified in the query".to_string()))
868 }
869 }
870 }
871 FtsQuery::Boost(boost_query) => {
872 let positive = fill_fts_query_column(&boost_query.positive, columns, replace)?;
873 let negative = fill_fts_query_column(&boost_query.negative, columns, replace)?;
874 Ok(FtsQuery::Boost(BoostQuery {
875 positive: Box::new(positive),
876 negative: Box::new(negative),
877 negative_boost: boost_query.negative_boost,
878 }))
879 }
880 FtsQuery::MultiMatch(multi_match_query) => {
881 let match_queries = multi_match_query
882 .match_queries
883 .iter()
884 .map(|query| fill_fts_query_column(&FtsQuery::Match(query.clone()), columns, replace))
885 .map(|result| {
886 result.map(|query| {
887 if let FtsQuery::Match(match_query) = query {
888 match_query
889 } else {
890 unreachable!("Expected MatchQuery")
891 }
892 })
893 })
894 .collect::<Result<Vec<_>>>()?;
895 Ok(FtsQuery::MultiMatch(MultiMatchQuery { match_queries }))
896 }
897 FtsQuery::Boolean(bool_query) => {
898 let must = bool_query
899 .must
900 .iter()
901 .map(|query| fill_fts_query_column(query, columns, replace))
902 .collect::<Result<Vec<_>>>()?;
903 let should = bool_query
904 .should
905 .iter()
906 .map(|query| fill_fts_query_column(query, columns, replace))
907 .collect::<Result<Vec<_>>>()?;
908 let must_not = bool_query
909 .must_not
910 .iter()
911 .map(|query| fill_fts_query_column(query, columns, replace))
912 .collect::<Result<Vec<_>>>()?;
913 Ok(FtsQuery::Boolean(BooleanQuery { must, should, must_not }))
914 }
915 }
916}
917
918#[cfg(test)]
919mod tests {
920 #[test]
921 fn test_match_query_serde() {
922 use super::*;
923 use serde_json::json;
924
925 let query = MatchQuery::new("hello world".to_string())
926 .with_column(Some("text".to_string()))
927 .with_boost(2.0)
928 .with_fuzziness(Some(1))
929 .with_max_expansions(10)
930 .with_operator(Operator::And);
931
932 let serialized = serde_json::to_value(&query).unwrap();
933 let expected = json!({
934 "column": "text",
935 "terms": "hello world",
936 "boost": 2.0,
937 "fuzziness": 1,
938 "max_expansions": 10,
939 "operator": "And",
940 "prefix_length": 0,
941 });
942 assert_eq!(serialized, expected);
943
944 let expected = json!({
945 "column": "text",
946 "terms": "hello world",
947 "fuzziness": 0,
948 });
949 let query = serde_json::from_str::<MatchQuery>(&expected.to_string()).unwrap();
950 assert_eq!(query.column, Some("text".to_owned()));
951 assert_eq!(query.terms, "hello world");
952 assert_eq!(query.boost, 1.0);
953 assert_eq!(query.fuzziness, Some(0));
954 assert_eq!(query.max_expansions, 50);
955 assert_eq!(query.operator, Operator::Or);
956 assert_eq!(query.prefix_length, 0);
957 }
958
959 #[test]
960 fn test_phrase_query_serde() {
961 use super::*;
962 use serde_json::json;
963
964 let query = json!({
965 "terms": "hello world",
966 });
967 let expected = PhraseQuery::new("hello world".to_string());
968 let query: PhraseQuery = serde_json::from_value(query).unwrap();
969 assert_eq!(query, expected);
970
971 let query = json!({
972 "terms": "hello world",
973 "column": "text",
974 "slop": 2,
975 });
976 let expected = PhraseQuery::new("hello world".to_string())
977 .with_column(Some("text".to_string()))
978 .with_slop(2);
979 let query: PhraseQuery = serde_json::from_value(query).unwrap();
980 assert_eq!(query, expected);
981 }
982
983 #[test]
984 fn test_boolean_match_plan_match_query() {
985 use super::*;
986
987 let query = MatchQuery::new("hello".to_string()).with_column(Some("text".to_string()));
988 let plan = BooleanMatchPlan::try_build(&FtsQuery::Match(query.clone())).unwrap();
989 assert_eq!(plan.column, "text");
990 assert_eq!(plan.should, vec![query]);
991 assert!(plan.must.is_empty());
992 assert!(plan.must_not.is_empty());
993 }
994
995 #[test]
996 fn test_boolean_match_plan_boolean_query() {
997 use super::*;
998
999 let should = MatchQuery::new("a".to_string()).with_column(Some("text".to_string()));
1000 let must = MatchQuery::new("b".to_string()).with_column(Some("text".to_string()));
1001 let must_not = MatchQuery::new("c".to_string()).with_column(Some("text".to_string()));
1002 let query = BooleanQuery::new(vec![
1003 (Occur::Should, should.clone().into()),
1004 (Occur::Must, must.clone().into()),
1005 (Occur::MustNot, must_not.clone().into()),
1006 ]);
1007 let plan = BooleanMatchPlan::try_build(&FtsQuery::Boolean(query)).unwrap();
1008 assert_eq!(plan.column, "text");
1009 assert_eq!(plan.should, vec![should]);
1010 assert_eq!(plan.must, vec![must]);
1011 assert_eq!(plan.must_not, vec![must_not]);
1012 }
1013
1014 #[test]
1015 fn test_boolean_match_plan_rejects_mixed_columns() {
1016 use super::*;
1017
1018 let should = MatchQuery::new("a".to_string()).with_column(Some("text".to_string()));
1019 let must = MatchQuery::new("b".to_string()).with_column(Some("title".to_string()));
1020 let query = BooleanQuery::new(vec![
1021 (Occur::Should, should.into()),
1022 (Occur::Must, must.into()),
1023 ]);
1024 assert!(BooleanMatchPlan::try_build(&FtsQuery::Boolean(query)).is_none());
1025 }
1026
1027 #[test]
1028 fn test_boolean_match_plan_rejects_non_match_queries() {
1029 use super::*;
1030
1031 let phrase =
1032 PhraseQuery::new("hello world".to_string()).with_column(Some("text".to_string()));
1033 let query = BooleanQuery::new(vec![(Occur::Should, phrase.into())]);
1034 assert!(BooleanMatchPlan::try_build(&FtsQuery::Boolean(query)).is_none());
1035 }
1036
1037 #[test]
1038 fn test_boolean_match_plan_rejects_only_must_not() {
1039 use super::*;
1040
1041 let must_not = MatchQuery::new("c".to_string()).with_column(Some("text".to_string()));
1042 let query = BooleanQuery::new(vec![(Occur::MustNot, must_not.into())]);
1043 assert!(BooleanMatchPlan::try_build(&FtsQuery::Boolean(query)).is_none());
1044 }
1045
1046 #[test]
1047 fn test_boolean_match_plan_rejects_missing_column() {
1048 use super::*;
1049
1050 let query = MatchQuery::new("hello".to_string());
1051 assert!(BooleanMatchPlan::try_build(&FtsQuery::Match(query)).is_none());
1052 }
1053}