elasticsearch_dsl/search/queries/specialized/
distance_feature_query.rs

1use crate::search::*;
2use crate::util::*;
3use chrono::{DateTime, Utc};
4use serde::ser::Serialize;
5use std::fmt::Debug;
6
7#[doc(hidden)]
8pub trait Origin: Debug + PartialEq + Serialize + Clone {
9    type Pivot: Debug + PartialEq + Serialize + Clone;
10}
11
12impl Origin for DateTime<Utc> {
13    type Pivot = Time;
14}
15
16impl Origin for GeoLocation {
17    type Pivot = Distance;
18}
19
20/// Boosts the [relevance score](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#relevance-scores)
21/// of documents closer to a provided `origin` date or point.
22/// For example, you can use this query to give more weight to documents
23/// closer to a certain date or location.
24///
25/// You can use the `distance_feature` query to find the nearest neighbors to a location.
26/// You can also use the query in a [bool](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html)
27/// search’s `should` filter to add boosted relevance scores to the `bool` query’s scores.
28///
29/// **How the `distance_feature` query calculates relevance scores**
30///
31/// The `distance_feature` query dynamically calculates the distance between the
32/// `origin` value and a document's field values. It then uses this distance as a
33/// feature to boost the
34/// [relevance-scores](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#relevance-scores)
35/// of closer documents.
36///
37/// The `distance_feature` query calculates a document's
38/// [relevance score](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#relevance-scores)
39/// as follows:
40///
41/// ```text
42/// relevance score = boost * pivot / (pivot + distance)
43/// ```
44///
45/// The `distance` is the absolute difference between the `origin` value and a
46/// document's field value.
47///
48/// **Skip non-competitive hits**
49///
50/// Unlike the
51/// [`function_score`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html)
52/// query or other ways to change
53/// [relevance scores](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html#relevance-scores)
54/// , the `distance_feature` query efficiently skips non-competitive hits when the
55/// [`track_total_hits`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-uri-request.html)
56/// parameter is **not** `true`.
57///
58/// To create distance feature query date query:
59/// ```
60/// # use elasticsearch_dsl::Time;
61/// # use elasticsearch_dsl::queries::*;
62/// # use elasticsearch_dsl::queries::params::*;
63/// # use chrono::prelude::*;
64/// # let query =
65/// Query::distance_feature("test", Utc.with_ymd_and_hms(2014, 7, 8, 9, 1, 0).unwrap(), Time::Days(7))
66///     .boost(1.5)
67///     .name("test");
68/// ```
69/// To create distance feature query geo query:
70/// ```
71/// # use elasticsearch_dsl::{Distance, GeoLocation};
72/// # use elasticsearch_dsl::queries::*;
73/// # use elasticsearch_dsl::queries::params::*;
74/// # let query =
75/// Query::distance_feature("test", GeoLocation::new(-71.34, 40.12), Distance::Kilometers(15))
76///     .boost(1.5)
77///     .name("test");
78/// ```
79/// Distance Feature is built to allow only valid origin and pivot values,
80/// the following won't compile:
81/// ```compile_fail
82/// # use elasticsearch_dsl::Distance;
83/// # use chrono::prelude::*;
84/// # use elasticsearch_dsl::queries::*;
85/// # use elasticsearch_dsl::queries::params::*;
86/// # let query =
87/// Query::distance_feature("test", Utc.with_ymd_and_hms(2014, 7, 8, 9, 1, 0).unwrap(), Distance::Kilometers(15))
88///     .boost(1.5)
89///     .name("test");
90/// ```
91///
92/// <https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-distance-feature-query.html>
93#[derive(Debug, Clone, PartialEq, Serialize)]
94#[serde(remote = "Self")]
95pub struct DistanceFeatureQuery<O>
96where
97    O: Origin,
98{
99    field: String,
100
101    origin: O,
102
103    pivot: <O as Origin>::Pivot,
104
105    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
106    boost: Option<f32>,
107
108    #[serde(skip_serializing_if = "ShouldSkip::should_skip")]
109    _name: Option<String>,
110}
111
112impl Query {
113    /// Creates an instance of [`DistanceFeatureQuery`]
114    ///
115    /// - `field` - Name of the field used to calculate distances. This field must meet the following criteria:<br/>
116    ///   - Be a [`date`](https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html),
117    ///     [`date_nanos`](https://www.elastic.co/guide/en/elasticsearch/reference/current/date_nanos.html) or
118    ///     [`geo_point`](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html) field
119    ///   - Have an [index](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index.html)
120    ///     mapping parameter value of `true`, which is the default
121    ///   - Have an [`doc_values`](https://www.elastic.co/guide/en/elasticsearch/reference/current/doc-values.html)
122    ///     mapping parameter value of `true`, which is the default
123    /// - `origin` - Date or point of origin used to calculate distances.<br/>
124    ///   If the `field` value is a
125    ///   [`date`](https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html) or
126    ///   [`date_nanos`](https://www.elastic.co/guide/en/elasticsearch/reference/current/date_nanos.html)
127    ///   field, the `origin` value must be a
128    ///   [date](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-daterange-aggregation.html#date-format-pattern).
129    ///   [Date Math](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#date-math),
130    ///   such as `now-1h`, is supported.<br/>
131    ///   If the `field` value is a
132    ///   [`geo_point`](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html)
133    ///   field, the `origin` value must be a geopoint.
134    /// - `pivot` - Distance from the `origin` at which relevance scores receive half of the boost value.<br/>
135    ///   If the field value is a
136    ///   [`date`](https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html) or
137    ///   [`date_nanos`](https://www.elastic.co/guide/en/elasticsearch/reference/current/date_nanos.html)
138    ///   field, the `pivot` value must be a
139    ///   [`time unit`](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units)
140    ///   , such as `1h` or `10d`.<br/>
141    ///   If the `field` value is a
142    ///   [`geo_point`](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html)
143    ///   field, the `pivot` value must be a
144    ///   [distance unit](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#distance-units)
145    ///   , such as `1km` or `12m`.
146    pub fn distance_feature<T, O>(
147        field: T,
148        origin: O,
149        pivot: <O as Origin>::Pivot,
150    ) -> DistanceFeatureQuery<O>
151    where
152        T: ToString,
153        O: Origin,
154    {
155        DistanceFeatureQuery {
156            field: field.to_string(),
157            origin,
158            pivot,
159            boost: None,
160            _name: None,
161        }
162    }
163}
164
165impl<O> DistanceFeatureQuery<O>
166where
167    O: Origin,
168{
169    add_boost_and_name!();
170}
171
172impl<O> ShouldSkip for DistanceFeatureQuery<O> where O: Origin {}
173
174serialize_with_root!("distance_feature": DistanceFeatureQuery<DateTime<Utc>>);
175serialize_with_root!("distance_feature": DistanceFeatureQuery<GeoLocation>);
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use chrono::prelude::*;
181
182    #[test]
183    fn serialization() {
184        assert_serialize_query(
185            Query::distance_feature(
186                "test",
187                Utc.with_ymd_and_hms(2014, 7, 8, 9, 1, 0).single().unwrap(),
188                Time::Days(7),
189            ),
190            json!({
191                "distance_feature": {
192                    "field": "test",
193                    "origin": "2014-07-08T09:01:00Z",
194                    "pivot": "7d",
195                }
196            }),
197        );
198
199        assert_serialize_query(
200            Query::distance_feature(
201                "test",
202                Utc.with_ymd_and_hms(2014, 7, 8, 9, 1, 0).single().unwrap(),
203                Time::Days(7),
204            )
205            .boost(1.5)
206            .name("test"),
207            json!({
208                "distance_feature": {
209                    "field": "test",
210                    "origin": "2014-07-08T09:01:00Z",
211                    "pivot": "7d",
212                    "boost": 1.5,
213                    "_name": "test",
214                }
215            }),
216        );
217        assert_serialize_query(
218            Query::distance_feature(
219                "test",
220                GeoLocation::new(12.0, 13.0),
221                Distance::Kilometers(15),
222            ),
223            json!({
224                "distance_feature": {
225                    "field": "test",
226                    "origin": [13.0, 12.0],
227                    "pivot": "15km",
228                }
229            }),
230        );
231
232        assert_serialize_query(
233            Query::distance_feature(
234                "test",
235                GeoLocation::new(12.0, 13.0),
236                Distance::Kilometers(15),
237            )
238            .boost(2)
239            .name("test"),
240            json!({
241                "distance_feature": {
242                    "field": "test",
243                    "origin": [13.0, 12.0],
244                    "pivot": "15km",
245                    "boost": 2.0,
246                    "_name": "test",
247                }
248            }),
249        );
250    }
251}