Skip to content

Commit 7319550

Browse files
authored
Add support for combined fields query (#5619)
* Add support for combined fields query * Fix tests
1 parent 163e588 commit 7319550

File tree

9 files changed

+269
-16
lines changed

9 files changed

+269
-16
lines changed

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

+3
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ public interface IQueryContainer
185185
[DataMember(Name = "pinned")]
186186
IPinnedQuery Pinned { get; set; }
187187

188+
/// <inheritdoc cref="ICombinedFieldsQuery"/>
189+
[DataMember(Name = "combined_fields")]
190+
ICombinedFieldsQuery CombinedFields { get; set; }
188191

189192
void Accept(IQueryVisitor visitor);
190193
}

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// Licensed to Elasticsearch B.V under one or more agreements.
2-
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3-
// See the LICENSE file in the project root for more information
4-
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
55
using System;
66
using System.Runtime.Serialization;
77

@@ -64,6 +64,7 @@ public partial class QueryContainer : IQueryContainer, IDescriptor
6464
private IWildcardQuery _wildcard;
6565
private IRankFeatureQuery _rankFeature;
6666
private IPinnedQuery _pinned;
67+
private ICombinedFieldsQuery _combinedFieldsQuery;
6768

6869
[IgnoreDataMember]
6970
private IQueryContainer Self => this;
@@ -385,6 +386,11 @@ IPinnedQuery IQueryContainer.Pinned
385386
set => _pinned = Set(value);
386387
}
387388

389+
ICombinedFieldsQuery IQueryContainer.CombinedFields
390+
{
391+
get => _combinedFieldsQuery;
392+
set => _combinedFieldsQuery = Set(value);
393+
}
388394

389395
private T Set<T>(T value) where T : IQuery
390396
{

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

+3
Original file line numberDiff line numberDiff line change
@@ -492,5 +492,8 @@ public QueryContainer TermsSet(Func<TermsSetQueryDescriptor<T>, ITermsSetQuery>
492492

493493
public QueryContainer Pinned(Func<PinnedQueryDescriptor<T>, IPinnedQuery> selector) =>
494494
WrapInContainer(selector, (query, container) => container.Pinned = query);
495+
496+
public QueryContainer CombinedFields(Func<CombinedFieldsQueryDescriptor<T>, ICombinedFieldsQuery> selector) =>
497+
WrapInContainer(selector, (query, container) => container.CombinedFields = query);
495498
}
496499
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System;
6+
using System.Runtime.Serialization;
7+
using Elasticsearch.Net.Utf8Json;
8+
9+
namespace Nest
10+
{
11+
[InterfaceDataContract]
12+
[ReadAs(typeof(CombinedFieldsQuery))]
13+
public interface ICombinedFieldsQuery : IQuery
14+
{
15+
/// <summary>
16+
/// The query to execute
17+
/// </summary>
18+
[DataMember(Name = "query")]
19+
string Query { get; set; }
20+
21+
/// <summary>
22+
/// The fields to perform the query against.
23+
/// </summary>
24+
[DataMember(Name = "fields")]
25+
Fields Fields { get; set; }
26+
27+
/// <summary>
28+
/// A value controlling how many "should" clauses in the resulting boolean query should match.
29+
/// It can be an absolute value, a percentage or a combination of both.
30+
/// </summary>
31+
[DataMember(Name = "minimum_should_match")]
32+
MinimumShouldMatch MinimumShouldMatch { get; set; }
33+
34+
/// <summary>
35+
/// If `true`, match phrase queries are automatically created for multi-term synonyms.
36+
/// </summary>
37+
[DataMember(Name = "auto_generate_synonyms_phrase_query")]
38+
bool? AutoGenerateSynonymsPhraseQuery { get; set; }
39+
40+
/// <summary>
41+
/// The operator used if no explicit operator is specified.
42+
/// The default operator is <see cref="Nest.Operator.Or" />
43+
/// </summary>
44+
/// <remarks>
45+
/// <see cref="TextQueryType.BestFields" /> and <see cref="TextQueryType.MostFields" /> types are field-centric?;
46+
/// they generate a match query per field. This means that <see cref="Operator" /> and <see cref="MinimumShouldMatch" />
47+
/// are applied to each field individually, which is probably not what you want.
48+
/// Consider using <see cref="TextQueryType.CrossFields" />.
49+
/// </remarks>
50+
[DataMember(Name = "operator")]
51+
Operator? Operator { get; set; }
52+
53+
/// <summary>
54+
/// If the analyzer used removes all tokens in a query like a stop filter does, the default behavior is
55+
/// to match no documents at all. In order to change that, <see cref="Nest.ZeroTermsQuery" /> can be used,
56+
/// which accepts <see cref="Nest.ZeroTermsQuery.None" /> (default) and <see cref="Nest.ZeroTermsQuery.All" />
57+
/// which corresponds to a match_all query.
58+
/// </summary>
59+
[DataMember(Name = "zero_terms_query")]
60+
ZeroTermsQuery? ZeroTermsQuery { get; set; }
61+
}
62+
63+
/// <inheritdoc cref="ICombinedFieldsQuery" />
64+
[DataContract]
65+
public class CombinedFieldsQuery : QueryBase, ICombinedFieldsQuery
66+
{
67+
/// <inheritdoc />
68+
public string Query { get; set; }
69+
/// <inheritdoc />
70+
public Fields Fields { get; set; }
71+
/// <inheritdoc />
72+
public MinimumShouldMatch MinimumShouldMatch { get; set; }
73+
/// <inheritdoc />
74+
public bool? AutoGenerateSynonymsPhraseQuery { get; set; }
75+
/// <inheritdoc />
76+
public Operator? Operator { get; set; }
77+
/// <inheritdoc />
78+
public ZeroTermsQuery? ZeroTermsQuery { get; set; }
79+
80+
protected override bool Conditionless => IsConditionless(this);
81+
82+
internal override void InternalWrapInContainer(IQueryContainer c) => c.CombinedFields = this;
83+
84+
internal static bool IsConditionless(ICombinedFieldsQuery q) => q.Fields.IsConditionless() || q.Query.IsNullOrEmpty();
85+
}
86+
87+
public class CombinedFieldsQueryDescriptor<T>
88+
: QueryDescriptorBase<CombinedFieldsQueryDescriptor<T>, ICombinedFieldsQuery>, ICombinedFieldsQuery where T : class
89+
{
90+
protected override bool Conditionless => CombinedFieldsQuery.IsConditionless(this);
91+
92+
string ICombinedFieldsQuery.Query { get; set; }
93+
Fields ICombinedFieldsQuery.Fields { get; set; }
94+
MinimumShouldMatch ICombinedFieldsQuery.MinimumShouldMatch { get; set; }
95+
bool? ICombinedFieldsQuery.AutoGenerateSynonymsPhraseQuery { get; set; }
96+
Operator? ICombinedFieldsQuery.Operator { get; set; }
97+
ZeroTermsQuery? ICombinedFieldsQuery.ZeroTermsQuery { get; set; }
98+
99+
/// <inheritdoc cref="ICombinedFieldsQuery.Query" />
100+
public CombinedFieldsQueryDescriptor<T> Query(string query) => Assign(query, (a, v) => a.Query = v);
101+
102+
/// <inheritdoc cref="ICombinedFieldsQuery.Fields" />
103+
public CombinedFieldsQueryDescriptor<T> Fields(Func<FieldsDescriptor<T>, IPromise<Fields>> fields) =>
104+
Assign(fields, (a, v) => a.Fields = v?.Invoke(new FieldsDescriptor<T>())?.Value);
105+
106+
/// <inheritdoc cref="ICombinedFieldsQuery.Fields" />
107+
public CombinedFieldsQueryDescriptor<T> Fields(Fields fields) => Assign(fields, (a, v) => a.Fields = v);
108+
109+
/// <inheritdoc cref="ICombinedFieldsQuery.MinimumShouldMatch" />
110+
public CombinedFieldsQueryDescriptor<T> MinimumShouldMatch(MinimumShouldMatch minimumShouldMatch)
111+
=> Assign(minimumShouldMatch, (a, v) => a.MinimumShouldMatch = v);
112+
113+
/// <inheritdoc cref="ICombinedFieldsQuery.Operator" />
114+
public CombinedFieldsQueryDescriptor<T> Operator(Operator? op) => Assign(op, (a, v) => a.Operator = v);
115+
116+
/// <inheritdoc cref="ICombinedFieldsQuery.ZeroTermsQuery" />
117+
public CombinedFieldsQueryDescriptor<T> ZeroTermsQuery(ZeroTermsQuery? zeroTermsQuery) => Assign(zeroTermsQuery, (a, v) => a.ZeroTermsQuery = v);
118+
119+
/// <inheritdoc cref="ICombinedFieldsQuery.AutoGenerateSynonymsPhraseQuery" />
120+
public CombinedFieldsQueryDescriptor<T> AutoGenerateSynonymsPhraseQuery(bool? autoGenerateSynonymsPhraseQuery = true) =>
121+
Assign(autoGenerateSynonymsPhraseQuery, (a, v) => a.AutoGenerateSynonymsPhraseQuery = v);
122+
}
123+
}

src/Nest/QueryDsl/Query.cs

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// Licensed to Elasticsearch B.V under one or more agreements.
2-
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3-
// See the LICENSE file in the project root for more information
4-
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
55
using System;
66
using System.Linq.Expressions;
77

@@ -205,5 +205,8 @@ public static QueryContainer Wildcard(Func<WildcardQueryDescriptor<T>, IWildcard
205205
public static QueryContainer Pinned(Func<PinnedQueryDescriptor<T>, IPinnedQuery> selector) =>
206206
new QueryContainerDescriptor<T>().Pinned(selector);
207207

208+
public static QueryContainer CombinedFields(Func<CombinedFieldsQueryDescriptor<T>, ICombinedFieldsQuery> selector) =>
209+
new QueryContainerDescriptor<T>().CombinedFields(selector);
210+
208211
}
209212
}

src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// Licensed to Elasticsearch B.V under one or more agreements.
2-
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3-
// See the LICENSE file in the project root for more information
4-
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
55
using System;
66
using System.Collections.Generic;
77
using System.Linq;
@@ -215,6 +215,8 @@ private void WriteShape(IGeoShape shape, IFieldLookup indexedField, Field field,
215215

216216
public virtual void Visit(IPinnedQuery query) => Write("pinned");
217217

218+
public virtual void Visit(ICombinedFieldsQuery query) => Write("combined_fields");
219+
218220
private void Write(string queryType, Dictionary<string, string> properties)
219221
{
220222
properties = properties ?? new Dictionary<string, string>();

src/Nest/QueryDsl/Visitor/QueryVisitor.cs

+4
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ public interface IQueryVisitor
151151
void Visit(ITermsSetQuery query);
152152

153153
void Visit(IPinnedQuery query);
154+
155+
void Visit(ICombinedFieldsQuery query);
154156
}
155157

156158
public class QueryVisitor : IQueryVisitor
@@ -287,6 +289,8 @@ public virtual void Visit(ITermsSetQuery query) { }
287289

288290
public virtual void Visit(IPinnedQuery query) { }
289291

292+
public virtual void Visit(ICombinedFieldsQuery query) { }
293+
290294
public virtual void Visit(IQueryVisitor visitor) { }
291295
}
292296
}

src/Nest/QueryDsl/Visitor/QueryWalker.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// Licensed to Elasticsearch B.V under one or more agreements.
2-
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3-
// See the LICENSE file in the project root for more information
4-
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
55
using System;
66
using System.Collections.Generic;
77
using System.Linq;
@@ -61,6 +61,7 @@ public void Walk(IQueryContainer qd, IQueryVisitor visitor)
6161
VisitQuery(qd.ParentId, visitor, (v, d) => v.Visit(d));
6262
VisitQuery(qd.TermsSet, visitor, (v, d) => v.Visit(d));
6363
VisitQuery(qd.Pinned, visitor, (v, d) => v.Visit(d));
64+
VisitQuery(qd.CombinedFields, visitor, (v, d) => v.Visit(d));
6465

6566
VisitQuery(qd.Bool, visitor, (v, d) =>
6667
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Elasticsearch.Xunit.XunitPlumbing;
6+
using Nest;
7+
using Tests.Core.ManagedElasticsearch.Clusters;
8+
using Tests.Domain;
9+
using Tests.Framework.EndpointTests.TestState;
10+
using static Nest.Infer;
11+
12+
namespace Tests.QueryDsl.FullText.CombinedFields
13+
{
14+
/**
15+
* The `combined_fields` query supports searching multiple text fields as if their contents had been indexed into one combined field. It takes a
16+
* term-centric view of the query: first it analyzes the query string into individual terms, then looks for each term in any of the fields.
17+
*
18+
* See the Elasticsearch documentation on {ref_current}/query-dsl-combined-fields-query.html[combined fields query] for more details.
19+
*/
20+
[SkipVersion("<7.13.0", "Implemented in version 7.13.0")]
21+
public class CombinedFieldsUsageTests : QueryDslUsageTestsBase
22+
{
23+
public CombinedFieldsUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
24+
25+
protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen<ICombinedFieldsQuery>(a => a.CombinedFields)
26+
{
27+
q => q.Query = null,
28+
q => q.Query = string.Empty
29+
};
30+
31+
protected override QueryContainer QueryInitializer => new CombinedFieldsQuery
32+
{
33+
Fields = Field<Project>(p => p.Description).And("myOtherField"),
34+
Query = "hello world",
35+
Boost = 1.1,
36+
Operator = Operator.Or,
37+
MinimumShouldMatch = "2",
38+
ZeroTermsQuery = ZeroTermsQuery.All,
39+
Name = "combined_fields",
40+
AutoGenerateSynonymsPhraseQuery = false
41+
};
42+
43+
protected override object QueryJson => new
44+
{
45+
combined_fields = new
46+
{
47+
_name = "combined_fields",
48+
boost = 1.1,
49+
query = "hello world",
50+
minimum_should_match = "2",
51+
@operator = "or",
52+
fields = new[]
53+
{
54+
"description",
55+
"myOtherField"
56+
},
57+
zero_terms_query = "all",
58+
auto_generate_synonyms_phrase_query = false
59+
}
60+
};
61+
62+
protected override QueryContainer QueryFluent(QueryContainerDescriptor<Project> q) => q
63+
.CombinedFields(c => c
64+
.Fields(f => f.Field(p => p.Description).Field("myOtherField"))
65+
.Query("hello world")
66+
.Boost(1.1)
67+
.Operator(Operator.Or)
68+
.MinimumShouldMatch("2")
69+
.ZeroTermsQuery(ZeroTermsQuery.All)
70+
.Name("combined_fields")
71+
.AutoGenerateSynonymsPhraseQuery(false)
72+
);
73+
}
74+
75+
/**[float]
76+
* === Combined fields with boost usage
77+
*/
78+
[SkipVersion("<7.13.0", "Implemented in version 7.13.0")]
79+
public class CombinedFieldsWithBoostUsageTests : QueryDslUsageTestsBase
80+
{
81+
public CombinedFieldsWithBoostUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
82+
83+
protected override QueryContainer QueryInitializer => new CombinedFieldsQuery
84+
{
85+
Fields = Field<Project>(p => p.Description, 2.2).And("myOtherField^1.2"),
86+
Query = "hello world",
87+
};
88+
89+
protected override object QueryJson => new
90+
{
91+
combined_fields = new
92+
{
93+
query = "hello world",
94+
fields = new[]
95+
{
96+
"description^2.2",
97+
"myOtherField^1.2"
98+
}
99+
}
100+
};
101+
102+
protected override QueryContainer QueryFluent(QueryContainerDescriptor<Project> q) => q
103+
.CombinedFields(c => c
104+
.Fields(Field<Project>(p => p.Description, 2.2).And("myOtherField^1.2"))
105+
.Query("hello world")
106+
);
107+
}
108+
}

0 commit comments

Comments
 (0)