Skip to content

Commit e58db5b

Browse files
committed
Implement distance_feature query (#3983)
Implement distance_feature query
1 parent 39ad9a2 commit e58db5b

File tree

9 files changed

+273
-0
lines changed

9 files changed

+273
-0
lines changed

src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ public interface IQueryContainer
165165
[DataMember(Name = "rank_feature")]
166166
IRankFeatureQuery RankFeature { get; set; }
167167

168+
/// <inheritdoc cref="IDistanceFeatureQuery"/>
169+
[DataMember(Name = "distance_feature")]
170+
IDistanceFeatureQuery DistanceFeature { get; set; }
171+
168172
void Accept(IQueryVisitor visitor);
169173
}
170174
}

src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public partial class QueryContainer : IQueryContainer, IDescriptor
1111
private ICommonTermsQuery _commonTerms;
1212
private IConstantScoreQuery _constantScore;
1313
private IDisMaxQuery _disMax;
14+
private IDistanceFeatureQuery _distanceFeature;
1415
private IExistsQuery _exists;
1516
private IFunctionScoreQuery _functionScore;
1617
private IFuzzyQuery _fuzzy;
@@ -91,6 +92,12 @@ IDisMaxQuery IQueryContainer.DisMax
9192
set => _disMax = Set(value);
9293
}
9394

95+
IDistanceFeatureQuery IQueryContainer.DistanceFeature
96+
{
97+
get => _distanceFeature;
98+
set => _distanceFeature = Set(value);
99+
}
100+
94101
IExistsQuery IQueryContainer.Exists
95102
{
96103
get => _exists;

src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ public QueryContainer HasParent<TParent>(Func<HasParentQueryDescriptor<TParent>,
214214
public QueryContainer DisMax(Func<DisMaxQueryDescriptor<T>, IDisMaxQuery> selector) =>
215215
WrapInContainer(selector, (query, container) => container.DisMax = query);
216216

217+
/// <inheritdoc cref="IDistanceFeatureQuery"/>
218+
public QueryContainer DistanceFeature(Func<DistanceFeatureQueryDescriptor<T>, IDistanceFeatureQuery> selector) =>
219+
WrapInContainer(selector, (query, container) => container.DistanceFeature = query);
220+
217221
/// <summary>
218222
/// A query that wraps a filter or another query and simply returns a constant score equal to the query boost
219223
/// for every document in the filter. Maps to Lucene ConstantScoreQuery.

src/Nest/QueryDsl/Query.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ public static QueryContainer DateRange(Func<DateRangeQueryDescriptor<T>, IDateRa
2626
public static QueryContainer DisMax(Func<DisMaxQueryDescriptor<T>, IDisMaxQuery> selector) =>
2727
new QueryContainerDescriptor<T>().DisMax(selector);
2828

29+
/// <inheritdoc cref="IDistanceFeatureQuery"/>
30+
public static QueryContainer DistanceFeature(Func<DistanceFeatureQueryDescriptor<T>, IDistanceFeatureQuery> selector) =>
31+
new QueryContainerDescriptor<T>().DistanceFeature(selector);
32+
2933
public static QueryContainer Exists(Func<ExistsQueryDescriptor<T>, IExistsQuery> selector) =>
3034
new QueryContainerDescriptor<T>().Exists(selector);
3135

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
using System;
2+
using System.Runtime.Serialization;
3+
using Elasticsearch.Net.Utf8Json;
4+
using Elasticsearch.Net.Utf8Json.Internal;
5+
6+
namespace Nest
7+
{
8+
/// <summary>
9+
/// Boosts the relevance score of documents closer to a provided origin date or point. For example, you can use this query to give
10+
/// more weight to documents closer to a certain date or location.
11+
/// You can use the distance_feature query to find the nearest neighbors to a location. You can also use the query in a bool
12+
/// search’s should filter to add boosted relevance scores to the bool query’s scores.
13+
/// </summary>
14+
[JsonFormatter(typeof(DistanceFeatureQueryFormatter))]
15+
[InterfaceDataContract]
16+
public interface IDistanceFeatureQuery : IFieldNameQuery
17+
{
18+
/// <summary>
19+
/// Date or point of origin used to calculate distances.
20+
// If the field value is a date or date_nanos field, the origin value must be a date. Date Math, such as now-1h, is supported.
21+
// If the field value is a geo_point field, the origin value must be a geopoint.
22+
/// </summary>
23+
[DataMember(Name = "origin")]
24+
Union<GeoLocation, DateMath> Origin { get; set; }
25+
26+
/// <summary>
27+
/// Distance from the origin at which relevance scores receive half of the boost value.
28+
// If the field value is a date or date_nanos field, the pivot value must be a time unit, such as 1h or 10d.
29+
// If the field value is a geo_point field, the pivot value must be a distance unit, such as 1km or 12m.
30+
/// </summary>
31+
[DataMember(Name = "pivot")]
32+
Union<Distance, Time> Pivot { get; set; }
33+
}
34+
35+
/// <inheritdoc cref="IDistanceFeatureQuery" />
36+
public class DistanceFeatureQuery : FieldNameQueryBase, IDistanceFeatureQuery
37+
{
38+
protected override bool Conditionless => IsConditionless(this);
39+
40+
internal static bool IsConditionless(IDistanceFeatureQuery q) => q.Field.IsConditionless() || q.Origin == null && q.Pivot == null;
41+
42+
internal override void InternalWrapInContainer(IQueryContainer container) => container.DistanceFeature = this;
43+
44+
/// <inheritdoc />
45+
public Union<GeoLocation, DateMath> Origin { get; set; }
46+
47+
/// <inheritdoc />
48+
public Union<Distance, Time> Pivot { get; set; }
49+
}
50+
51+
public class DistanceFeatureQueryDescriptor<T>
52+
: FieldNameQueryDescriptorBase<DistanceFeatureQueryDescriptor<T>, IDistanceFeatureQuery, T>
53+
, IDistanceFeatureQuery where T : class
54+
{
55+
Union<GeoLocation, DateMath> IDistanceFeatureQuery.Origin { get; set; }
56+
57+
Union<Distance, Time> IDistanceFeatureQuery.Pivot { get; set; }
58+
59+
protected override bool Conditionless => DistanceFeatureQuery.IsConditionless(this);
60+
61+
/// <inheritdoc cref="IDistanceFeatureQuery.Origin" />
62+
public DistanceFeatureQueryDescriptor<T> Origin(DateMath origin) =>
63+
Assign(origin, (a, v) => a.Origin = v);
64+
65+
/// <inheritdoc cref="IDistanceFeatureQuery.Origin" />
66+
public DistanceFeatureQueryDescriptor<T> Origin(GeoLocation origin) =>
67+
Assign(origin, (a, v) => a.Origin = v);
68+
69+
/// <inheritdoc cref="IDistanceFeatureQuery.Pivot" />
70+
public DistanceFeatureQueryDescriptor<T> Pivot(Time pivot) =>
71+
Assign(pivot, (a, v) => a.Pivot = v);
72+
73+
/// <inheritdoc cref="IDistanceFeatureQuery.Pivot" />
74+
public DistanceFeatureQueryDescriptor<T> Pivot(Distance pivot) =>
75+
Assign(pivot, (a, v) => a.Pivot = v);
76+
}
77+
78+
internal class DistanceFeatureQueryFormatter : IJsonFormatter<IDistanceFeatureQuery>
79+
{
80+
private static readonly UnionFormatter<GeoLocation, DateMath> OriginUnionFormatter = new UnionFormatter<GeoLocation, DateMath> ();
81+
private static readonly UnionFormatter<Distance, Time> PivotUnionFormatter = new UnionFormatter<Distance, Time>();
82+
83+
public void Serialize(ref JsonWriter writer, IDistanceFeatureQuery value, IJsonFormatterResolver formatterResolver)
84+
{
85+
if (value == null)
86+
{
87+
writer.WriteNull();
88+
return;
89+
}
90+
91+
writer.WriteBeginObject();
92+
93+
writer.WritePropertyName("field");
94+
var fieldFormatter = formatterResolver.GetFormatter<Field>();
95+
fieldFormatter.Serialize(ref writer, value.Field, formatterResolver);
96+
writer.WriteValueSeparator();
97+
98+
writer.WritePropertyName("origin");
99+
OriginUnionFormatter.Serialize(ref writer, value.Origin, formatterResolver);
100+
writer.WriteValueSeparator();
101+
102+
writer.WritePropertyName("pivot");
103+
PivotUnionFormatter.Serialize(ref writer, value.Pivot, formatterResolver);
104+
105+
writer.WriteValueSeparator();
106+
107+
if (value.Boost.HasValue)
108+
{
109+
writer.WritePropertyName("boost");
110+
writer.WriteDouble(value.Boost.Value);
111+
}
112+
113+
writer.WriteEndObject();
114+
}
115+
116+
private static readonly AutomataDictionary Fields = new AutomataDictionary
117+
{
118+
{ "field", 0 },
119+
{ "origin", 1 },
120+
{ "pivot", 2 },
121+
{ "boost", 3 }
122+
};
123+
124+
public IDistanceFeatureQuery Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
125+
{
126+
if (reader.ReadIsNull())
127+
return null;
128+
129+
var query = new DistanceFeatureQuery();
130+
var count = 0;
131+
while (reader.ReadIsInObject(ref count))
132+
{
133+
if (Fields.TryGetValue(reader.ReadPropertyNameSegmentRaw(), out var value))
134+
{
135+
switch (value)
136+
{
137+
case 0:
138+
query.Field = formatterResolver.GetFormatter<Field>().Deserialize(ref reader, formatterResolver);
139+
break;
140+
case 1:
141+
query.Origin = OriginUnionFormatter.Deserialize(ref reader, formatterResolver);
142+
break;
143+
case 2:
144+
query.Pivot = PivotUnionFormatter.Deserialize(ref reader, formatterResolver);
145+
break;
146+
case 3:
147+
query.Boost = reader.ReadDouble();
148+
break;
149+
}
150+
}
151+
else
152+
reader.ReadNextBlock();
153+
}
154+
155+
return query;
156+
}
157+
}
158+
}

src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public virtual void Visit(IQuery query) { }
5555

5656
public virtual void Visit(IDisMaxQuery query) => Write("dis_max");
5757

58+
public virtual void Visit(IDistanceFeatureQuery query) => Write("distance_feature");
59+
5860
public virtual void Visit(ISpanContainingQuery query) => Write("span_containing");
5961

6062
public virtual void Visit(ISpanWithinQuery query) => Write("span_within");

src/Nest/QueryDsl/Visitor/QueryVisitor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public interface IQueryVisitor
3434

3535
void Visit(IDisMaxQuery query);
3636

37+
void Visit(IDistanceFeatureQuery query);
38+
3739
void Visit(IFunctionScoreQuery query);
3840

3941
void Visit(IFuzzyQuery query);
@@ -159,6 +161,8 @@ public virtual void Visit(IConstantScoreQuery query) { }
159161

160162
public virtual void Visit(IDisMaxQuery query) { }
161163

164+
public virtual void Visit(IDistanceFeatureQuery query) { }
165+
162166
public virtual void Visit(ISpanContainingQuery query) { }
163167

164168
public virtual void Visit(ISpanWithinQuery query) { }

src/Nest/QueryDsl/Visitor/QueryWalker.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ public void Walk(IQueryContainer qd, IQueryVisitor visitor)
8282
v.Visit(d);
8383
Accept(v, d.Queries);
8484
});
85+
VisitQuery(qd.DistanceFeature, visitor, (v, d) =>
86+
{
87+
v.Visit(d);
88+
});
8589
VisitQuery(qd.FunctionScore, visitor, (v, d) =>
8690
{
8791
v.Visit(d);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using Elastic.Xunit.XunitPlumbing;
2+
using Nest;
3+
using Tests.Core.ManagedElasticsearch.Clusters;
4+
using Tests.Domain;
5+
using Tests.Framework.EndpointTests.TestState;
6+
7+
namespace Tests.QueryDsl.Specialized.DistanceFeature
8+
{
9+
/**
10+
* Boosts the relevance score of documents closer to a provided origin date or point. For example, you can use this query to give
11+
* more weight to documents closer to a certain date.
12+
*/
13+
[SkipVersion("<7.2.0", "Implemented in version 7.2.0")]
14+
public class DistanceFeatureTimeQueryUsageTests : QueryDslUsageTestsBase
15+
{
16+
public DistanceFeatureTimeQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
17+
18+
protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen<IDistanceFeatureQuery>(a => a.DistanceFeature)
19+
{
20+
q =>
21+
{
22+
q.Field = null;
23+
q.Origin = null;
24+
q.Pivot = null;
25+
}
26+
};
27+
28+
protected override QueryContainer QueryInitializer => new DistanceFeatureQuery
29+
{
30+
Boost = 1.1,
31+
Field = Infer.Field<Project>(f => f.StartedOn),
32+
Origin = DateMath.FromString("now"),
33+
Pivot = new Time("7d")
34+
};
35+
36+
protected override object QueryJson =>
37+
new { distance_feature = new { boost = 1.1, field = "startedOn", origin = "now", pivot = "7d" } };
38+
39+
protected override QueryContainer QueryFluent(QueryContainerDescriptor<Project> q) => q
40+
.DistanceFeature(rf => rf
41+
.Boost(1.1)
42+
.Field(f => f.StartedOn)
43+
.Origin(DateMath.FromString("now"))
44+
.Pivot(new Time("7d"))
45+
);
46+
}
47+
48+
/**
49+
* You can use the distance_feature query to find the nearest neighbors to a location. You can also use the query in a bool
50+
* search’s should filter to add boosted relevance scores to the bool query’s scores.
51+
*/
52+
[SkipVersion("<7.2.0", "Implemented in version 7.2.0")]
53+
public class DistanceFeatureDistanceQueryUsageTests : QueryDslUsageTestsBase
54+
{
55+
public DistanceFeatureDistanceQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
56+
57+
protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen<IDistanceFeatureQuery>(a => a.DistanceFeature)
58+
{
59+
q =>
60+
{
61+
q.Field = null;
62+
q.Origin = null;
63+
q.Pivot = null;
64+
}
65+
};
66+
67+
protected override QueryContainer QueryInitializer => new DistanceFeatureQuery()
68+
{
69+
Boost = 1.1,
70+
Field = Infer.Field<Project>(f => f.StartedOn),
71+
Origin = new GeoLocation(70, -70),
72+
Pivot = new Distance(100, DistanceUnit.Miles)
73+
};
74+
75+
protected override object QueryJson =>
76+
new { distance_feature = new { boost = 1.1, field = "startedOn", origin = new { lat = 70.0, lon = -70.0 }, pivot = "100mi" } };
77+
78+
protected override QueryContainer QueryFluent(QueryContainerDescriptor<Project> q) => q
79+
.DistanceFeature(rf => rf
80+
.Boost(1.1)
81+
.Field(f => f.StartedOn)
82+
.Origin(new GeoLocation(70, -70))
83+
.Pivot(new Distance(100, DistanceUnit.Miles))
84+
);
85+
}
86+
}

0 commit comments

Comments
 (0)