Skip to main content

lb_tantivy/query/query_parser/
query_parser.rs

1use std::net::{AddrParseError, IpAddr};
2use std::num::{ParseFloatError, ParseIntError};
3use std::ops::Bound;
4use std::str::{FromStr, ParseBoolError};
5
6use base64::engine::general_purpose::STANDARD as BASE64;
7use base64::Engine;
8use itertools::Itertools;
9use query_grammar::{UserInputAst, UserInputBound, UserInputLeaf, UserInputLiteral};
10use rustc_hash::FxHashMap;
11
12use super::logical_ast::*;
13use crate::index::Index;
14use crate::json_utils::convert_to_fast_value_and_append_to_json_term;
15use crate::query::range_query::{is_type_valid_for_fastfield_range_query, RangeQuery};
16use crate::query::{
17    AllQuery, BooleanQuery, BoostQuery, EmptyQuery, FuzzyTermQuery, Occur, PhrasePrefixQuery,
18    PhraseQuery, Query, TermQuery, TermSetQuery,
19};
20use crate::schema::{
21    Facet, FacetParseError, Field, FieldType, IndexRecordOption, IntoIpv6Addr, JsonObjectOptions,
22    Schema, Term, TextFieldIndexing, Type,
23};
24use crate::time::format_description::well_known::Rfc3339;
25use crate::time::OffsetDateTime;
26use crate::tokenizer::{TextAnalyzer, TokenizerManager};
27use crate::{DateTime, Score};
28
29/// Possible error that may happen when parsing a query.
30#[derive(Debug, PartialEq, Eq, Error)]
31pub enum QueryParserError {
32    /// Error in the query syntax
33    #[error("Syntax Error: {0}")]
34    SyntaxError(String),
35    /// This query is unsupported.
36    #[error("Unsupported query: {0}")]
37    UnsupportedQuery(String),
38    /// The query references a field that is not in the schema
39    #[error("Field does not exist: '{0}'")]
40    FieldDoesNotExist(String),
41    /// The query contains a term for a `u64` or `i64`-field, but the value
42    /// is neither.
43    #[error("Expected a valid integer: '{0:?}'")]
44    ExpectedInt(#[from] ParseIntError),
45    /// The query contains a term for a bytes field, but the value is not valid
46    /// base64.
47    #[error("Expected base64: '{0:?}'")]
48    ExpectedBase64(#[from] base64::DecodeError),
49    /// The query contains a term for a `f64`-field, but the value
50    /// is not a f64.
51    #[error("Invalid query: Only excluding terms given")]
52    ExpectedFloat(#[from] ParseFloatError),
53    /// The query contains a term for a bool field, but the value
54    /// is not a bool.
55    #[error("Expected a bool value: '{0:?}'")]
56    ExpectedBool(#[from] ParseBoolError),
57    /// It is forbidden queries that are only "excluding". (e.g. -title:pop)
58    #[error("Invalid query: Only excluding terms given")]
59    AllButQueryForbidden,
60    /// If no default field is declared, running a query without any
61    /// field specified is forbidden.
62    #[error("No default field declared and no field specified in query")]
63    NoDefaultFieldDeclared,
64    /// The field searched for is not declared
65    /// as indexed in the schema.
66    #[error("The field '{0}' is not declared as indexed")]
67    FieldNotIndexed(String),
68    /// A phrase query was requested for a field that does not
69    /// have any positions indexed.
70    #[error("The field '{0}' does not have positions indexed")]
71    FieldDoesNotHavePositionsIndexed(String),
72    /// A phrase-prefix query requires at least two terms
73    #[error(
74        "The phrase '{phrase:?}' does not produce at least two terms using the tokenizer \
75         '{tokenizer:?}'"
76    )]
77    PhrasePrefixRequiresAtLeastTwoTerms {
78        /// The phrase which triggered the issue
79        phrase: String,
80        /// The tokenizer configured for the field
81        tokenizer: String,
82    },
83    /// The tokenizer for the given field is unknown
84    /// The two argument strings are the name of the field, the name of the tokenizer
85    #[error("The tokenizer '{tokenizer:?}' for the field '{field:?}' is unknown")]
86    UnknownTokenizer {
87        /// The name of the tokenizer
88        tokenizer: String,
89        /// The field name
90        field: String,
91    },
92    /// The query contains a range query with a phrase as one of the bounds.
93    /// Only terms can be used as bounds.
94    #[error("A range query cannot have a phrase as one of the bounds")]
95    RangeMustNotHavePhrase,
96    /// The format for the date field is not RFC 3339 compliant.
97    #[error("The date field has an invalid format")]
98    DateFormatError(#[from] time::error::Parse),
99    /// The format for the facet field is invalid.
100    #[error("The facet field is malformed: {0}")]
101    FacetFormatError(#[from] FacetParseError),
102    /// The format for the ip field is invalid.
103    #[error("The ip field is malformed: {0}")]
104    IpFormatError(#[from] AddrParseError),
105}
106
107/// Recursively remove empty clause from the AST
108///
109/// Returns `None` if and only if the `logical_ast` ended up being empty.
110fn trim_ast(logical_ast: LogicalAst) -> Option<LogicalAst> {
111    match logical_ast {
112        LogicalAst::Clause(children) => {
113            let trimmed_children = children
114                .into_iter()
115                .flat_map(|(occur, child)| {
116                    trim_ast(child).map(|trimmed_child| (occur, trimmed_child))
117                })
118                .collect::<Vec<_>>();
119            if trimmed_children.is_empty() {
120                None
121            } else {
122                Some(LogicalAst::Clause(trimmed_children))
123            }
124        }
125        _ => Some(logical_ast),
126    }
127}
128
129/// Tantivy's Query parser
130///
131/// The language covered by the current parser is extremely simple.
132///
133/// * simple terms: "e.g.: `Barack Obama` will be seen as a sequence of two tokens Barack and Obama.
134///   By default, the query parser will interpret this as a disjunction (see
135///   `.set_conjunction_by_default()`) and will match all documents that contains either "Barack" or
136///   "Obama" or both. Since we did not target a specific field, the query parser will look into the
137///   so-called default fields (as set up in the constructor).
138///
139///   Assuming that the default fields are `body` and `title`, and the query parser is set with
140///   conjunction as a default, our query will be interpreted as.
141///   `(body:Barack OR title:Barack) AND (title:Obama OR body:Obama)`.
142///   By default, all tokenized and indexed fields are default fields.
143///
144///   It is possible to explicitly target a field by prefixing the text by the `fieldname:`.
145///   Note this only applies to the term directly following.
146///   For instance, assuming the query parser is configured to use conjunction by default,
147///   `body:Barack Obama` is not interpreted as `body:Barack AND body:Obama` but as
148///   `body:Barack OR (body:Barack OR text:Obama)` .
149///
150/// * boolean operators `AND`, `OR`. `AND` takes precedence over `OR`, so that `a AND b OR c` is
151///   interpreted as `(a AND b) OR c`.
152///
153/// * In addition to the boolean operators, the `-`, `+` can help define. These operators are
154///   sufficient to express all queries using boolean operators. For instance `x AND y OR z` can be
155///   written (`(+x +y) z`). In addition, these operators can help define "required optional"
156///   queries. `(+x y)` matches the same document set as simply `x`, but `y` will help refining the
157///   score.
158///
159/// * negative terms: By prepending a term by a `-`, a term can be excluded from the search. This is
160///   useful for disambiguating a query. e.g. `apple -fruit`
161///
162/// * must terms: By prepending a term by a `+`, a term can be made required for the search.
163///
164/// * phrase terms: Quoted terms become phrase searches on fields that have positions indexed. e.g.,
165///   `title:"Barack Obama"` will only find documents that have "barack" immediately followed by
166///   "obama". Single quotes can also be used. If the text to be searched contains quotation mark,
167///   it is possible to escape them with a `\`.
168///
169/// * range terms: Range searches can be done by specifying the start and end bound. These can be
170///   inclusive or exclusive. e.g., `title:[a TO c}` will find all documents whose title contains a
171///   word lexicographically between `a` and `c` (inclusive lower bound, exclusive upper bound).
172///   Inclusive bounds are `[]`, exclusive are `{}`.
173///
174/// * set terms: Using the `IN` operator, a field can be matched against a set of literals, e.g.
175///   `title: IN [a b cd]` will match documents where `title` is either `a`, `b` or `cd`, but do so
176///   more efficiently than the alternative query `title:a OR title:b OR title:c` does.
177///
178/// * date values: The query parser supports rfc3339 formatted dates. For example
179///   `"2002-10-02T15:00:00.05Z"` or `some_date_field:[2002-10-02T15:00:00Z TO
180///   2002-10-02T18:00:00Z}`
181///
182/// * all docs query: A plain `*` will match all documents in the index.
183///
184/// Parts of the queries can be boosted by appending `^boostfactor`.
185/// For instance, `"SRE"^2.0 OR devops^0.4` will boost documents containing `SRE` instead of
186/// devops. Negative boosts are not allowed.
187///
188/// It is also possible to define a boost for a some specific field, at the query parser level.
189/// (See [`set_field_boost(...)`](QueryParser::set_field_boost)). Typically you may want to boost a
190/// title field.
191///
192/// Additionally, specific fields can be marked to use fuzzy term queries for each literal
193/// via the [`QueryParser::set_field_fuzzy`] method.
194///
195/// Phrase terms support the `~` slop operator which allows to set the phrase's matching
196/// distance in words. `"big wolf"~1` will return documents containing the phrase `"big bad wolf"`.
197///
198/// Phrase terms also support the `*` prefix operator which switches the phrase's matching
199/// to consider all documents which contain the last term as a prefix, e.g. `"big bad wo"*` will
200/// match `"big bad wolf"`.
201#[derive(Clone)]
202pub struct QueryParser {
203    schema: Schema,
204    default_fields: Vec<Field>,
205    conjunction_by_default: bool,
206    tokenizer_manager: TokenizerManager,
207    boost: FxHashMap<Field, Score>,
208    fuzzy: FxHashMap<Field, Fuzzy>,
209}
210
211#[derive(Clone)]
212struct Fuzzy {
213    prefix: bool,
214    distance: u8,
215    transpose_cost_one: bool,
216}
217
218fn all_negative(ast: &LogicalAst) -> bool {
219    match ast {
220        LogicalAst::Leaf(_) => false,
221        LogicalAst::Boost(ref child_ast, _) => all_negative(child_ast),
222        LogicalAst::Clause(children) => children
223            .iter()
224            .all(|(ref occur, child)| (*occur == Occur::MustNot) || all_negative(child)),
225    }
226}
227
228// Make an all-negative ast into a normal ast. Must not be used on an already okay ast.
229fn make_non_negative(ast: &mut LogicalAst) {
230    match ast {
231        LogicalAst::Leaf(_) => (),
232        LogicalAst::Boost(ref mut child_ast, _) => make_non_negative(child_ast),
233        LogicalAst::Clause(children) => children.push((Occur::Should, LogicalLiteral::All.into())),
234    }
235}
236
237/// Similar to the try/? macro, but returns a tuple of (None, Vec<Error>) instead of Err(Error)
238macro_rules! try_tuple {
239    ($expr:expr) => {{
240        match $expr {
241            Ok(val) => val,
242            Err(e) => return (None, vec![e.into()]),
243        }
244    }};
245}
246
247impl QueryParser {
248    /// Creates a `QueryParser`, given
249    /// * schema - index Schema
250    /// * default_fields - fields used to search if no field is specifically defined in the query.
251    pub fn new(
252        schema: Schema,
253        default_fields: Vec<Field>,
254        tokenizer_manager: TokenizerManager,
255    ) -> QueryParser {
256        QueryParser {
257            schema,
258            default_fields,
259            tokenizer_manager,
260            conjunction_by_default: false,
261            boost: Default::default(),
262            fuzzy: Default::default(),
263        }
264    }
265
266    // Splits a full_path as written in a query, into a field name and a
267    // json path.
268    pub(crate) fn split_full_path<'a>(&self, full_path: &'a str) -> Option<(Field, &'a str)> {
269        self.schema.find_field(full_path)
270    }
271
272    /// Creates a `QueryParser`, given
273    ///  * an index
274    ///  * a set of default fields used to search if no field is specifically defined in the query.
275    pub fn for_index(index: &Index, default_fields: Vec<Field>) -> QueryParser {
276        QueryParser::new(index.schema(), default_fields, index.tokenizers().clone())
277    }
278
279    /// Set the default way to compose queries to a conjunction.
280    ///
281    /// By default, the query `happy tax payer` is equivalent to the query
282    /// `happy OR tax OR payer`. After calling `.set_conjunction_by_default()`
283    /// `happy tax payer` will be interpreted by the parser as `happy AND tax AND payer`.
284    pub fn set_conjunction_by_default(&mut self) {
285        self.conjunction_by_default = true;
286    }
287
288    /// Sets a boost for a specific field.
289    ///
290    /// The parse query will automatically boost this field.
291    ///
292    /// If the query defines a query boost through the query language (e.g: `country:France^3.0`),
293    /// the two boosts (the one defined in the query, and the one defined in the `QueryParser`)
294    /// are multiplied together.
295    pub fn set_field_boost(&mut self, field: Field, boost: Score) {
296        self.boost.insert(field, boost);
297    }
298
299    /// Sets the given [field][`Field`] to use [fuzzy term queries][`FuzzyTermQuery`]
300    ///
301    /// If set, the parse will produce queries using fuzzy term queries
302    /// with the given parameters for each literal matched against the given field.
303    ///
304    /// See the [`FuzzyTermQuery::new`] and [`FuzzyTermQuery::new_prefix`] methods
305    /// for the meaning of the individual parameters.
306    pub fn set_field_fuzzy(
307        &mut self,
308        field: Field,
309        prefix: bool,
310        distance: u8,
311        transpose_cost_one: bool,
312    ) {
313        self.fuzzy.insert(
314            field,
315            Fuzzy {
316                prefix,
317                distance,
318                transpose_cost_one,
319            },
320        );
321    }
322
323    /// Parse a query
324    ///
325    /// Note that `parse_query` returns an error if the input
326    /// is not a valid query.
327    pub fn parse_query(&self, query: &str) -> Result<Box<dyn Query>, QueryParserError> {
328        let logical_ast = self.parse_query_to_logical_ast(query)?;
329        Ok(convert_to_query(&self.fuzzy, logical_ast))
330    }
331
332    /// Parse a query leniently
333    ///
334    /// This variant parses invalid query on a best effort basis. If some part of the query can't
335    /// reasonably be executed (range query without field, searching on a non existing field,
336    /// searching without precising field when no default field is provided...), they may get
337    /// turned into a "match-nothing" subquery.
338    ///
339    /// In case it encountered such issues, they are reported as a Vec of errors.
340    pub fn parse_query_lenient(&self, query: &str) -> (Box<dyn Query>, Vec<QueryParserError>) {
341        let (logical_ast, errors) = self.parse_query_to_logical_ast_lenient(query);
342        (convert_to_query(&self.fuzzy, logical_ast), errors)
343    }
344
345    /// Build a query from an already parsed user input AST
346    ///
347    /// This can be useful if the user input AST parsed using [`query_grammar`]
348    /// needs to be inspected before the query is re-interpreted w.r.t.
349    /// index specifics like field names and tokenizers.
350    pub fn build_query_from_user_input_ast(
351        &self,
352        user_input_ast: UserInputAst,
353    ) -> Result<Box<dyn Query>, QueryParserError> {
354        let (logical_ast, mut err) = self.compute_logical_ast_lenient(user_input_ast);
355        if !err.is_empty() {
356            return Err(err.swap_remove(0));
357        }
358        Ok(convert_to_query(&self.fuzzy, logical_ast))
359    }
360
361    /// Build leniently a query from an already parsed user input AST.
362    ///
363    /// See also [`QueryParser::build_query_from_user_input_ast`]
364    pub fn build_query_from_user_input_ast_lenient(
365        &self,
366        user_input_ast: UserInputAst,
367    ) -> (Box<dyn Query>, Vec<QueryParserError>) {
368        let (logical_ast, errors) = self.compute_logical_ast_lenient(user_input_ast);
369        (convert_to_query(&self.fuzzy, logical_ast), errors)
370    }
371
372    /// Parse the user query into an AST.
373    fn parse_query_to_logical_ast(&self, query: &str) -> Result<LogicalAst, QueryParserError> {
374        let user_input_ast = query_grammar::parse_query(query)
375            .map_err(|_| QueryParserError::SyntaxError(query.to_string()))?;
376        let (ast, mut err) = self.compute_logical_ast_lenient(user_input_ast);
377        if !err.is_empty() {
378            return Err(err.swap_remove(0));
379        }
380        Ok(ast.simplify())
381    }
382
383    /// Parse the user query into an AST.
384    fn parse_query_to_logical_ast_lenient(
385        &self,
386        query: &str,
387    ) -> (LogicalAst, Vec<QueryParserError>) {
388        let (user_input_ast, errors) = query_grammar::parse_query_lenient(query);
389        let mut errors: Vec<_> = errors
390            .into_iter()
391            .map(|error| {
392                QueryParserError::SyntaxError(format!(
393                    "{} at position {}",
394                    error.message, error.pos
395                ))
396            })
397            .collect();
398        let (ast, mut ast_errors) = self.compute_logical_ast_lenient(user_input_ast);
399        errors.append(&mut ast_errors);
400        (ast, errors)
401    }
402
403    fn compute_logical_ast_lenient(
404        &self,
405        user_input_ast: UserInputAst,
406    ) -> (LogicalAst, Vec<QueryParserError>) {
407        let (mut ast, mut err) = self.compute_logical_ast_with_occur_lenient(user_input_ast);
408        if let LogicalAst::Clause(children) = &ast {
409            if children.is_empty() {
410                return (ast, err);
411            }
412        }
413        if all_negative(&ast) {
414            err.push(QueryParserError::AllButQueryForbidden);
415            make_non_negative(&mut ast);
416        }
417        (ast, err)
418    }
419
420    fn compute_boundary_term(
421        &self,
422        field: Field,
423        json_path: &str,
424        phrase: &str,
425    ) -> Result<Term, QueryParserError> {
426        let field_entry = self.schema.get_field_entry(field);
427        let field_type = field_entry.field_type();
428        let field_supports_ff_range_queries = field_type.is_fast()
429            && is_type_valid_for_fastfield_range_query(field_type.value_type());
430
431        if !field_type.is_indexed() && !field_supports_ff_range_queries {
432            return Err(QueryParserError::FieldNotIndexed(
433                field_entry.name().to_string(),
434            ));
435        }
436        if !json_path.is_empty() && field_type.value_type() != Type::Json {
437            return Err(QueryParserError::UnsupportedQuery(format!(
438                "Json path is not supported for field {:?}",
439                field_entry.name()
440            )));
441        }
442        match *field_type {
443            FieldType::U64(_) => {
444                let val: u64 = u64::from_str(phrase)?;
445                Ok(Term::from_field_u64(field, val))
446            }
447            FieldType::I64(_) => {
448                let val: i64 = i64::from_str(phrase)?;
449                Ok(Term::from_field_i64(field, val))
450            }
451            FieldType::F64(_) => {
452                let val: f64 = f64::from_str(phrase)?;
453                Ok(Term::from_field_f64(field, val))
454            }
455            FieldType::Bool(_) => {
456                let val: bool = bool::from_str(phrase)?;
457                Ok(Term::from_field_bool(field, val))
458            }
459            FieldType::Date(_) => {
460                let dt = OffsetDateTime::parse(phrase, &Rfc3339)?;
461                Ok(Term::from_field_date(field, DateTime::from_utc(dt)))
462            }
463            FieldType::Str(ref str_options) => {
464                let option = str_options.get_indexing_options().ok_or_else(|| {
465                    // This should have been seen earlier really.
466                    QueryParserError::FieldNotIndexed(field_entry.name().to_string())
467                })?;
468                let mut text_analyzer =
469                    self.tokenizer_manager
470                        .get(option.tokenizer())
471                        .ok_or_else(|| QueryParserError::UnknownTokenizer {
472                            field: field_entry.name().to_string(),
473                            tokenizer: option.tokenizer().to_string(),
474                        })?;
475                let mut terms: Vec<Term> = Vec::new();
476                let mut token_stream = text_analyzer.token_stream(phrase);
477                token_stream.process(&mut |token| {
478                    let term = Term::from_field_text(field, &token.text);
479                    terms.push(term);
480                });
481                if terms.len() != 1 {
482                    return Err(QueryParserError::UnsupportedQuery(format!(
483                        "Range query boundary cannot have multiple tokens: {phrase:?} [{terms:?}]."
484                    )));
485                }
486                Ok(terms.into_iter().next().unwrap())
487            }
488            FieldType::JsonObject(ref json_options) => {
489                let get_term_with_path = || {
490                    Term::from_field_json_path(
491                        field,
492                        json_path,
493                        json_options.is_expand_dots_enabled(),
494                    )
495                };
496                if let Some(term) =
497                    // Try to convert the phrase to a fast value
498                    convert_to_fast_value_and_append_to_json_term(
499                        get_term_with_path(),
500                        phrase,
501                        false,
502                    )
503                {
504                    Ok(term)
505                } else {
506                    let mut term = get_term_with_path();
507                    term.append_type_and_str(phrase);
508                    Ok(term)
509                }
510            }
511            FieldType::Facet(_) => match Facet::from_text(phrase) {
512                Ok(facet) => Ok(Term::from_facet(field, &facet)),
513                Err(e) => Err(QueryParserError::from(e)),
514            },
515            FieldType::Bytes(_) => {
516                let bytes = BASE64
517                    .decode(phrase)
518                    .map_err(QueryParserError::ExpectedBase64)?;
519                Ok(Term::from_field_bytes(field, &bytes))
520            }
521            FieldType::IpAddr(_) => {
522                let ip_v6 = IpAddr::from_str(phrase)?.into_ipv6_addr();
523                Ok(Term::from_field_ip_addr(field, ip_v6))
524            }
525        }
526    }
527
528    fn compute_logical_ast_for_leaf(
529        &self,
530        field: Field,
531        json_path: &str,
532        phrase: &str,
533        slop: u32,
534        prefix: bool,
535    ) -> Result<Vec<LogicalLiteral>, QueryParserError> {
536        let field_entry = self.schema.get_field_entry(field);
537        let field_type = field_entry.field_type();
538        let field_name = field_entry.name();
539        if !field_type.is_indexed() {
540            return Err(QueryParserError::FieldNotIndexed(field_name.to_string()));
541        }
542        if field_type.value_type() != Type::Json && !json_path.is_empty() {
543            let field_name = self.schema.get_field_name(field);
544            return Err(QueryParserError::FieldDoesNotExist(format!(
545                "{field_name}.{json_path}"
546            )));
547        }
548        match *field_type {
549            FieldType::U64(_) => {
550                let val: u64 = u64::from_str(phrase)?;
551                let i64_term = Term::from_field_u64(field, val);
552                Ok(vec![LogicalLiteral::Term(i64_term)])
553            }
554            FieldType::I64(_) => {
555                let val: i64 = i64::from_str(phrase)?;
556                let i64_term = Term::from_field_i64(field, val);
557                Ok(vec![LogicalLiteral::Term(i64_term)])
558            }
559            FieldType::F64(_) => {
560                let val: f64 = f64::from_str(phrase)?;
561                let f64_term = Term::from_field_f64(field, val);
562                Ok(vec![LogicalLiteral::Term(f64_term)])
563            }
564            FieldType::Bool(_) => {
565                let val: bool = bool::from_str(phrase)?;
566                let bool_term = Term::from_field_bool(field, val);
567                Ok(vec![LogicalLiteral::Term(bool_term)])
568            }
569            FieldType::Date(_) => {
570                let dt = OffsetDateTime::parse(phrase, &Rfc3339)?;
571                let dt_term = Term::from_field_date_for_search(field, DateTime::from_utc(dt));
572                Ok(vec![LogicalLiteral::Term(dt_term)])
573            }
574            FieldType::Str(ref str_options) => {
575                let indexing_options = str_options.get_indexing_options().ok_or_else(|| {
576                    // This should have been seen earlier really.
577                    QueryParserError::FieldNotIndexed(field_name.to_string())
578                })?;
579                let mut text_analyzer = self
580                    .tokenizer_manager
581                    .get(indexing_options.tokenizer())
582                    .ok_or_else(|| QueryParserError::UnknownTokenizer {
583                        field: field_name.to_string(),
584                        tokenizer: indexing_options.tokenizer().to_string(),
585                    })?;
586                Ok(generate_literals_for_str(
587                    field_name,
588                    field,
589                    phrase,
590                    slop,
591                    prefix,
592                    indexing_options,
593                    &mut text_analyzer,
594                )?
595                .into_iter()
596                .collect())
597            }
598            FieldType::JsonObject(ref json_options) => generate_literals_for_json_object(
599                field_name,
600                field,
601                json_path,
602                phrase,
603                &self.tokenizer_manager,
604                json_options,
605            ),
606            FieldType::Facet(_) => match Facet::from_text(phrase) {
607                Ok(facet) => {
608                    let facet_term = Term::from_facet(field, &facet);
609                    Ok(vec![LogicalLiteral::Term(facet_term)])
610                }
611                Err(e) => Err(QueryParserError::from(e)),
612            },
613            FieldType::Bytes(_) => {
614                let bytes = BASE64
615                    .decode(phrase)
616                    .map_err(QueryParserError::ExpectedBase64)?;
617                let bytes_term = Term::from_field_bytes(field, &bytes);
618                Ok(vec![LogicalLiteral::Term(bytes_term)])
619            }
620            FieldType::IpAddr(_) => {
621                let ip_v6 = IpAddr::from_str(phrase)?.into_ipv6_addr();
622                let term = Term::from_field_ip_addr(field, ip_v6);
623                Ok(vec![LogicalLiteral::Term(term)])
624            }
625        }
626    }
627
628    fn default_occur(&self) -> Occur {
629        if self.conjunction_by_default {
630            Occur::Must
631        } else {
632            Occur::Should
633        }
634    }
635
636    fn resolve_bound(
637        &self,
638        field: Field,
639        json_path: &str,
640        bound: &UserInputBound,
641    ) -> Result<Bound<Term>, QueryParserError> {
642        if bound.term_str() == "*" {
643            return Ok(Bound::Unbounded);
644        }
645        let term = self.compute_boundary_term(field, json_path, bound.term_str())?;
646        match *bound {
647            UserInputBound::Inclusive(_) => Ok(Bound::Included(term)),
648            UserInputBound::Exclusive(_) => Ok(Bound::Excluded(term)),
649            UserInputBound::Unbounded => Ok(Bound::Unbounded),
650        }
651    }
652
653    fn compute_logical_ast_with_occur_lenient(
654        &self,
655        user_input_ast: UserInputAst,
656    ) -> (LogicalAst, Vec<QueryParserError>) {
657        match user_input_ast {
658            UserInputAst::Clause(sub_queries) => {
659                let default_occur = self.default_occur();
660                let mut logical_sub_queries: Vec<(Occur, LogicalAst)> = Vec::new();
661                let mut errors = Vec::new();
662                for (occur_opt, sub_ast) in sub_queries {
663                    let (sub_ast, mut sub_errors) =
664                        self.compute_logical_ast_with_occur_lenient(sub_ast);
665                    let occur = occur_opt.unwrap_or(default_occur);
666                    logical_sub_queries.push((occur, sub_ast));
667                    errors.append(&mut sub_errors);
668                }
669                (LogicalAst::Clause(logical_sub_queries), errors)
670            }
671            UserInputAst::Boost(ast, boost) => {
672                let (ast, errors) = self.compute_logical_ast_with_occur_lenient(*ast);
673                (ast.boost(boost as Score), errors)
674            }
675            UserInputAst::Leaf(leaf) => {
676                let (ast, errors) = self.compute_logical_ast_from_leaf_lenient(*leaf);
677                // if the error is not recoverable, replace it with an empty clause. We will end up
678                // trimming those later
679                (
680                    ast.unwrap_or_else(|| LogicalAst::Clause(Vec::new())),
681                    errors,
682                )
683            }
684        }
685    }
686
687    fn field_boost(&self, field: Field) -> Score {
688        self.boost.get(&field).cloned().unwrap_or(1.0)
689    }
690
691    fn default_indexed_json_fields(&self) -> impl Iterator<Item = Field> + '_ {
692        let schema = self.schema.clone();
693        self.default_fields.iter().cloned().filter(move |field| {
694            let field_type = schema.get_field_entry(*field).field_type();
695            field_type.value_type() == Type::Json && field_type.is_indexed()
696        })
697    }
698
699    /// Given a literal, returns the list of terms that should be searched.
700    ///
701    /// The terms are identified by a triplet:
702    /// - tantivy field
703    /// - field_path: tantivy has JSON fields. It is possible to target a member of a JSON object by
704    ///   naturally extending the json field name with a "." separated field_path
705    /// - field_phrase: the phrase that is being searched.
706    ///
707    /// The literal identifies the targeted field by a so-called *full field path*,
708    /// specified before the ":". (e.g. identity.username:fulmicoton).
709    ///
710    /// The way we split the full field path into (field_name, field_path) can be ambiguous,
711    /// because field_names can contain "." themselves.
712    // For instance if a field is named `one.two` and another one is named `one`,
713    /// should `one.two:three` target `one.two` with field path `` or or `one` with
714    /// the field path `two`.
715    ///
716    /// In this case tantivy, just picks the solution with the longest field name.
717    ///
718    /// Quirk: As a hack for quickwit, we do not split over a dot that appear escaped '\.'.
719    fn compute_path_triplets_for_literal<'a>(
720        &self,
721        literal: &'a UserInputLiteral,
722    ) -> Result<Vec<(Field, &'a str, &'a str)>, QueryParserError> {
723        let full_path = if let Some(full_path) = &literal.field_name {
724            full_path
725        } else {
726            // The user did not specify any path...
727            // We simply target default fields.
728            if self.default_fields.is_empty() {
729                return Err(QueryParserError::NoDefaultFieldDeclared);
730            }
731            return Ok(self
732                .default_fields
733                .iter()
734                .map(|default_field| (*default_field, "", literal.phrase.as_str()))
735                .collect::<Vec<(Field, &str, &str)>>());
736        };
737        if let Some((field, path)) = self.split_full_path(full_path) {
738            return Ok(vec![(field, path, literal.phrase.as_str())]);
739        }
740        // We need to add terms associated with json default fields.
741        let triplets: Vec<(Field, &str, &str)> = self
742            .default_indexed_json_fields()
743            .map(|json_field| (json_field, full_path.as_str(), literal.phrase.as_str()))
744            .collect();
745        if triplets.is_empty() {
746            return Err(QueryParserError::FieldDoesNotExist(full_path.to_string()));
747        }
748        Ok(triplets)
749    }
750
751    fn compute_logical_ast_from_leaf_lenient(
752        &self,
753        leaf: UserInputLeaf,
754    ) -> (Option<LogicalAst>, Vec<QueryParserError>) {
755        match leaf {
756            UserInputLeaf::Literal(literal) => {
757                let term_phrases: Vec<(Field, &str, &str)> =
758                    try_tuple!(self.compute_path_triplets_for_literal(&literal));
759                let mut asts: Vec<LogicalAst> = Vec::new();
760                let mut errors: Vec<QueryParserError> = Vec::new();
761                for (field, json_path, phrase) in term_phrases {
762                    let unboosted_asts = match self.compute_logical_ast_for_leaf(
763                        field,
764                        json_path,
765                        phrase,
766                        literal.slop,
767                        literal.prefix,
768                    ) {
769                        Ok(asts) => asts,
770                        Err(e) => {
771                            errors.push(e);
772                            continue;
773                        }
774                    };
775                    for ast in unboosted_asts {
776                        // Apply some field specific boost defined at the query parser level.
777                        let boost = self.field_boost(field);
778                        asts.push(LogicalAst::Leaf(Box::new(ast)).boost(boost));
779                    }
780                }
781                if !asts.is_empty() {
782                    // if some fields failed but other succeeded, we consider this a success, it
783                    // probably means the default_fields contains
784                    // text and non-text fields, and the non-text ones failed
785                    errors.clear();
786                }
787                let result_ast: LogicalAst = if asts.len() == 1 {
788                    asts.into_iter().next().unwrap()
789                } else {
790                    LogicalAst::Clause(asts.into_iter().map(|ast| (Occur::Should, ast)).collect())
791                };
792                (Some(result_ast), errors)
793            }
794            UserInputLeaf::All => (
795                Some(LogicalAst::Leaf(Box::new(LogicalLiteral::All))),
796                Vec::new(),
797            ),
798            UserInputLeaf::Range {
799                field: full_field_opt,
800                lower,
801                upper,
802            } => {
803                let Some(full_path) = full_field_opt else {
804                    return (
805                        None,
806                        vec![QueryParserError::UnsupportedQuery(
807                            "Range query need to target a specific field.".to_string(),
808                        )],
809                    );
810                };
811                let (field, json_path) = try_tuple!(self
812                    .split_full_path(&full_path)
813                    .ok_or_else(|| QueryParserError::FieldDoesNotExist(full_path.clone())));
814                let mut errors = Vec::new();
815                let lower = match self.resolve_bound(field, json_path, &lower) {
816                    Ok(bound) => bound,
817                    Err(error) => {
818                        errors.push(error);
819                        Bound::Unbounded
820                    }
821                };
822                let upper = match self.resolve_bound(field, json_path, &upper) {
823                    Ok(bound) => bound,
824                    Err(error) => {
825                        errors.push(error);
826                        Bound::Unbounded
827                    }
828                };
829                if lower == Bound::Unbounded && upper == Bound::Unbounded {
830                    // this range is useless, either because a user requested [* TO *], or because
831                    // we failed to parse something. Either way, there is no point emitting it
832                    return (None, errors);
833                }
834                let logical_ast =
835                    LogicalAst::Leaf(Box::new(LogicalLiteral::Range { lower, upper }));
836                (Some(logical_ast), errors)
837            }
838            UserInputLeaf::Set {
839                field: full_field_opt,
840                elements,
841            } => {
842                let full_path = try_tuple!(full_field_opt.ok_or_else(|| {
843                    QueryParserError::UnsupportedQuery(
844                        "Range query need to target a specific field.".to_string(),
845                    )
846                }));
847                let (field, json_path) = try_tuple!(self
848                    .split_full_path(&full_path)
849                    .ok_or_else(|| QueryParserError::FieldDoesNotExist(full_path.clone())));
850                let (elements, errors) = elements
851                    .into_iter()
852                    .map(|element| self.compute_boundary_term(field, json_path, &element))
853                    .partition_result();
854                let logical_ast = LogicalAst::Leaf(Box::new(LogicalLiteral::Set { elements }));
855                (Some(logical_ast), errors)
856            }
857            UserInputLeaf::Exists { .. } => (
858                None,
859                vec![QueryParserError::UnsupportedQuery(
860                    "Range query need to target a specific field.".to_string(),
861                )],
862            ),
863        }
864    }
865}
866
867fn convert_literal_to_query(
868    fuzzy: &FxHashMap<Field, Fuzzy>,
869    logical_literal: LogicalLiteral,
870) -> Box<dyn Query> {
871    match logical_literal {
872        LogicalLiteral::Term(term) => {
873            if let Some(fuzzy) = fuzzy.get(&term.field()) {
874                if fuzzy.prefix {
875                    Box::new(FuzzyTermQuery::new_prefix(
876                        term,
877                        fuzzy.distance,
878                        fuzzy.transpose_cost_one,
879                    ))
880                } else {
881                    Box::new(FuzzyTermQuery::new(
882                        term,
883                        fuzzy.distance,
884                        fuzzy.transpose_cost_one,
885                    ))
886                }
887            } else {
888                Box::new(TermQuery::new(term, IndexRecordOption::WithFreqs))
889            }
890        }
891        LogicalLiteral::Phrase {
892            terms,
893            slop,
894            prefix,
895        } => {
896            if prefix {
897                Box::new(PhrasePrefixQuery::new_with_offset(terms))
898            } else {
899                Box::new(PhraseQuery::new_with_offset_and_slop(terms, slop))
900            }
901        }
902        LogicalLiteral::Range { lower, upper } => Box::new(RangeQuery::new(lower, upper)),
903        LogicalLiteral::Set { elements, .. } => Box::new(TermSetQuery::new(elements)),
904        LogicalLiteral::All => Box::new(AllQuery),
905    }
906}
907
908fn generate_literals_for_str(
909    field_name: &str,
910    field: Field,
911    phrase: &str,
912    slop: u32,
913    prefix: bool,
914    indexing_options: &TextFieldIndexing,
915    text_analyzer: &mut TextAnalyzer,
916) -> Result<Option<LogicalLiteral>, QueryParserError> {
917    let mut terms: Vec<(usize, Term)> = Vec::new();
918    let mut token_stream = text_analyzer.token_stream(phrase);
919    token_stream.process(&mut |token| {
920        let term = Term::from_field_text(field, &token.text);
921        terms.push((token.position, term));
922    });
923    if terms.len() <= 1 {
924        if prefix {
925            return Err(QueryParserError::PhrasePrefixRequiresAtLeastTwoTerms {
926                phrase: phrase.to_owned(),
927                tokenizer: indexing_options.tokenizer().to_owned(),
928            });
929        }
930        let term_literal_opt = terms
931            .into_iter()
932            .next()
933            .map(|(_, term)| LogicalLiteral::Term(term));
934        return Ok(term_literal_opt);
935    }
936    if !indexing_options.index_option().has_positions() {
937        return Err(QueryParserError::FieldDoesNotHavePositionsIndexed(
938            field_name.to_string(),
939        ));
940    }
941    Ok(Some(LogicalLiteral::Phrase {
942        terms,
943        slop,
944        prefix,
945    }))
946}
947
948fn generate_literals_for_json_object(
949    field_name: &str,
950    field: Field,
951    json_path: &str,
952    phrase: &str,
953    tokenizer_manager: &TokenizerManager,
954    json_options: &JsonObjectOptions,
955) -> Result<Vec<LogicalLiteral>, QueryParserError> {
956    let text_options = json_options.get_text_indexing_options().ok_or_else(|| {
957        // This should have been seen earlier really.
958        QueryParserError::FieldNotIndexed(field_name.to_string())
959    })?;
960    let mut text_analyzer = tokenizer_manager
961        .get(text_options.tokenizer())
962        .ok_or_else(|| QueryParserError::UnknownTokenizer {
963            field: field_name.to_string(),
964            tokenizer: text_options.tokenizer().to_string(),
965        })?;
966    let index_record_option = text_options.index_option();
967    let mut logical_literals = Vec::new();
968
969    let get_term_with_path =
970        || Term::from_field_json_path(field, json_path, json_options.is_expand_dots_enabled());
971
972    // Try to convert the phrase to a fast value
973    if let Some(term) =
974        convert_to_fast_value_and_append_to_json_term(get_term_with_path(), phrase, true)
975    {
976        logical_literals.push(LogicalLiteral::Term(term));
977    }
978
979    // Try to tokenize the phrase and create Terms.
980    let mut positions_and_terms = Vec::<(usize, Term)>::new();
981    let mut token_stream = text_analyzer.token_stream(phrase);
982    token_stream.process(&mut |token| {
983        let mut term = get_term_with_path();
984        term.append_type_and_str(&token.text);
985        positions_and_terms.push((token.position, term.clone()));
986    });
987
988    if positions_and_terms.len() <= 1 {
989        for (_, term) in positions_and_terms {
990            logical_literals.push(LogicalLiteral::Term(term));
991        }
992        return Ok(logical_literals);
993    }
994    if !index_record_option.has_positions() {
995        return Err(QueryParserError::FieldDoesNotHavePositionsIndexed(
996            field_name.to_string(),
997        ));
998    }
999    logical_literals.push(LogicalLiteral::Phrase {
1000        terms: positions_and_terms,
1001        slop: 0,
1002        prefix: false,
1003    });
1004    Ok(logical_literals)
1005}
1006
1007fn convert_to_query(fuzzy: &FxHashMap<Field, Fuzzy>, logical_ast: LogicalAst) -> Box<dyn Query> {
1008    match trim_ast(logical_ast) {
1009        Some(LogicalAst::Clause(trimmed_clause)) => {
1010            let occur_subqueries = trimmed_clause
1011                .into_iter()
1012                .map(|(occur, subquery)| (occur, convert_to_query(fuzzy, subquery)))
1013                .collect::<Vec<_>>();
1014            assert!(
1015                !occur_subqueries.is_empty(),
1016                "Should not be empty after trimming"
1017            );
1018            Box::new(BooleanQuery::new(occur_subqueries))
1019        }
1020        Some(LogicalAst::Leaf(trimmed_logical_literal)) => {
1021            convert_literal_to_query(fuzzy, *trimmed_logical_literal)
1022        }
1023        Some(LogicalAst::Boost(ast, boost)) => {
1024            let query = convert_to_query(fuzzy, *ast);
1025            let boosted_query = BoostQuery::new(query, boost);
1026            Box::new(boosted_query)
1027        }
1028        None => Box::new(EmptyQuery),
1029    }
1030}
1031
1032#[cfg(test)]
1033mod test {
1034    use matches::assert_matches;
1035
1036    use super::super::logical_ast::*;
1037    use super::{QueryParser, QueryParserError};
1038    use crate::query::Query;
1039    use crate::schema::{
1040        FacetOptions, Field, IndexRecordOption, Schema, Term, TextFieldIndexing, TextOptions, FAST,
1041        INDEXED, STORED, STRING, TEXT,
1042    };
1043    use crate::tokenizer::{
1044        LowerCaser, SimpleTokenizer, StopWordFilter, TextAnalyzer, TokenizerManager,
1045    };
1046    use crate::Index;
1047
1048    fn make_schema() -> Schema {
1049        let mut schema_builder = Schema::builder();
1050        let text_field_indexing = TextFieldIndexing::default()
1051            .set_tokenizer("en_with_stop_words")
1052            .set_index_option(IndexRecordOption::WithFreqsAndPositions);
1053        let text_options = TextOptions::default()
1054            .set_indexing_options(text_field_indexing)
1055            .set_stored();
1056        schema_builder.add_text_field("title", TEXT);
1057        schema_builder.add_text_field("text", TEXT);
1058        schema_builder.add_i64_field("signed", INDEXED);
1059        schema_builder.add_u64_field("unsigned", INDEXED);
1060        schema_builder.add_text_field("notindexed_text", STORED);
1061        schema_builder.add_text_field("notindexed_u64", STORED);
1062        schema_builder.add_text_field("notindexed_i64", STORED);
1063        schema_builder.add_text_field("nottokenized", STRING);
1064        schema_builder.add_text_field("with_stop_words", text_options);
1065        schema_builder.add_date_field("date", INDEXED);
1066        schema_builder.add_f64_field("float", INDEXED);
1067        schema_builder.add_facet_field("facet", FacetOptions::default());
1068        schema_builder.add_bytes_field("bytes", INDEXED);
1069        schema_builder.add_bytes_field("bytes_not_indexed", STORED);
1070        schema_builder.add_json_field("json", TEXT);
1071        schema_builder.add_json_field("json_not_indexed", STORED);
1072        schema_builder.add_bool_field("bool", INDEXED);
1073        schema_builder.add_bool_field("notindexed_bool", STORED);
1074        schema_builder.add_u64_field("u64_ff", FAST);
1075        schema_builder.build()
1076    }
1077
1078    fn make_query_parser_with_default_fields(default_fields: &[&'static str]) -> QueryParser {
1079        let schema = make_schema();
1080        let default_fields: Vec<Field> = default_fields
1081            .iter()
1082            .flat_map(|field_name| schema.get_field(field_name))
1083            .collect();
1084        let tokenizer_manager = TokenizerManager::default();
1085        tokenizer_manager.register(
1086            "en_with_stop_words",
1087            TextAnalyzer::builder(SimpleTokenizer::default())
1088                .filter(LowerCaser)
1089                .filter(StopWordFilter::remove(vec!["the".to_string()]))
1090                .build(),
1091        );
1092        QueryParser::new(schema, default_fields, tokenizer_manager)
1093    }
1094
1095    fn make_query_parser() -> QueryParser {
1096        make_query_parser_with_default_fields(&["title", "text"])
1097    }
1098
1099    fn parse_query_to_logical_ast_with_default_fields(
1100        query: &str,
1101        default_conjunction: bool,
1102        default_fields: &[&'static str],
1103    ) -> Result<LogicalAst, QueryParserError> {
1104        let mut query_parser = make_query_parser_with_default_fields(default_fields);
1105        if default_conjunction {
1106            query_parser.set_conjunction_by_default();
1107        }
1108        query_parser.parse_query_to_logical_ast(query)
1109    }
1110
1111    fn parse_query_to_logical_ast(
1112        query: &str,
1113        default_conjunction: bool,
1114    ) -> Result<LogicalAst, QueryParserError> {
1115        parse_query_to_logical_ast_with_default_fields(
1116            query,
1117            default_conjunction,
1118            &["title", "text"],
1119        )
1120    }
1121
1122    #[track_caller]
1123    fn test_parse_query_to_logical_ast_helper_with_default_fields(
1124        query: &str,
1125        expected: &str,
1126        default_conjunction: bool,
1127        default_fields: &[&'static str],
1128    ) {
1129        let query = parse_query_to_logical_ast_with_default_fields(
1130            query,
1131            default_conjunction,
1132            default_fields,
1133        )
1134        .unwrap();
1135        let query_str = format!("{query:?}");
1136        assert_eq!(query_str, expected);
1137    }
1138
1139    #[track_caller]
1140    fn test_parse_query_to_logical_ast_helper(
1141        query: &str,
1142        expected: &str,
1143        default_conjunction: bool,
1144    ) {
1145        test_parse_query_to_logical_ast_helper_with_default_fields(
1146            query,
1147            expected,
1148            default_conjunction,
1149            &["title", "text"],
1150        )
1151    }
1152
1153    #[test]
1154    pub fn test_parse_query_facet() {
1155        let query_parser = make_query_parser();
1156        let query = query_parser.parse_query("facet:/root/branch/leaf").unwrap();
1157        assert_eq!(
1158            format!("{query:?}"),
1159            r#"TermQuery(Term(field=11, type=Facet, Facet(/root/branch/leaf)))"#
1160        );
1161    }
1162
1163    #[test]
1164    pub fn test_parse_query_with_boost() {
1165        let mut query_parser = make_query_parser();
1166        let schema = make_schema();
1167        let text_field = schema.get_field("text").unwrap();
1168        query_parser.set_field_boost(text_field, 2.0);
1169        let query = query_parser.parse_query("text:hello").unwrap();
1170        assert_eq!(
1171            format!("{query:?}"),
1172            r#"Boost(query=TermQuery(Term(field=1, type=Str, "hello")), boost=2)"#
1173        );
1174    }
1175
1176    #[test]
1177    pub fn test_parse_query_range_with_boost() {
1178        let query = make_query_parser().parse_query("title:[A TO B]").unwrap();
1179        assert_eq!(
1180            format!("{query:?}"),
1181            "RangeQuery { bounds: BoundsRange { lower_bound: Included(Term(field=0, type=Str, \
1182             \"a\")), upper_bound: Included(Term(field=0, type=Str, \"b\")) } }"
1183        );
1184    }
1185
1186    #[test]
1187    pub fn test_parse_query_with_default_boost_and_custom_boost() {
1188        let mut query_parser = make_query_parser();
1189        let schema = make_schema();
1190        let text_field = schema.get_field("text").unwrap();
1191        query_parser.set_field_boost(text_field, 2.0);
1192        let query = query_parser.parse_query("text:hello^2").unwrap();
1193        assert_eq!(
1194            format!("{query:?}"),
1195            r#"Boost(query=Boost(query=TermQuery(Term(field=1, type=Str, "hello")), boost=2), boost=2)"#
1196        );
1197    }
1198
1199    #[test]
1200    pub fn test_parse_nonindexed_field_yields_error() {
1201        let query_parser = make_query_parser();
1202
1203        let is_not_indexed_err = |query: &str| {
1204            let result: Result<Box<dyn Query>, QueryParserError> = query_parser.parse_query(query);
1205            if let Err(QueryParserError::FieldNotIndexed(field_name)) = result {
1206                Some(field_name)
1207            } else {
1208                None
1209            }
1210        };
1211
1212        assert_eq!(
1213            is_not_indexed_err("notindexed_text:titi"),
1214            Some(String::from("notindexed_text"))
1215        );
1216        assert_eq!(
1217            is_not_indexed_err("notindexed_u64:23424"),
1218            Some(String::from("notindexed_u64"))
1219        );
1220        assert_eq!(
1221            is_not_indexed_err("notindexed_i64:-234324"),
1222            Some(String::from("notindexed_i64"))
1223        );
1224        assert_eq!(
1225            is_not_indexed_err("notindexed_bool:true"),
1226            Some(String::from("notindexed_bool"))
1227        );
1228    }
1229
1230    #[test]
1231    pub fn test_parse_query_untokenized() {
1232        test_parse_query_to_logical_ast_helper(
1233            "nottokenized:\"wordone wordtwo\"",
1234            r#"Term(field=7, type=Str, "wordone wordtwo")"#,
1235            false,
1236        );
1237    }
1238
1239    #[test]
1240    pub fn test_parse_query_empty() {
1241        test_parse_query_to_logical_ast_helper("", "<emptyclause>", false);
1242        test_parse_query_to_logical_ast_helper(" ", "<emptyclause>", false);
1243        let query_parser = make_query_parser();
1244        let query_result = query_parser.parse_query("");
1245        let query = query_result.unwrap();
1246        assert_eq!(format!("{query:?}"), "EmptyQuery");
1247    }
1248
1249    #[test]
1250    pub fn test_parse_query_ints() {
1251        let query_parser = make_query_parser();
1252        assert!(query_parser.parse_query("signed:2324").is_ok());
1253        assert!(query_parser.parse_query("signed:\"22\"").is_ok());
1254        assert!(query_parser.parse_query("signed:\"-2234\"").is_ok());
1255        assert!(query_parser
1256            .parse_query("signed:\"-9999999999999\"")
1257            .is_ok());
1258        assert!(query_parser.parse_query("signed:\"a\"").is_err());
1259        assert!(query_parser.parse_query("signed:\"2a\"").is_err());
1260        assert!(query_parser
1261            .parse_query("signed:\"18446744073709551615\"")
1262            .is_err());
1263        assert!(query_parser.parse_query("unsigned:\"2\"").is_ok());
1264        assert!(query_parser.parse_query("unsigned:\"-2\"").is_err());
1265        assert!(query_parser
1266            .parse_query("unsigned:\"18446744073709551615\"")
1267            .is_ok());
1268        assert!(query_parser.parse_query("float:\"3.1\"").is_ok());
1269        assert!(query_parser.parse_query("float:\"-2.4\"").is_ok());
1270        assert!(query_parser.parse_query("float:\"2.1.2\"").is_err());
1271        assert!(query_parser.parse_query("float:\"2.1a\"").is_err());
1272        assert!(query_parser
1273            .parse_query("float:\"18446744073709551615.0\"")
1274            .is_ok());
1275        test_parse_query_to_logical_ast_helper(
1276            "unsigned:2324",
1277            "Term(field=3, type=U64, 2324)",
1278            false,
1279        );
1280
1281        test_parse_query_to_logical_ast_helper(
1282            "signed:-2324",
1283            &format!(
1284                "{:?}",
1285                Term::from_field_i64(Field::from_field_id(2u32), -2324)
1286            ),
1287            false,
1288        );
1289
1290        test_parse_query_to_logical_ast_helper(
1291            "float:2.5",
1292            &format!(
1293                "{:?}",
1294                Term::from_field_f64(Field::from_field_id(10u32), 2.5)
1295            ),
1296            false,
1297        );
1298    }
1299
1300    #[test]
1301    fn test_parse_bytes() {
1302        test_parse_query_to_logical_ast_helper(
1303            "bytes:YnVidQ==",
1304            "Term(field=12, type=Bytes, [98, 117, 98, 117])",
1305            false,
1306        );
1307    }
1308
1309    #[test]
1310    fn test_parse_bool() {
1311        test_parse_query_to_logical_ast_helper(
1312            "bool:true",
1313            &format!(
1314                "{:?}",
1315                Term::from_field_bool(Field::from_field_id(16u32), true),
1316            ),
1317            false,
1318        );
1319    }
1320
1321    #[test]
1322    fn test_parse_bytes_not_indexed() {
1323        let error = parse_query_to_logical_ast("bytes_not_indexed:aaa", false).unwrap_err();
1324        assert!(matches!(error, QueryParserError::FieldNotIndexed(_)));
1325    }
1326
1327    #[test]
1328    fn test_json_field() {
1329        test_parse_query_to_logical_ast_helper(
1330            "json.titi:hello",
1331            "Term(field=14, type=Json, path=titi, type=Str, \"hello\")",
1332            false,
1333        );
1334    }
1335
1336    fn extract_query_term_json_path(query: &str) -> String {
1337        let LogicalAst::Leaf(literal) = parse_query_to_logical_ast(query, false).unwrap() else {
1338            panic!();
1339        };
1340        let LogicalLiteral::Term(term) = *literal else {
1341            panic!();
1342        };
1343        std::str::from_utf8(term.serialized_value_bytes())
1344            .unwrap()
1345            .to_string()
1346    }
1347
1348    #[test]
1349    fn test_json_field_query_with_escaped_dot() {
1350        assert_eq!(
1351            extract_query_term_json_path(r#"json.k8s.node.name:hello"#),
1352            "k8s\u{1}node\u{1}name\0shello"
1353        );
1354        assert_eq!(
1355            extract_query_term_json_path(r"json.k8s\.node\.name:hello"),
1356            "k8s.node.name\0shello"
1357        );
1358    }
1359
1360    #[test]
1361    fn test_json_field_possibly_a_number() {
1362        test_parse_query_to_logical_ast_helper(
1363            "json.titi:5",
1364            r#"(Term(field=14, type=Json, path=titi, type=I64, 5) Term(field=14, type=Json, path=titi, type=Str, "5"))"#,
1365            true,
1366        );
1367        test_parse_query_to_logical_ast_helper(
1368            "json.titi:-5",
1369            r#"(Term(field=14, type=Json, path=titi, type=I64, -5) Term(field=14, type=Json, path=titi, type=Str, "5"))"#, //< Yes this is a bit weird after going through the tokenizer we lose the "-".
1370            true,
1371        );
1372        test_parse_query_to_logical_ast_helper(
1373            "json.titi:10000000000000000000",
1374            r#"(Term(field=14, type=Json, path=titi, type=U64, 10000000000000000000) Term(field=14, type=Json, path=titi, type=Str, "10000000000000000000"))"#,
1375            true,
1376        );
1377        test_parse_query_to_logical_ast_helper(
1378            "json.titi:-5.2",
1379            r#"(Term(field=14, type=Json, path=titi, type=F64, -5.2) "[(0, Term(field=14, type=Json, path=titi, type=Str, "5")), (1, Term(field=14, type=Json, path=titi, type=Str, "2"))]")"#,
1380            true,
1381        );
1382    }
1383
1384    #[test]
1385    fn test_json_field_possibly_a_date() {
1386        test_parse_query_to_logical_ast_helper(
1387            r#"json.date:"2019-10-12T07:20:50.52Z""#,
1388            r#"(Term(field=14, type=Json, path=date, type=Date, 2019-10-12T07:20:50Z) "[(0, Term(field=14, type=Json, path=date, type=Str, "2019")), (1, Term(field=14, type=Json, path=date, type=Str, "10")), (2, Term(field=14, type=Json, path=date, type=Str, "12t07")), (3, Term(field=14, type=Json, path=date, type=Str, "20")), (4, Term(field=14, type=Json, path=date, type=Str, "50")), (5, Term(field=14, type=Json, path=date, type=Str, "52z"))]")"#,
1389            true,
1390        );
1391    }
1392
1393    #[test]
1394    fn test_json_field_possibly_a_bool() {
1395        test_parse_query_to_logical_ast_helper(
1396            "json.titi:true",
1397            r#"(Term(field=14, type=Json, path=titi, type=Bool, true) Term(field=14, type=Json, path=titi, type=Str, "true"))"#,
1398            true,
1399        );
1400    }
1401
1402    #[test]
1403    fn test_json_field_not_indexed() {
1404        let error = parse_query_to_logical_ast("json_not_indexed.titi:hello", false).unwrap_err();
1405        assert!(matches!(error, QueryParserError::FieldNotIndexed(_)));
1406    }
1407
1408    fn test_query_to_logical_ast_with_default_json(
1409        query: &str,
1410        expected: &str,
1411        default_conjunction: bool,
1412    ) {
1413        let mut query_parser = make_query_parser_with_default_fields(&["json"]);
1414        if default_conjunction {
1415            query_parser.set_conjunction_by_default();
1416        }
1417        let ast = query_parser.parse_query_to_logical_ast(query).unwrap();
1418        let ast_str = format!("{ast:?}");
1419        assert_eq!(ast_str, expected);
1420    }
1421
1422    #[test]
1423    fn test_json_default() {
1424        test_query_to_logical_ast_with_default_json(
1425            "titi:4",
1426            "(Term(field=14, type=Json, path=titi, type=I64, 4) Term(field=14, type=Json, \
1427             path=titi, type=Str, \"4\"))",
1428            false,
1429        );
1430    }
1431
1432    #[test]
1433    fn test_json_default_with_different_field() {
1434        for conjunction in [false, true] {
1435            test_query_to_logical_ast_with_default_json(
1436                "text:4",
1437                r#"Term(field=1, type=Str, "4")"#,
1438                conjunction,
1439            );
1440        }
1441    }
1442
1443    #[test]
1444    fn test_json_default_with_same_field() {
1445        for conjunction in [false, true] {
1446            test_query_to_logical_ast_with_default_json(
1447                "json:4",
1448                r#"(Term(field=14, type=Json, path=, type=I64, 4) Term(field=14, type=Json, path=, type=Str, "4"))"#,
1449                conjunction,
1450            );
1451        }
1452    }
1453
1454    #[test]
1455    fn test_parse_bytes_phrase() {
1456        test_parse_query_to_logical_ast_helper(
1457            "bytes:\"YnVidQ==\"",
1458            "Term(field=12, type=Bytes, [98, 117, 98, 117])",
1459            false,
1460        );
1461    }
1462
1463    #[test]
1464    fn test_parse_bytes_invalid_base64() {
1465        let base64_err: QueryParserError =
1466            parse_query_to_logical_ast("bytes:aa", false).unwrap_err();
1467        assert!(matches!(base64_err, QueryParserError::ExpectedBase64(_)));
1468    }
1469
1470    #[test]
1471    fn test_parse_query_to_ast_ab_c() {
1472        test_parse_query_to_logical_ast_helper(
1473            "(+title:a +title:b) title:c",
1474            r#"((+Term(field=0, type=Str, "a") +Term(field=0, type=Str, "b")) Term(field=0, type=Str, "c"))"#,
1475            false,
1476        );
1477        test_parse_query_to_logical_ast_helper(
1478            "(+title:a +title:b) title:c",
1479            r#"(+Term(field=0, type=Str, "a") +Term(field=0, type=Str, "b") +Term(field=0, type=Str, "c"))"#,
1480            true,
1481        );
1482    }
1483
1484    #[test]
1485    pub fn test_parse_query_to_ast_single_term() {
1486        test_parse_query_to_logical_ast_helper(
1487            "title:toto",
1488            r#"Term(field=0, type=Str, "toto")"#,
1489            false,
1490        );
1491        test_parse_query_to_logical_ast_helper(
1492            "+title:toto",
1493            r#"Term(field=0, type=Str, "toto")"#,
1494            false,
1495        );
1496        test_parse_query_to_logical_ast_helper(
1497            "+title:toto -titi",
1498            r#"(+Term(field=0, type=Str, "toto") -(Term(field=0, type=Str, "titi") Term(field=1, type=Str, "titi")))"#,
1499            false,
1500        );
1501    }
1502
1503    #[test]
1504    fn test_single_negative_term() {
1505        assert_matches!(
1506            parse_query_to_logical_ast("-title:toto", false),
1507            Err(QueryParserError::AllButQueryForbidden)
1508        );
1509    }
1510
1511    #[test]
1512    pub fn test_parse_query_to_ast_two_terms() {
1513        test_parse_query_to_logical_ast_helper(
1514            "title:a b",
1515            r#"(Term(field=0, type=Str, "a") Term(field=0, type=Str, "b") Term(field=1, type=Str, "b"))"#,
1516            false,
1517        );
1518        test_parse_query_to_logical_ast_helper(
1519            r#"title:"a b""#,
1520            r#""[(0, Term(field=0, type=Str, "a")), (1, Term(field=0, type=Str, "b"))]""#,
1521            false,
1522        );
1523    }
1524    #[test]
1525    pub fn test_parse_query_all_query() {
1526        let logical_ast = parse_query_to_logical_ast("*", false).unwrap();
1527        assert_eq!(format!("{logical_ast:?}"), "*");
1528    }
1529
1530    #[test]
1531    pub fn test_parse_query_range_require_a_target_field() {
1532        let query_parser_error = parse_query_to_logical_ast("[A TO B]", false).err().unwrap();
1533        assert_eq!(
1534            query_parser_error.to_string(),
1535            "Unsupported query: Range query need to target a specific field."
1536        );
1537    }
1538
1539    #[test]
1540    pub fn test_parse_query_to_ast_ranges() {
1541        test_parse_query_to_logical_ast_helper(
1542            "title:[a TO b]",
1543            r#"(Included(Term(field=0, type=Str, "a")) TO Included(Term(field=0, type=Str, "b")))"#,
1544            false,
1545        );
1546        test_parse_query_to_logical_ast_helper(
1547            "title:{titi TO toto}",
1548            r#"(Excluded(Term(field=0, type=Str, "titi")) TO Excluded(Term(field=0, type=Str, "toto")))"#,
1549            false,
1550        );
1551        test_parse_query_to_logical_ast_helper(
1552            "title:{* TO toto}",
1553            r#"(Unbounded TO Excluded(Term(field=0, type=Str, "toto")))"#,
1554            false,
1555        );
1556        test_parse_query_to_logical_ast_helper(
1557            "title:{titi TO *}",
1558            r#"(Excluded(Term(field=0, type=Str, "titi")) TO Unbounded)"#,
1559            false,
1560        );
1561        test_parse_query_to_logical_ast_helper(
1562            "signed:{-5 TO 3}",
1563            r#"(Excluded(Term(field=2, type=I64, -5)) TO Excluded(Term(field=2, type=I64, 3)))"#,
1564            false,
1565        );
1566        test_parse_query_to_logical_ast_helper(
1567            "float:{-1.5 TO 1.5}",
1568            r#"(Excluded(Term(field=10, type=F64, -1.5)) TO Excluded(Term(field=10, type=F64, 1.5)))"#,
1569            false,
1570        );
1571        test_parse_query_to_logical_ast_helper(
1572            "u64_ff:[7 TO 77]",
1573            r#"(Included(Term(field=18, type=U64, 7)) TO Included(Term(field=18, type=U64, 77)))"#,
1574            false,
1575        );
1576    }
1577
1578    #[test]
1579    pub fn test_query_parser_field_does_not_exist() {
1580        let query_parser = make_query_parser();
1581        assert_eq!(
1582            query_parser
1583                .parse_query("boujou:\"18446744073709551615\"")
1584                .unwrap_err(),
1585            QueryParserError::FieldDoesNotExist("boujou".to_string())
1586        );
1587    }
1588
1589    #[test]
1590    pub fn test_query_parser_field_not_indexed() {
1591        let query_parser = make_query_parser();
1592        assert_matches!(
1593            query_parser.parse_query("notindexed_text:\"18446744073709551615\""),
1594            Err(QueryParserError::FieldNotIndexed(_))
1595        );
1596    }
1597
1598    #[test]
1599    pub fn test_unknown_tokenizer() {
1600        let mut schema_builder = Schema::builder();
1601        let text_field_indexing = TextFieldIndexing::default()
1602            .set_tokenizer("nonexistingtokenizer")
1603            .set_index_option(IndexRecordOption::Basic);
1604        let text_options = TextOptions::default().set_indexing_options(text_field_indexing);
1605        let title = schema_builder.add_text_field("title", text_options);
1606        let schema = schema_builder.build();
1607        let default_fields = vec![title];
1608        let tokenizer_manager = TokenizerManager::default();
1609        let query_parser = QueryParser::new(schema, default_fields, tokenizer_manager);
1610
1611        assert_matches!(
1612            query_parser.parse_query("title:\"happy tax payer\""),
1613            Err(QueryParserError::UnknownTokenizer { .. })
1614        );
1615    }
1616
1617    #[test]
1618    pub fn test_query_parser_no_positions() {
1619        let mut schema_builder = Schema::builder();
1620        let text_field_indexing = TextFieldIndexing::default()
1621            .set_tokenizer("customtokenizer")
1622            .set_index_option(IndexRecordOption::Basic);
1623        let text_options = TextOptions::default().set_indexing_options(text_field_indexing);
1624        let title = schema_builder.add_text_field("title", text_options);
1625        let schema = schema_builder.build();
1626        let index = Index::create_in_ram(schema);
1627        index
1628            .tokenizers()
1629            .register("customtokenizer", SimpleTokenizer::default());
1630        let query_parser = QueryParser::for_index(&index, vec![title]);
1631        assert_eq!(
1632            query_parser.parse_query("title:\"happy tax\"").unwrap_err(),
1633            QueryParserError::FieldDoesNotHavePositionsIndexed("title".to_string())
1634        );
1635    }
1636
1637    #[test]
1638    pub fn test_query_parser_expected_int() {
1639        let query_parser = make_query_parser();
1640        assert_matches!(
1641            query_parser.parse_query("unsigned:18a"),
1642            Err(QueryParserError::ExpectedInt(_))
1643        );
1644        assert!(query_parser.parse_query("unsigned:\"18\"").is_ok());
1645        assert_matches!(
1646            query_parser.parse_query("signed:18b"),
1647            Err(QueryParserError::ExpectedInt(_))
1648        );
1649        assert!(query_parser.parse_query("float:\"1.8\"").is_ok());
1650        assert_matches!(
1651            query_parser.parse_query("float:1.8a"),
1652            Err(QueryParserError::ExpectedFloat(_))
1653        );
1654    }
1655
1656    #[test]
1657    pub fn test_query_parser_expected_bool() {
1658        let query_parser = make_query_parser();
1659        assert_matches!(
1660            query_parser.parse_query("bool:brie"),
1661            Err(QueryParserError::ExpectedBool(_))
1662        );
1663        assert!(query_parser.parse_query("bool:\"true\"").is_ok());
1664        assert!(query_parser.parse_query("bool:\"false\"").is_ok());
1665    }
1666
1667    #[test]
1668    pub fn test_query_parser_expected_date() {
1669        let query_parser = make_query_parser();
1670        assert_matches!(
1671            query_parser.parse_query("date:18a"),
1672            Err(QueryParserError::DateFormatError(_))
1673        );
1674        test_parse_query_to_logical_ast_helper(
1675            r#"date:"2010-11-21T09:55:06.000000000+02:00""#,
1676            r#"Term(field=9, type=Date, 2010-11-21T07:55:06Z)"#,
1677            true,
1678        );
1679        test_parse_query_to_logical_ast_helper(
1680            r#"date:"1985-04-12T23:20:50.52Z""#,
1681            r#"Term(field=9, type=Date, 1985-04-12T23:20:50Z)"#,
1682            true,
1683        );
1684    }
1685
1686    #[test]
1687    pub fn test_query_parser_expected_facet() {
1688        let query_parser = make_query_parser();
1689        match query_parser.parse_query("facet:INVALID") {
1690            Ok(_) => panic!("should never succeed"),
1691            Err(e) => assert_eq!(
1692                "The facet field is malformed: Failed to parse the facet string: 'INVALID'",
1693                format!("{e}")
1694            ),
1695        }
1696        assert!(query_parser.parse_query("facet:\"/foo/bar\"").is_ok());
1697    }
1698
1699    #[test]
1700    pub fn test_query_parser_not_empty_but_no_tokens() {
1701        let query_parser = make_query_parser();
1702        assert!(query_parser.parse_query(" !, ").is_ok());
1703        assert!(query_parser.parse_query("with_stop_words:the").is_ok());
1704    }
1705
1706    #[test]
1707    pub fn test_parse_query_single_negative_term_through_error() {
1708        assert_matches!(
1709            parse_query_to_logical_ast("-title:toto", true),
1710            Err(QueryParserError::AllButQueryForbidden)
1711        );
1712        assert_matches!(
1713            parse_query_to_logical_ast("-title:toto", false),
1714            Err(QueryParserError::AllButQueryForbidden)
1715        );
1716    }
1717
1718    #[test]
1719    pub fn test_parse_query_to_ast_conjunction() {
1720        test_parse_query_to_logical_ast_helper(
1721            "title:toto",
1722            r#"Term(field=0, type=Str, "toto")"#,
1723            true,
1724        );
1725        test_parse_query_to_logical_ast_helper(
1726            "+title:toto",
1727            r#"Term(field=0, type=Str, "toto")"#,
1728            true,
1729        );
1730        test_parse_query_to_logical_ast_helper(
1731            "+title:toto -titi",
1732            r#"(+Term(field=0, type=Str, "toto") -(Term(field=0, type=Str, "titi") Term(field=1, type=Str, "titi")))"#,
1733            true,
1734        );
1735        test_parse_query_to_logical_ast_helper(
1736            "title:a b",
1737            r#"(+Term(field=0, type=Str, "a") +(Term(field=0, type=Str, "b") Term(field=1, type=Str, "b")))"#,
1738            true,
1739        );
1740        test_parse_query_to_logical_ast_helper(
1741            "title:\"a b\"",
1742            r#""[(0, Term(field=0, type=Str, "a")), (1, Term(field=0, type=Str, "b"))]""#,
1743            true,
1744        );
1745    }
1746
1747    #[test]
1748    pub fn test_parse_query_negative() {
1749        test_parse_query_to_logical_ast_helper(
1750            "title:b -title:a",
1751            r#"(+Term(field=0, type=Str, "b") -Term(field=0, type=Str, "a"))"#,
1752            true,
1753        );
1754
1755        test_parse_query_to_logical_ast_helper(
1756            "title:b -(-title:a -title:c)",
1757            r#"(+Term(field=0, type=Str, "b") -(-Term(field=0, type=Str, "a") -Term(field=0, type=Str, "c")))"#,
1758            true,
1759        );
1760    }
1761
1762    #[test]
1763    pub fn test_query_parser_hyphen() {
1764        test_parse_query_to_logical_ast_helper(
1765            "title:www-form-encoded",
1766            r#""[(0, Term(field=0, type=Str, "www")), (1, Term(field=0, type=Str, "form")), (2, Term(field=0, type=Str, "encoded"))]""#,
1767            false,
1768        );
1769    }
1770
1771    #[test]
1772    fn test_and_default_regardless_of_default_conjunctive() {
1773        for &default_conjunction in &[false, true] {
1774            test_parse_query_to_logical_ast_helper(
1775                "title:a AND title:b",
1776                r#"(+Term(field=0, type=Str, "a") +Term(field=0, type=Str, "b"))"#,
1777                default_conjunction,
1778            );
1779        }
1780    }
1781
1782    #[test]
1783    fn test_or_default_conjunctive() {
1784        for &default_conjunction in &[false, true] {
1785            test_parse_query_to_logical_ast_helper(
1786                "title:a OR title:b",
1787                r#"(Term(field=0, type=Str, "a") Term(field=0, type=Str, "b"))"#,
1788                default_conjunction,
1789            );
1790        }
1791    }
1792
1793    #[test]
1794    fn test_escaped_field() {
1795        let mut schema_builder = Schema::builder();
1796        schema_builder.add_text_field(r"a\.b", STRING);
1797        let schema = schema_builder.build();
1798        let query_parser = QueryParser::new(schema, Vec::new(), TokenizerManager::default());
1799        let query = query_parser.parse_query(r"a\.b:hello").unwrap();
1800        assert_eq!(
1801            format!("{query:?}"),
1802            "TermQuery(Term(field=0, type=Str, \"hello\"))"
1803        );
1804    }
1805
1806    #[test]
1807    fn test_split_full_path() {
1808        let mut schema_builder = Schema::builder();
1809        schema_builder.add_text_field("second", STRING);
1810        schema_builder.add_text_field("first", STRING);
1811        schema_builder.add_text_field("first.toto", STRING);
1812        schema_builder.add_text_field("first.toto.titi", STRING);
1813        schema_builder.add_text_field("third.a.b.c", STRING);
1814        let schema = schema_builder.build();
1815        let query_parser =
1816            QueryParser::new(schema.clone(), Vec::new(), TokenizerManager::default());
1817        assert_eq!(
1818            query_parser.split_full_path("first.toto"),
1819            Some((schema.get_field("first.toto").unwrap(), ""))
1820        );
1821        assert_eq!(
1822            query_parser.split_full_path("first.toto.bubu"),
1823            Some((schema.get_field("first.toto").unwrap(), "bubu"))
1824        );
1825        assert_eq!(
1826            query_parser.split_full_path("first.toto.titi"),
1827            Some((schema.get_field("first.toto.titi").unwrap(), ""))
1828        );
1829        assert_eq!(
1830            query_parser.split_full_path("first.titi"),
1831            Some((schema.get_field("first").unwrap(), "titi"))
1832        );
1833        assert_eq!(query_parser.split_full_path("third"), None);
1834        assert_eq!(query_parser.split_full_path("hello.toto"), None);
1835        assert_eq!(query_parser.split_full_path(""), None);
1836        assert_eq!(query_parser.split_full_path("firsty"), None);
1837    }
1838
1839    #[test]
1840    pub fn test_phrase_slop() {
1841        test_parse_query_to_logical_ast_helper(
1842            "\"a b\"~0",
1843            r#"("[(0, Term(field=0, type=Str, "a")), (1, Term(field=0, type=Str, "b"))]" "[(0, Term(field=1, type=Str, "a")), (1, Term(field=1, type=Str, "b"))]")"#,
1844            false,
1845        );
1846        test_parse_query_to_logical_ast_helper(
1847            "\"a b\"~2",
1848            r#"("[(0, Term(field=0, type=Str, "a")), (1, Term(field=0, type=Str, "b"))]"~2 "[(0, Term(field=1, type=Str, "a")), (1, Term(field=1, type=Str, "b"))]"~2)"#,
1849            false,
1850        );
1851        test_parse_query_to_logical_ast_helper(
1852            "title:\"a b~4\"~2",
1853            r#""[(0, Term(field=0, type=Str, "a")), (1, Term(field=0, type=Str, "b")), (2, Term(field=0, type=Str, "4"))]"~2"#,
1854            false,
1855        );
1856    }
1857
1858    #[test]
1859    pub fn test_phrase_prefix() {
1860        test_parse_query_to_logical_ast_helper(
1861            "\"big bad wo\"*",
1862            r#"("[(0, Term(field=0, type=Str, "big")), (1, Term(field=0, type=Str, "bad")), (2, Term(field=0, type=Str, "wo"))]"* "[(0, Term(field=1, type=Str, "big")), (1, Term(field=1, type=Str, "bad")), (2, Term(field=1, type=Str, "wo"))]"*)"#,
1863            false,
1864        );
1865
1866        let query_parser = make_query_parser();
1867        let query = query_parser.parse_query("\"big bad wo\"*").unwrap();
1868        assert_eq!(
1869            format!("{query:?}"),
1870            "BooleanQuery { subqueries: [(Should, PhrasePrefixQuery { field: Field(0), \
1871             phrase_terms: [(0, Term(field=0, type=Str, \"big\")), (1, Term(field=0, type=Str, \
1872             \"bad\"))], prefix: (2, Term(field=0, type=Str, \"wo\")), max_expansions: 50 }), \
1873             (Should, PhrasePrefixQuery { field: Field(1), phrase_terms: [(0, Term(field=1, \
1874             type=Str, \"big\")), (1, Term(field=1, type=Str, \"bad\"))], prefix: (2, \
1875             Term(field=1, type=Str, \"wo\")), max_expansions: 50 })], \
1876             minimum_number_should_match: 1 }"
1877        );
1878    }
1879
1880    #[test]
1881    pub fn test_phrase_prefix_too_short() {
1882        let err = parse_query_to_logical_ast("\"wo\"*", true).unwrap_err();
1883        assert_eq!(
1884            err,
1885            QueryParserError::PhrasePrefixRequiresAtLeastTwoTerms {
1886                phrase: "wo".to_owned(),
1887                tokenizer: "default".to_owned()
1888            }
1889        );
1890
1891        let err = parse_query_to_logical_ast("\"\"*", true).unwrap_err();
1892        assert_eq!(
1893            err,
1894            QueryParserError::PhrasePrefixRequiresAtLeastTwoTerms {
1895                phrase: "".to_owned(),
1896                tokenizer: "default".to_owned()
1897            }
1898        );
1899    }
1900
1901    #[test]
1902    pub fn test_term_set_query() {
1903        test_parse_query_to_logical_ast_helper(
1904            "title: IN [a b cd]",
1905            r#"IN [Term(field=0, type=Str, "a"), Term(field=0, type=Str, "b"), Term(field=0, type=Str, "cd")]"#,
1906            false,
1907        );
1908        test_parse_query_to_logical_ast_helper(
1909            "bytes: IN [AA== ABA= ABCD]",
1910            r#"IN [Term(field=12, type=Bytes, [0]), Term(field=12, type=Bytes, [0, 16]), Term(field=12, type=Bytes, [0, 16, 131])]"#,
1911            false,
1912        );
1913        test_parse_query_to_logical_ast_helper(
1914            "signed: IN [1 2 -3]",
1915            r#"IN [Term(field=2, type=I64, 1), Term(field=2, type=I64, 2), Term(field=2, type=I64, -3)]"#,
1916            false,
1917        );
1918
1919        test_parse_query_to_logical_ast_helper(
1920            "float: IN [1.1 2.2 -3.3]",
1921            r#"IN [Term(field=10, type=F64, 1.1), Term(field=10, type=F64, 2.2), Term(field=10, type=F64, -3.3)]"#,
1922            false,
1923        );
1924    }
1925
1926    #[test]
1927    pub fn test_set_field_fuzzy() {
1928        {
1929            let mut query_parser = make_query_parser();
1930            query_parser.set_field_fuzzy(
1931                query_parser.schema.get_field("title").unwrap(),
1932                false,
1933                1,
1934                true,
1935            );
1936            let query = query_parser.parse_query("abc").unwrap();
1937            assert_eq!(
1938                format!("{query:?}"),
1939                "BooleanQuery { subqueries: [(Should, FuzzyTermQuery { term: Term(field=0, \
1940                 type=Str, \"abc\"), distance: 1, transposition_cost_one: true, prefix: false }), \
1941                 (Should, TermQuery(Term(field=1, type=Str, \"abc\")))], \
1942                 minimum_number_should_match: 1 }"
1943            );
1944        }
1945
1946        {
1947            let mut query_parser = make_query_parser();
1948            query_parser.set_field_fuzzy(
1949                query_parser.schema.get_field("text").unwrap(),
1950                true,
1951                2,
1952                false,
1953            );
1954            let query = query_parser.parse_query("abc").unwrap();
1955            assert_eq!(
1956                format!("{query:?}"),
1957                "BooleanQuery { subqueries: [(Should, TermQuery(Term(field=0, type=Str, \
1958                 \"abc\"))), (Should, FuzzyTermQuery { term: Term(field=1, type=Str, \"abc\"), \
1959                 distance: 2, transposition_cost_one: false, prefix: true })], \
1960                 minimum_number_should_match: 1 }"
1961            );
1962        }
1963    }
1964
1965    #[test]
1966    pub fn test_set_default_field_integer() {
1967        test_parse_query_to_logical_ast_helper_with_default_fields(
1968            "2324",
1969            "(Term(field=0, type=Str, \"2324\") Term(field=2, type=I64, 2324))",
1970            false,
1971            &["title", "signed"],
1972        );
1973
1974        test_parse_query_to_logical_ast_helper_with_default_fields(
1975            "abc",
1976            "Term(field=0, type=Str, \"abc\")",
1977            false,
1978            &["title", "signed"],
1979        );
1980
1981        let query_parser = make_query_parser_with_default_fields(&["signed"]);
1982        assert_matches!(
1983            query_parser.parse_query("abc"),
1984            Err(QueryParserError::ExpectedInt(_))
1985        );
1986    }
1987}