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}