Skip to main content

tantivy/query/term_query/
term_query.rs

1use std::fmt;
2use std::ops::Bound;
3
4use super::term_weight::TermWeight;
5use crate::query::bm25::Bm25Weight;
6use crate::query::range_query::is_type_valid_for_fastfield_range_query;
7use crate::query::{EnableScoring, Explanation, Query, RangeQuery, Weight};
8use crate::schema::IndexRecordOption;
9use crate::Term;
10
11/// A Term query matches all of the documents
12/// containing a specific term.
13///
14/// The score associated is defined as
15/// `idf` *  sqrt(`term_freq` / `field norm`)
16/// in which :
17/// * `idf`        - inverse document frequency.
18/// * `term_freq`  - number of occurrences of the term in the field
19/// * `field norm` - number of tokens in the field.
20///
21/// ```rust
22/// use tantivy::collector::{Count, TopDocs};
23/// use tantivy::query::TermQuery;
24/// use tantivy::schema::{Schema, TEXT, IndexRecordOption};
25/// use tantivy::{doc, Index, IndexWriter, Term};
26/// # fn test() -> tantivy::Result<()> {
27/// let mut schema_builder = Schema::builder();
28/// let title = schema_builder.add_text_field("title", TEXT);
29/// let schema = schema_builder.build();
30/// let index = Index::create_in_ram(schema);
31/// {
32///     let mut index_writer: IndexWriter = index.writer(15_000_000)?;
33///     index_writer.add_document(doc!(
34///         title => "The Name of the Wind",
35///     ))?;
36///     index_writer.add_document(doc!(
37///         title => "The Diary of Muadib",
38///     ))?;
39///     index_writer.add_document(doc!(
40///         title => "A Dairy Cow",
41///     ))?;
42///     index_writer.add_document(doc!(
43///         title => "The Diary of a Young Girl",
44///     ))?;
45///     index_writer.commit()?;
46/// }
47/// let reader = index.reader()?;
48/// let searcher = reader.searcher();
49/// let query = TermQuery::new(
50///     Term::from_field_text(title, "diary"),
51///     IndexRecordOption::Basic,
52/// );
53/// let (top_docs, count) = searcher.search(&query, &(TopDocs::with_limit(2).order_by_score(), Count))?;
54/// assert_eq!(count, 2);
55/// Ok(())
56/// # }
57/// # assert!(test().is_ok());
58/// ```
59#[derive(Clone)]
60pub struct TermQuery {
61    term: Term,
62    index_record_option: IndexRecordOption,
63}
64
65impl fmt::Debug for TermQuery {
66    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
67        write!(f, "TermQuery({:?})", self.term)
68    }
69}
70
71impl TermQuery {
72    /// Creates a new term query.
73    pub fn new(term: Term, segment_postings_options: IndexRecordOption) -> TermQuery {
74        TermQuery {
75            term,
76            index_record_option: segment_postings_options,
77        }
78    }
79
80    /// The `Term` this query is built out of.
81    pub fn term(&self) -> &Term {
82        &self.term
83    }
84
85    /// Returns a weight object.
86    ///
87    /// While `.weight(...)` returns a boxed trait object,
88    /// this method return a specific implementation.
89    /// This is useful for optimization purpose.
90    pub fn specialized_weight(
91        &self,
92        enable_scoring: EnableScoring<'_>,
93    ) -> crate::Result<TermWeight> {
94        let schema = enable_scoring.schema();
95        let field_entry = schema.get_field_entry(self.term.field());
96        if !field_entry.is_indexed() {
97            let error_msg = format!("Field {:?} is not indexed.", field_entry.name());
98            return Err(crate::TantivyError::SchemaError(error_msg));
99        }
100        let bm25_weight = match enable_scoring {
101            EnableScoring::Enabled {
102                statistics_provider,
103                ..
104            } => Bm25Weight::for_terms(statistics_provider, std::slice::from_ref(&self.term))?,
105            EnableScoring::Disabled { .. } => {
106                Bm25Weight::new(Explanation::new("<no score>", 1.0f32), 1.0f32)
107            }
108        };
109        let scoring_enabled = enable_scoring.is_scoring_enabled();
110        let index_record_option = if scoring_enabled {
111            self.index_record_option
112        } else {
113            IndexRecordOption::Basic
114        };
115
116        Ok(TermWeight::new(
117            self.term.clone(),
118            index_record_option,
119            bm25_weight,
120            scoring_enabled,
121        ))
122    }
123}
124
125impl Query for TermQuery {
126    fn weight(&self, enable_scoring: EnableScoring<'_>) -> crate::Result<Box<dyn Weight>> {
127        // If the field is not indexed but is a suitable fast field, fall back to a range query
128        // on the fast field matching exactly this term.
129        //
130        // Note: This is considerable slower since it requires to scan the entire fast field.
131        // TODO: The range query would gain from having a single-value optimization
132        let schema = enable_scoring.schema();
133        let field_entry = schema.get_field_entry(self.term.field());
134        if !field_entry.is_indexed()
135            && field_entry.is_fast()
136            && is_type_valid_for_fastfield_range_query(self.term.typ())
137            && !enable_scoring.is_scoring_enabled()
138        {
139            let range_query = RangeQuery::new(
140                Bound::Included(self.term.clone()),
141                Bound::Included(self.term.clone()),
142            );
143            return range_query.weight(enable_scoring);
144        }
145        Ok(Box::new(self.specialized_weight(enable_scoring)?))
146    }
147    fn query_terms<'a>(&'a self, visitor: &mut dyn FnMut(&'a Term, bool)) {
148        visitor(&self.term, false);
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use std::net::{IpAddr, Ipv6Addr};
155    use std::str::FromStr;
156
157    use columnar::MonotonicallyMappableToU128;
158
159    use crate::collector::{Count, TopDocs};
160    use crate::query::{Query, QueryParser, TermQuery};
161    use crate::schema::{IndexRecordOption, IntoIpv6Addr, Schema, INDEXED, STORED};
162    use crate::{Index, IndexWriter, Term};
163
164    #[test]
165    fn search_ip_test() {
166        let mut schema_builder = Schema::builder();
167        let ip_field = schema_builder.add_ip_addr_field("ip", INDEXED | STORED);
168        let schema = schema_builder.build();
169        let index = Index::create_in_ram(schema);
170        let ip_addr_1 = IpAddr::from_str("127.0.0.1").unwrap().into_ipv6_addr();
171        let ip_addr_2 = Ipv6Addr::from_u128(10);
172
173        {
174            let mut index_writer: IndexWriter = index.writer_for_tests().unwrap();
175            index_writer
176                .add_document(doc!(
177                    ip_field => ip_addr_1
178                ))
179                .unwrap();
180            index_writer
181                .add_document(doc!(
182                    ip_field => ip_addr_2
183                ))
184                .unwrap();
185
186            index_writer.commit().unwrap();
187        }
188        let reader = index.reader().unwrap();
189        let searcher = reader.searcher();
190
191        let assert_single_hit = |query| {
192            let (_top_docs, count) = searcher
193                .search(&query, &(TopDocs::with_limit(2).order_by_score(), Count))
194                .unwrap();
195            assert_eq!(count, 1);
196        };
197        let query_from_text = |text: String| {
198            QueryParser::for_index(&index, vec![ip_field])
199                .parse_query(&text)
200                .unwrap()
201        };
202
203        let query_from_ip = |ip_addr| -> Box<dyn Query> {
204            Box::new(TermQuery::new(
205                Term::from_field_ip_addr(ip_field, ip_addr),
206                IndexRecordOption::Basic,
207            ))
208        };
209
210        assert_single_hit(query_from_ip(ip_addr_1));
211        assert_single_hit(query_from_ip(ip_addr_2));
212        assert_single_hit(query_from_text("127.0.0.1".to_string()));
213        assert_single_hit(query_from_text("\"127.0.0.1\"".to_string()));
214        assert_single_hit(query_from_text(format!("\"{ip_addr_1}\"")));
215        assert_single_hit(query_from_text(format!("\"{ip_addr_2}\"")));
216    }
217}