Skip to main content

lb_tantivy/aggregation/
mod.rs

1//! # Aggregations
2//!
3//! An aggregation summarizes your data as statistics on buckets or metrics.
4//!
5//! Aggregations can provide answer to questions like:
6//! - What is the average price of all sold articles?
7//! - How many errors with status code 500 do we have per day?
8//! - What is the average listing price of cars grouped by color?
9//!
10//! There are two categories: [Metrics](metric) and [Buckets](bucket).
11//!
12//! ## Prerequisite
13//! Currently aggregations work only on [fast fields](`crate::fastfield`). Fast fields
14//! of type `u64`, `f64`, `i64`, `date` and fast fields on text fields.
15//!
16//! ## Usage
17//! To use aggregations, build an aggregation request by constructing
18//! [`Aggregations`](agg_req::Aggregations).
19//! Create an [`AggregationCollector`] from this request. `AggregationCollector` implements the
20//! [`Collector`](crate::collector::Collector) trait and can be passed as collector into
21//! [`Searcher::search()`](crate::Searcher::search).
22//!
23//!
24//! ## JSON Format
25//! Aggregations request and result structures de/serialize into elasticsearch compatible JSON.
26//!
27//! Notice: Intermediate aggregation results should not be de/serialized via JSON format.
28//! Postcard is a good choice.
29//!
30//! ```verbatim
31//! let agg_req: Aggregations = serde_json::from_str(json_request_string).unwrap();
32//! let collector = AggregationCollector::from_aggs(agg_req, None);
33//! let searcher = reader.searcher();
34//! let agg_res = searcher.search(&term_query, &collector).unwrap_err();
35//! let json_response_string: String = &serde_json::to_string(&agg_res)?;
36//! ```
37//!
38//! ## Supported Aggregations
39//! - [Bucket](bucket)
40//!     - [Histogram](bucket::HistogramAggregation)
41//!     - [DateHistogram](bucket::DateHistogramAggregationReq)
42//!     - [Range](bucket::RangeAggregation)
43//!     - [Terms](bucket::TermsAggregation)
44//! - [Metric](metric)
45//!     - [Average](metric::AverageAggregation)
46//!     - [Stats](metric::StatsAggregation)
47//!     - [ExtendedStats](metric::ExtendedStatsAggregation)
48//!     - [Min](metric::MinAggregation)
49//!     - [Max](metric::MaxAggregation)
50//!     - [Sum](metric::SumAggregation)
51//!     - [Count](metric::CountAggregation)
52//!     - [Percentiles](metric::PercentilesAggregationReq)
53//!     - [Cardinality](metric::CardinalityAggregationReq)
54//!     - [TopHits](metric::TopHitsAggregationReq)
55//!
56//! # Example
57//! Compute the average metric, by building [`agg_req::Aggregations`], which is built from an
58//! `(String, agg_req::Aggregation)` iterator.
59//!
60//! Requests are compatible with the elasticsearch JSON request format.
61//!
62//! ```
63//! use tantivy::aggregation::agg_req::Aggregations;
64//!
65//! let elasticsearch_compatible_json_req = r#"
66//! {
67//!   "average": {
68//!     "avg": { "field": "score" }
69//!   },
70//!   "range": {
71//!     "range": {
72//!       "field": "score",
73//!       "ranges": [
74//!         { "to": 3.0 },
75//!         { "from": 3.0, "to": 7.0 },
76//!         { "from": 7.0, "to": 20.0 },
77//!         { "from": 20.0 }
78//!       ]
79//!     },
80//!     "aggs": {
81//!       "average_in_range": { "avg": { "field": "score" } }
82//!     }
83//!   }
84//! }
85//! "#;
86//! let agg_req: Aggregations =
87//!     serde_json::from_str(elasticsearch_compatible_json_req).unwrap();
88//! ```
89//! # Code Organization
90//!
91//! Check the [README](https://github.com/quickwit-oss/tantivy/tree/main/src/aggregation#readme) on github to see how the code is organized.
92//!
93//! # Nested Aggregation
94//!
95//! Buckets can contain sub-aggregations. In this example we create buckets with the range
96//! aggregation and then calculate the average on each bucket.
97//! ```
98//! use tantivy::aggregation::agg_req::*;
99//! use serde_json::json;
100//!
101//! let agg_req_1: Aggregations = serde_json::from_value(json!({
102//!     "rangef64": {
103//!         "range": {
104//!             "field": "score",
105//!             "ranges": [
106//!                 { "from": 3, "to": 7000 },
107//!                 { "from": 7000, "to": 20000 },
108//!                 { "from": 50000, "to": 60000 }
109//!             ]
110//!         },
111//!         "aggs": {
112//!             "average_in_range": { "avg": { "field": "score" } }
113//!         }
114//!     },
115//! }))
116//! .unwrap();
117//! ```
118//!
119//! # Distributed Aggregation
120//! When the data is distributed on different [`Index`](crate::Index) instances, the
121//! [`DistributedAggregationCollector`] provides functionality to merge data between independent
122//! search calls by returning
123//! [`IntermediateAggregationResults`](intermediate_agg_result::IntermediateAggregationResults).
124//! `IntermediateAggregationResults` provides the
125//! [`merge_fruits`](intermediate_agg_result::IntermediateAggregationResults::merge_fruits) method
126//! to merge multiple results. The merged result can then be converted into
127//! [`AggregationResults`](agg_result::AggregationResults) via the
128//! [`into_final_result`](intermediate_agg_result::IntermediateAggregationResults::into_final_result) method.
129
130mod agg_limits;
131pub mod agg_req;
132mod agg_req_with_accessor;
133pub mod agg_result;
134pub mod bucket;
135mod buf_collector;
136mod collector;
137mod date;
138mod error;
139pub mod intermediate_agg_result;
140pub mod metric;
141
142mod segment_agg_result;
143use std::collections::HashMap;
144use std::fmt::Display;
145
146#[cfg(test)]
147mod agg_tests;
148
149use core::fmt;
150
151pub use agg_limits::AggregationLimitsGuard;
152pub use collector::{
153    AggregationCollector, AggregationSegmentCollector, DistributedAggregationCollector,
154    DEFAULT_BUCKET_LIMIT,
155};
156use columnar::{ColumnType, MonotonicallyMappableToU64};
157pub(crate) use date::format_date;
158pub use error::AggregationError;
159use itertools::Itertools;
160use serde::de::{self, Visitor};
161use serde::{Deserialize, Deserializer, Serialize};
162
163fn parse_str_into_f64<E: de::Error>(value: &str) -> Result<f64, E> {
164    let parsed = value
165        .parse::<f64>()
166        .map_err(|_err| de::Error::custom(format!("Failed to parse f64 from string: {value:?}")))?;
167
168    // Check if the parsed value is NaN or infinity
169    if parsed.is_nan() || parsed.is_infinite() {
170        Err(de::Error::custom(format!(
171            "Value is not a valid f64 (NaN or Infinity): {value:?}"
172        )))
173    } else {
174        Ok(parsed)
175    }
176}
177
178/// deserialize Option<f64> from string or float
179pub(crate) fn deserialize_option_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
180where D: Deserializer<'de> {
181    struct StringOrFloatVisitor;
182
183    impl Visitor<'_> for StringOrFloatVisitor {
184        type Value = Option<f64>;
185
186        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
187            formatter.write_str("a string or a float")
188        }
189
190        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
191        where E: de::Error {
192            parse_str_into_f64(value).map(Some)
193        }
194
195        fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
196        where E: de::Error {
197            Ok(Some(value))
198        }
199
200        fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
201        where E: de::Error {
202            Ok(Some(value as f64))
203        }
204
205        fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
206        where E: de::Error {
207            Ok(Some(value as f64))
208        }
209
210        fn visit_none<E>(self) -> Result<Self::Value, E>
211        where E: de::Error {
212            Ok(None)
213        }
214
215        fn visit_unit<E>(self) -> Result<Self::Value, E>
216        where E: de::Error {
217            Ok(None)
218        }
219    }
220
221    deserializer.deserialize_any(StringOrFloatVisitor)
222}
223
224/// deserialize f64 from string or float
225pub(crate) fn deserialize_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
226where D: Deserializer<'de> {
227    struct StringOrFloatVisitor;
228
229    impl Visitor<'_> for StringOrFloatVisitor {
230        type Value = f64;
231
232        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
233            formatter.write_str("a string or a float")
234        }
235
236        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
237        where E: de::Error {
238            parse_str_into_f64(value)
239        }
240
241        fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
242        where E: de::Error {
243            Ok(value)
244        }
245
246        fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
247        where E: de::Error {
248            Ok(value as f64)
249        }
250
251        fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
252        where E: de::Error {
253            Ok(value as f64)
254        }
255    }
256
257    deserializer.deserialize_any(StringOrFloatVisitor)
258}
259
260/// Represents an associative array `(key => values)` in a very efficient manner.
261#[derive(PartialEq, Serialize, Deserialize)]
262pub(crate) struct VecWithNames<T> {
263    pub(crate) values: Vec<T>,
264    keys: Vec<String>,
265}
266
267impl<T: Clone> Clone for VecWithNames<T> {
268    fn clone(&self) -> Self {
269        Self {
270            values: self.values.clone(),
271            keys: self.keys.clone(),
272        }
273    }
274}
275
276impl<T> Default for VecWithNames<T> {
277    fn default() -> Self {
278        Self {
279            values: Default::default(),
280            keys: Default::default(),
281        }
282    }
283}
284
285impl<T: std::fmt::Debug> std::fmt::Debug for VecWithNames<T> {
286    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
287        f.debug_map().entries(self.iter()).finish()
288    }
289}
290
291impl<T> From<HashMap<String, T>> for VecWithNames<T> {
292    fn from(map: HashMap<String, T>) -> Self {
293        VecWithNames::from_entries(map.into_iter().collect_vec())
294    }
295}
296
297impl<T> VecWithNames<T> {
298    fn from_entries(mut entries: Vec<(String, T)>) -> Self {
299        // Sort to ensure order of elements match across multiple instances
300        entries.sort_by(|left, right| left.0.cmp(&right.0));
301        let mut data = Vec::with_capacity(entries.len());
302        let mut data_names = Vec::with_capacity(entries.len());
303        for entry in entries {
304            data_names.push(entry.0);
305            data.push(entry.1);
306        }
307        VecWithNames {
308            values: data,
309            keys: data_names,
310        }
311    }
312    fn iter(&self) -> impl Iterator<Item = (&str, &T)> + '_ {
313        self.keys().zip(self.values.iter())
314    }
315    fn keys(&self) -> impl Iterator<Item = &str> + '_ {
316        self.keys.iter().map(|key| key.as_str())
317    }
318    fn values_mut(&mut self) -> impl Iterator<Item = &mut T> + '_ {
319        self.values.iter_mut()
320    }
321    fn is_empty(&self) -> bool {
322        self.keys.is_empty()
323    }
324    fn len(&self) -> usize {
325        self.keys.len()
326    }
327    fn get(&self, name: &str) -> Option<&T> {
328        self.keys()
329            .position(|key| key == name)
330            .map(|pos| &self.values[pos])
331    }
332}
333
334/// The serialized key is used in a `HashMap`.
335pub type SerializedKey = String;
336
337#[derive(Clone, Debug, Serialize, Deserialize, PartialOrd)]
338/// The key to identify a bucket.
339///
340/// The order is important, with serde untagged, that we try to deserialize into i64 first.
341#[serde(untagged)]
342pub enum Key {
343    /// String key
344    Str(String),
345    /// `i64` key
346    I64(i64),
347    /// `u64` key
348    U64(u64),
349    /// `f64` key
350    F64(f64),
351}
352impl Eq for Key {}
353impl std::hash::Hash for Key {
354    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
355        core::mem::discriminant(self).hash(state);
356        match self {
357            Key::Str(text) => text.hash(state),
358            Key::F64(val) => val.to_bits().hash(state),
359            Key::U64(val) => val.hash(state),
360            Key::I64(val) => val.hash(state),
361        }
362    }
363}
364
365impl PartialEq for Key {
366    fn eq(&self, other: &Self) -> bool {
367        match (self, other) {
368            (Self::Str(l), Self::Str(r)) => l == r,
369            (Self::F64(l), Self::F64(r)) => l.to_bits() == r.to_bits(),
370            (Self::I64(l), Self::I64(r)) => l == r,
371            (Self::U64(l), Self::U64(r)) => l == r,
372            // we list all variant of left operand to make sure this gets updated when we add
373            // variants to the enum
374            (Self::Str(_) | Self::F64(_) | Self::I64(_) | Self::U64(_), _) => false,
375        }
376    }
377}
378
379impl Display for Key {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        match self {
382            Key::Str(val) => f.write_str(val),
383            Key::F64(val) => f.write_str(&val.to_string()),
384            Key::U64(val) => f.write_str(&val.to_string()),
385            Key::I64(val) => f.write_str(&val.to_string()),
386        }
387    }
388}
389
390/// Inverse of `to_fastfield_u64`. Used to convert to `f64` for metrics.
391///
392/// # Panics
393/// Only `u64`, `f64`, `date`, and `i64` are supported.
394pub(crate) fn f64_from_fastfield_u64(val: u64, field_type: &ColumnType) -> f64 {
395    match field_type {
396        ColumnType::U64 => val as f64,
397        ColumnType::I64 | ColumnType::DateTime => i64::from_u64(val) as f64,
398        ColumnType::F64 => f64::from_u64(val),
399        ColumnType::Bool => val as f64,
400        _ => {
401            panic!("unexpected type {field_type:?}. This should not happen")
402        }
403    }
404}
405
406/// Converts the `f64` value to fast field value space, which is always u64.
407///
408/// If the fast field has `u64`, values are stored unchanged as `u64` in the fast field.
409///
410/// If the fast field has `f64` values are converted and stored to `u64` using a
411/// monotonic mapping.
412/// A `f64` value of e.g. `2.0` needs to be converted using the same monotonic
413/// conversion function, so that the value matches the `u64` value stored in the fast
414/// field.
415pub(crate) fn f64_to_fastfield_u64(val: f64, field_type: &ColumnType) -> Option<u64> {
416    match field_type {
417        ColumnType::U64 => Some(val as u64),
418        ColumnType::I64 | ColumnType::DateTime => Some((val as i64).to_u64()),
419        ColumnType::F64 => Some(val.to_u64()),
420        ColumnType::Bool => Some(val as u64),
421        _ => None,
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use std::net::Ipv6Addr;
428
429    use columnar::DateTime;
430    use serde_json::Value;
431    use time::OffsetDateTime;
432
433    use super::agg_req::Aggregations;
434    use super::*;
435    use crate::indexer::NoMergePolicy;
436    use crate::query::{AllQuery, TermQuery};
437    use crate::schema::{IndexRecordOption, Schema, TextFieldIndexing, FAST, STRING};
438    use crate::{Index, IndexWriter, Term};
439
440    pub fn get_test_index_with_num_docs(
441        merge_segments: bool,
442        num_docs: usize,
443    ) -> crate::Result<Index> {
444        get_test_index_from_values(
445            merge_segments,
446            &(0..num_docs).map(|el| el as f64).collect::<Vec<f64>>(),
447        )
448    }
449
450    pub fn exec_request(agg_req: Aggregations, index: &Index) -> crate::Result<Value> {
451        exec_request_with_query(agg_req, index, None)
452    }
453    pub fn exec_request_with_query(
454        agg_req: Aggregations,
455        index: &Index,
456        query: Option<(&str, &str)>,
457    ) -> crate::Result<Value> {
458        exec_request_with_query_and_memory_limit(agg_req, index, query, Default::default())
459    }
460
461    pub fn exec_request_with_query_and_memory_limit(
462        agg_req: Aggregations,
463        index: &Index,
464        query: Option<(&str, &str)>,
465        limits: AggregationLimitsGuard,
466    ) -> crate::Result<Value> {
467        let collector = AggregationCollector::from_aggs(agg_req, limits);
468
469        let reader = index.reader()?;
470        let searcher = reader.searcher();
471        let agg_res = if let Some((field, term)) = query {
472            let text_field = reader.searcher().schema().get_field(field).unwrap();
473
474            let term_query = TermQuery::new(
475                Term::from_field_text(text_field, term),
476                IndexRecordOption::Basic,
477            );
478
479            searcher.search(&term_query, &collector)?
480        } else {
481            searcher.search(&AllQuery, &collector)?
482        };
483
484        // Test serialization/deserialization roundtrip
485        let res: Value = serde_json::from_str(&serde_json::to_string(&agg_res)?)?;
486        Ok(res)
487    }
488
489    pub fn get_test_index_from_values(
490        merge_segments: bool,
491        values: &[f64],
492    ) -> crate::Result<Index> {
493        // Every value gets its own segment
494        let mut segment_and_values = vec![];
495        for value in values {
496            segment_and_values.push(vec![(*value, value.to_string())]);
497        }
498        get_test_index_from_values_and_terms(merge_segments, &segment_and_values)
499    }
500
501    pub fn get_test_index_from_terms(
502        merge_segments: bool,
503        values: &[Vec<&str>],
504    ) -> crate::Result<Index> {
505        // Every value gets its own segment
506        let segment_and_values = values
507            .iter()
508            .map(|terms| {
509                terms
510                    .iter()
511                    .enumerate()
512                    .map(|(i, term)| (i as f64, term.to_string()))
513                    .collect()
514            })
515            .collect::<Vec<_>>();
516        get_test_index_from_values_and_terms(merge_segments, &segment_and_values)
517    }
518
519    pub fn get_test_index_from_values_and_terms(
520        merge_segments: bool,
521        segment_and_values: &[Vec<(f64, String)>],
522    ) -> crate::Result<Index> {
523        let mut schema_builder = Schema::builder();
524        let text_fieldtype = crate::schema::TextOptions::default()
525            .set_indexing_options(
526                TextFieldIndexing::default()
527                    .set_index_option(IndexRecordOption::Basic)
528                    .set_fieldnorms(false),
529            )
530            .set_fast(None)
531            .set_stored();
532        let text_field = schema_builder.add_text_field("text", text_fieldtype.clone());
533        let text_field_id = schema_builder.add_text_field("text_id", text_fieldtype);
534        let string_field_id = schema_builder.add_text_field("string_id", STRING | FAST);
535        let score_fieldtype = crate::schema::NumericOptions::default().set_fast();
536        let score_field = schema_builder.add_u64_field("score", score_fieldtype.clone());
537        let score_field_f64 = schema_builder.add_f64_field("score_f64", score_fieldtype.clone());
538        let score_field_i64 = schema_builder.add_i64_field("score_i64", score_fieldtype);
539        let fraction_field = schema_builder.add_f64_field(
540            "fraction_f64",
541            crate::schema::NumericOptions::default().set_fast(),
542        );
543        let index = Index::create_in_ram(schema_builder.build());
544        {
545            // let mut index_writer = index.writer_for_tests()?;
546            let mut index_writer = index.writer_with_num_threads(1, 20_000_000)?;
547            index_writer.set_merge_policy(Box::new(NoMergePolicy));
548            for values in segment_and_values {
549                for (i, term) in values {
550                    let i = *i;
551                    // writing the segment
552                    index_writer.add_document(doc!(
553                        text_field => "cool",
554                        text_field_id => term.to_string(),
555                        string_field_id => term.to_string(),
556                        score_field => i as u64,
557                        score_field_f64 => i,
558                        score_field_i64 => i as i64,
559                        fraction_field => i/100.0,
560                    ))?;
561                }
562                index_writer.commit()?;
563            }
564        }
565        if merge_segments {
566            let segment_ids = index
567                .searchable_segment_ids()
568                .expect("Searchable segments failed.");
569            if segment_ids.len() > 1 {
570                let mut index_writer: IndexWriter = index.writer_for_tests()?;
571                index_writer.merge(&segment_ids).wait()?;
572                index_writer.wait_merging_threads()?;
573            }
574        }
575
576        Ok(index)
577    }
578
579    pub fn get_test_index_2_segments(merge_segments: bool) -> crate::Result<Index> {
580        let mut schema_builder = Schema::builder();
581        let text_fieldtype = crate::schema::TextOptions::default()
582            .set_indexing_options(
583                TextFieldIndexing::default().set_index_option(IndexRecordOption::WithFreqs),
584            )
585            .set_fast(Some("raw"))
586            .set_stored();
587        let text_field = schema_builder.add_text_field("text", text_fieldtype);
588        let date_field = schema_builder.add_date_field("date", FAST);
589        schema_builder.add_text_field("dummy_text", STRING);
590        let score_fieldtype = crate::schema::NumericOptions::default().set_fast();
591        let score_field = schema_builder.add_u64_field("score", score_fieldtype.clone());
592        let score_field_f64 = schema_builder.add_f64_field("score_f64", score_fieldtype.clone());
593        let ip_addr_field = schema_builder.add_ip_addr_field("ip_addr", FAST);
594
595        let multivalue = crate::schema::NumericOptions::default().set_fast();
596        let scores_field_i64 = schema_builder.add_i64_field("scores_i64", multivalue);
597
598        let score_field_i64 = schema_builder.add_i64_field("score_i64", score_fieldtype);
599        let index = Index::create_in_ram(schema_builder.build());
600        {
601            let mut index_writer = index.writer_for_tests()?;
602            // writing the segment
603            index_writer.add_document(doc!(
604                text_field => "cool",
605                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800).unwrap()),
606                score_field => 1u64,
607                ip_addr_field => Ipv6Addr::from(1u128),
608                score_field_f64 => 1f64,
609                score_field_i64 => 1i64,
610                scores_field_i64 => 1i64,
611                scores_field_i64 => 2i64,
612            ))?;
613            index_writer.add_document(doc!(
614                text_field => "cool",
615                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400).unwrap()),
616                score_field => 3u64,
617                score_field_f64 => 3f64,
618                score_field_i64 => 3i64,
619                scores_field_i64 => 5i64,
620                scores_field_i64 => 5i64,
621            ))?;
622            index_writer.add_document(doc!(
623                text_field => "cool",
624                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400).unwrap()),
625                score_field => 5u64,
626                score_field_f64 => 5f64,
627                score_field_i64 => 5i64,
628            ))?;
629            index_writer.add_document(doc!(
630                text_field => "nohit",
631                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400).unwrap()),
632                score_field => 6u64,
633                score_field_f64 => 6f64,
634                score_field_i64 => 6i64,
635            ))?;
636            index_writer.add_document(doc!(
637                text_field => "cool",
638                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400).unwrap()),
639                score_field => 7u64,
640                score_field_f64 => 7f64,
641                score_field_i64 => 7i64,
642            ))?;
643            index_writer.commit()?;
644            index_writer.add_document(doc!(
645                text_field => "cool",
646                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400).unwrap()),
647                score_field => 11u64,
648                score_field_f64 => 11f64,
649                score_field_i64 => 11i64,
650            ))?;
651            index_writer.add_document(doc!(
652                text_field => "cool",
653                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400 + 86400).unwrap()),
654                score_field => 14u64,
655                score_field_f64 => 14f64,
656                score_field_i64 => 14i64,
657            ))?;
658
659            index_writer.add_document(doc!(
660                text_field => "cool",
661                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400 + 86400).unwrap()),
662                score_field => 44u64,
663                score_field_f64 => 44.5f64,
664                score_field_i64 => 44i64,
665            ))?;
666
667            index_writer.commit()?;
668
669            // no hits segment
670            index_writer.add_document(doc!(
671                text_field => "nohit",
672                date_field => DateTime::from_utc(OffsetDateTime::from_unix_timestamp(1_546_300_800 + 86400 + 86400).unwrap()),
673                score_field => 44u64,
674                score_field_f64 => 44.5f64,
675                score_field_i64 => 44i64,
676            ))?;
677
678            index_writer.commit()?;
679        }
680        if merge_segments {
681            let segment_ids = index
682                .searchable_segment_ids()
683                .expect("Searchable segments failed.");
684            let mut index_writer: IndexWriter = index.writer_for_tests()?;
685            index_writer.merge(&segment_ids).wait()?;
686            index_writer.wait_merging_threads()?;
687        }
688
689        Ok(index)
690    }
691}