diff --git a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs index 7613319e3f1..5b85cebb6e0 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs @@ -185,6 +185,9 @@ public interface IQueryContainer [DataMember(Name = "pinned")] IPinnedQuery Pinned { get; set; } + /// + [DataMember(Name = "combined_fields")] + ICombinedFieldsQuery CombinedFields { get; set; } void Accept(IQueryVisitor visitor); } diff --git a/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs b/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs index 29fda74500a..96ddec955bb 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs @@ -1,7 +1,7 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + using System; using System.Runtime.Serialization; @@ -64,6 +64,7 @@ public partial class QueryContainer : IQueryContainer, IDescriptor private IWildcardQuery _wildcard; private IRankFeatureQuery _rankFeature; private IPinnedQuery _pinned; + private ICombinedFieldsQuery _combinedFieldsQuery; [IgnoreDataMember] private IQueryContainer Self => this; @@ -385,6 +386,11 @@ IPinnedQuery IQueryContainer.Pinned set => _pinned = Set(value); } + ICombinedFieldsQuery IQueryContainer.CombinedFields + { + get => _combinedFieldsQuery; + set => _combinedFieldsQuery = Set(value); + } private T Set(T value) where T : IQuery { diff --git a/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs b/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs index c638a07de9e..4b635833b13 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs @@ -492,5 +492,8 @@ public QueryContainer TermsSet(Func, ITermsSetQuery> public QueryContainer Pinned(Func, IPinnedQuery> selector) => WrapInContainer(selector, (query, container) => container.Pinned = query); + + public QueryContainer CombinedFields(Func, ICombinedFieldsQuery> selector) => + WrapInContainer(selector, (query, container) => container.CombinedFields = query); } } diff --git a/src/Nest/QueryDsl/FullText/CombinedFields/CombinedFieldsQuery.cs b/src/Nest/QueryDsl/FullText/CombinedFields/CombinedFieldsQuery.cs new file mode 100644 index 00000000000..5bcbea9980d --- /dev/null +++ b/src/Nest/QueryDsl/FullText/CombinedFields/CombinedFieldsQuery.cs @@ -0,0 +1,123 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using System.Runtime.Serialization; +using Nest.Utf8Json; + +namespace Nest +{ + [InterfaceDataContract] + [ReadAs(typeof(CombinedFieldsQuery))] + public interface ICombinedFieldsQuery : IQuery + { + /// + /// The query to execute + /// + [DataMember(Name = "query")] + string Query { get; set; } + + /// + /// The fields to perform the query against. + /// + [DataMember(Name = "fields")] + Fields Fields { get; set; } + + /// + /// A value controlling how many "should" clauses in the resulting boolean query should match. + /// It can be an absolute value, a percentage or a combination of both. + /// + [DataMember(Name = "minimum_should_match")] + MinimumShouldMatch MinimumShouldMatch { get; set; } + + /// + /// If `true`, match phrase queries are automatically created for multi-term synonyms. + /// + [DataMember(Name = "auto_generate_synonyms_phrase_query")] + bool? AutoGenerateSynonymsPhraseQuery { get; set; } + + /// + /// The operator used if no explicit operator is specified. + /// The default operator is + /// + /// + /// and types are field-centric?; + /// they generate a match query per field. This means that and + /// are applied to each field individually, which is probably not what you want. + /// Consider using . + /// + [DataMember(Name = "operator")] + Operator? Operator { get; set; } + + /// + /// If the analyzer used removes all tokens in a query like a stop filter does, the default behavior is + /// to match no documents at all. In order to change that, can be used, + /// which accepts (default) and + /// which corresponds to a match_all query. + /// + [DataMember(Name = "zero_terms_query")] + ZeroTermsQuery? ZeroTermsQuery { get; set; } + } + + /// + [DataContract] + public class CombinedFieldsQuery : QueryBase, ICombinedFieldsQuery + { + /// + public string Query { get; set; } + /// + public Fields Fields { get; set; } + /// + public MinimumShouldMatch MinimumShouldMatch { get; set; } + /// + public bool? AutoGenerateSynonymsPhraseQuery { get; set; } + /// + public Operator? Operator { get; set; } + /// + public ZeroTermsQuery? ZeroTermsQuery { get; set; } + + protected override bool Conditionless => IsConditionless(this); + + internal override void InternalWrapInContainer(IQueryContainer c) => c.CombinedFields = this; + + internal static bool IsConditionless(ICombinedFieldsQuery q) => q.Fields.IsConditionless() || q.Query.IsNullOrEmpty(); + } + + public class CombinedFieldsQueryDescriptor + : QueryDescriptorBase, ICombinedFieldsQuery>, ICombinedFieldsQuery where T : class + { + protected override bool Conditionless => CombinedFieldsQuery.IsConditionless(this); + + string ICombinedFieldsQuery.Query { get; set; } + Fields ICombinedFieldsQuery.Fields { get; set; } + MinimumShouldMatch ICombinedFieldsQuery.MinimumShouldMatch { get; set; } + bool? ICombinedFieldsQuery.AutoGenerateSynonymsPhraseQuery { get; set; } + Operator? ICombinedFieldsQuery.Operator { get; set; } + ZeroTermsQuery? ICombinedFieldsQuery.ZeroTermsQuery { get; set; } + + /// + public CombinedFieldsQueryDescriptor Query(string query) => Assign(query, (a, v) => a.Query = v); + + /// + public CombinedFieldsQueryDescriptor Fields(Func, IPromise> fields) => + Assign(fields, (a, v) => a.Fields = v?.Invoke(new FieldsDescriptor())?.Value); + + /// + public CombinedFieldsQueryDescriptor Fields(Fields fields) => Assign(fields, (a, v) => a.Fields = v); + + /// + public CombinedFieldsQueryDescriptor MinimumShouldMatch(MinimumShouldMatch minimumShouldMatch) + => Assign(minimumShouldMatch, (a, v) => a.MinimumShouldMatch = v); + + /// + public CombinedFieldsQueryDescriptor Operator(Operator? op) => Assign(op, (a, v) => a.Operator = v); + + /// + public CombinedFieldsQueryDescriptor ZeroTermsQuery(ZeroTermsQuery? zeroTermsQuery) => Assign(zeroTermsQuery, (a, v) => a.ZeroTermsQuery = v); + + /// + public CombinedFieldsQueryDescriptor AutoGenerateSynonymsPhraseQuery(bool? autoGenerateSynonymsPhraseQuery = true) => + Assign(autoGenerateSynonymsPhraseQuery, (a, v) => a.AutoGenerateSynonymsPhraseQuery = v); + } +} diff --git a/src/Nest/QueryDsl/Query.cs b/src/Nest/QueryDsl/Query.cs index aae59ad000b..77c4d054c02 100644 --- a/src/Nest/QueryDsl/Query.cs +++ b/src/Nest/QueryDsl/Query.cs @@ -1,7 +1,7 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + using System; using System.Linq.Expressions; @@ -205,5 +205,8 @@ public static QueryContainer Wildcard(Func, IWildcard public static QueryContainer Pinned(Func, IPinnedQuery> selector) => new QueryContainerDescriptor().Pinned(selector); + public static QueryContainer CombinedFields(Func, ICombinedFieldsQuery> selector) => + new QueryContainerDescriptor().CombinedFields(selector); + } } diff --git a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs index a5c832498e1..59fc0120981 100644 --- a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs @@ -215,6 +215,8 @@ private void WriteShape(IGeoShape shape, IFieldLookup indexedField, Field field, public virtual void Visit(IPinnedQuery query) => Write("pinned"); + public virtual void Visit(ICombinedFieldsQuery query) => Write("combined_fields"); + private void Write(string queryType, Dictionary properties) { properties = properties ?? new Dictionary(); diff --git a/src/Nest/QueryDsl/Visitor/QueryVisitor.cs b/src/Nest/QueryDsl/Visitor/QueryVisitor.cs index 041c9c02509..51092a477da 100644 --- a/src/Nest/QueryDsl/Visitor/QueryVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/QueryVisitor.cs @@ -151,6 +151,8 @@ public interface IQueryVisitor void Visit(ITermsSetQuery query); void Visit(IPinnedQuery query); + + void Visit(ICombinedFieldsQuery query); } public class QueryVisitor : IQueryVisitor @@ -287,6 +289,8 @@ public virtual void Visit(ITermsSetQuery query) { } public virtual void Visit(IPinnedQuery query) { } + public virtual void Visit(ICombinedFieldsQuery query) { } + public virtual void Visit(IQueryVisitor visitor) { } } } diff --git a/src/Nest/QueryDsl/Visitor/QueryWalker.cs b/src/Nest/QueryDsl/Visitor/QueryWalker.cs index 43066faebb3..e4160632b6a 100644 --- a/src/Nest/QueryDsl/Visitor/QueryWalker.cs +++ b/src/Nest/QueryDsl/Visitor/QueryWalker.cs @@ -1,7 +1,7 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + using System; using System.Collections.Generic; using System.Linq; @@ -61,6 +61,7 @@ public void Walk(IQueryContainer qd, IQueryVisitor visitor) VisitQuery(qd.ParentId, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.TermsSet, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.Pinned, visitor, (v, d) => v.Visit(d)); + VisitQuery(qd.CombinedFields, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.Bool, visitor, (v, d) => { diff --git a/tests/Tests/QueryDsl/FullText/CombinedFields/CombinedFieldsUsageTests.cs b/tests/Tests/QueryDsl/FullText/CombinedFields/CombinedFieldsUsageTests.cs new file mode 100644 index 00000000000..80b98c44c60 --- /dev/null +++ b/tests/Tests/QueryDsl/FullText/CombinedFields/CombinedFieldsUsageTests.cs @@ -0,0 +1,108 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Elasticsearch.Xunit.XunitPlumbing; +using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; +using Tests.Framework.EndpointTests.TestState; +using static Nest.Infer; + +namespace Tests.QueryDsl.FullText.CombinedFields +{ + /** + * The `combined_fields` query supports searching multiple text fields as if their contents had been indexed into one combined field. It takes a + * 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. + * + * See the Elasticsearch documentation on {ref_current}/query-dsl-combined-fields-query.html[combined fields query] for more details. + */ + [SkipVersion("<7.13.0", "Implemented in version 7.13.0")] + public class CombinedFieldsUsageTests : QueryDslUsageTestsBase + { + public CombinedFieldsUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.CombinedFields) + { + q => q.Query = null, + q => q.Query = string.Empty + }; + + protected override QueryContainer QueryInitializer => new CombinedFieldsQuery + { + Fields = Field(p => p.Description).And("myOtherField"), + Query = "hello world", + Boost = 1.1, + Operator = Operator.Or, + MinimumShouldMatch = "2", + ZeroTermsQuery = ZeroTermsQuery.All, + Name = "combined_fields", + AutoGenerateSynonymsPhraseQuery = false + }; + + protected override object QueryJson => new + { + combined_fields = new + { + _name = "combined_fields", + boost = 1.1, + query = "hello world", + minimum_should_match = "2", + @operator = "or", + fields = new[] + { + "description", + "myOtherField" + }, + zero_terms_query = "all", + auto_generate_synonyms_phrase_query = false + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .CombinedFields(c => c + .Fields(f => f.Field(p => p.Description).Field("myOtherField")) + .Query("hello world") + .Boost(1.1) + .Operator(Operator.Or) + .MinimumShouldMatch("2") + .ZeroTermsQuery(ZeroTermsQuery.All) + .Name("combined_fields") + .AutoGenerateSynonymsPhraseQuery(false) + ); + } + + /**[float] + * === Combined fields with boost usage + */ + [SkipVersion("<7.13.0", "Implemented in version 7.13.0")] + public class CombinedFieldsWithBoostUsageTests : QueryDslUsageTestsBase + { + public CombinedFieldsWithBoostUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override QueryContainer QueryInitializer => new CombinedFieldsQuery + { + Fields = Field(p => p.Description, 2.2).And("myOtherField^1.2"), + Query = "hello world", + }; + + protected override object QueryJson => new + { + combined_fields = new + { + query = "hello world", + fields = new[] + { + "description^2.2", + "myOtherField^1.2" + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .CombinedFields(c => c + .Fields(Field(p => p.Description, 2.2).And("myOtherField^1.2")) + .Query("hello world") + ); + } +}