Skip to content

Add support for combined fields query #5619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ public interface IQueryContainer
[DataMember(Name = "pinned")]
IPinnedQuery Pinned { get; set; }

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

void Accept(IQueryVisitor visitor);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -385,6 +386,11 @@ IPinnedQuery IQueryContainer.Pinned
set => _pinned = Set(value);
}

ICombinedFieldsQuery IQueryContainer.CombinedFields
{
get => _combinedFieldsQuery;
set => _combinedFieldsQuery = Set(value);
}

private T Set<T>(T value) where T : IQuery
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -492,5 +492,8 @@ public QueryContainer TermsSet(Func<TermsSetQueryDescriptor<T>, ITermsSetQuery>

public QueryContainer Pinned(Func<PinnedQueryDescriptor<T>, IPinnedQuery> selector) =>
WrapInContainer(selector, (query, container) => container.Pinned = query);

public QueryContainer CombinedFields(Func<CombinedFieldsQueryDescriptor<T>, ICombinedFieldsQuery> selector) =>
WrapInContainer(selector, (query, container) => container.CombinedFields = query);
}
}
123 changes: 123 additions & 0 deletions src/Nest/QueryDsl/FullText/CombinedFields/CombinedFieldsQuery.cs
Original file line number Diff line number Diff line change
@@ -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 Elasticsearch.Net.Utf8Json;

namespace Nest
{
[InterfaceDataContract]
[ReadAs(typeof(CombinedFieldsQuery))]
public interface ICombinedFieldsQuery : IQuery
{
/// <summary>
/// The query to execute
/// </summary>
[DataMember(Name = "query")]
string Query { get; set; }

/// <summary>
/// The fields to perform the query against.
/// </summary>
[DataMember(Name = "fields")]
Fields Fields { get; set; }

/// <summary>
/// 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.
/// </summary>
[DataMember(Name = "minimum_should_match")]
MinimumShouldMatch MinimumShouldMatch { get; set; }

/// <summary>
/// If `true`, match phrase queries are automatically created for multi-term synonyms.
/// </summary>
[DataMember(Name = "auto_generate_synonyms_phrase_query")]
bool? AutoGenerateSynonymsPhraseQuery { get; set; }

/// <summary>
/// The operator used if no explicit operator is specified.
/// The default operator is <see cref="Nest.Operator.Or" />
/// </summary>
/// <remarks>
/// <see cref="TextQueryType.BestFields" /> and <see cref="TextQueryType.MostFields" /> types are field-centric?;
/// they generate a match query per field. This means that <see cref="Operator" /> and <see cref="MinimumShouldMatch" />
/// are applied to each field individually, which is probably not what you want.
/// Consider using <see cref="TextQueryType.CrossFields" />.
/// </remarks>
[DataMember(Name = "operator")]
Operator? Operator { get; set; }

/// <summary>
/// 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, <see cref="Nest.ZeroTermsQuery" /> can be used,
/// which accepts <see cref="Nest.ZeroTermsQuery.None" /> (default) and <see cref="Nest.ZeroTermsQuery.All" />
/// which corresponds to a match_all query.
/// </summary>
[DataMember(Name = "zero_terms_query")]
ZeroTermsQuery? ZeroTermsQuery { get; set; }
}

/// <inheritdoc cref="ICombinedFieldsQuery" />
[DataContract]
public class CombinedFieldsQuery : QueryBase, ICombinedFieldsQuery
{
/// <inheritdoc />
public string Query { get; set; }
/// <inheritdoc />
public Fields Fields { get; set; }
/// <inheritdoc />
public MinimumShouldMatch MinimumShouldMatch { get; set; }
/// <inheritdoc />
public bool? AutoGenerateSynonymsPhraseQuery { get; set; }
/// <inheritdoc />
public Operator? Operator { get; set; }
/// <inheritdoc />
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<T>
: QueryDescriptorBase<CombinedFieldsQueryDescriptor<T>, 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; }

/// <inheritdoc cref="ICombinedFieldsQuery.Query" />
public CombinedFieldsQueryDescriptor<T> Query(string query) => Assign(query, (a, v) => a.Query = v);

/// <inheritdoc cref="ICombinedFieldsQuery.Fields" />
public CombinedFieldsQueryDescriptor<T> Fields(Func<FieldsDescriptor<T>, IPromise<Fields>> fields) =>
Assign(fields, (a, v) => a.Fields = v?.Invoke(new FieldsDescriptor<T>())?.Value);

/// <inheritdoc cref="ICombinedFieldsQuery.Fields" />
public CombinedFieldsQueryDescriptor<T> Fields(Fields fields) => Assign(fields, (a, v) => a.Fields = v);

/// <inheritdoc cref="ICombinedFieldsQuery.MinimumShouldMatch" />
public CombinedFieldsQueryDescriptor<T> MinimumShouldMatch(MinimumShouldMatch minimumShouldMatch)
=> Assign(minimumShouldMatch, (a, v) => a.MinimumShouldMatch = v);

/// <inheritdoc cref="ICombinedFieldsQuery.Operator" />
public CombinedFieldsQueryDescriptor<T> Operator(Operator? op) => Assign(op, (a, v) => a.Operator = v);

/// <inheritdoc cref="ICombinedFieldsQuery.ZeroTermsQuery" />
public CombinedFieldsQueryDescriptor<T> ZeroTermsQuery(ZeroTermsQuery? zeroTermsQuery) => Assign(zeroTermsQuery, (a, v) => a.ZeroTermsQuery = v);

/// <inheritdoc cref="ICombinedFieldsQuery.AutoGenerateSynonymsPhraseQuery" />
public CombinedFieldsQueryDescriptor<T> AutoGenerateSynonymsPhraseQuery(bool? autoGenerateSynonymsPhraseQuery = true) =>
Assign(autoGenerateSynonymsPhraseQuery, (a, v) => a.AutoGenerateSynonymsPhraseQuery = v);
}
}
11 changes: 7 additions & 4 deletions src/Nest/QueryDsl/Query.cs
Original file line number Diff line number Diff line change
@@ -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;

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

public static QueryContainer CombinedFields(Func<CombinedFieldsQueryDescriptor<T>, ICombinedFieldsQuery> selector) =>
new QueryContainerDescriptor<T>().CombinedFields(selector);

}
}
10 changes: 6 additions & 4 deletions src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<string, string> properties)
{
properties = properties ?? new Dictionary<string, string>();
Expand Down
4 changes: 4 additions & 0 deletions src/Nest/QueryDsl/Visitor/QueryVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ public interface IQueryVisitor
void Visit(ITermsSetQuery query);

void Visit(IPinnedQuery query);

void Visit(ICombinedFieldsQuery query);
}

public class QueryVisitor : IQueryVisitor
Expand Down Expand Up @@ -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) { }
}
}
9 changes: 5 additions & 4 deletions src/Nest/QueryDsl/Visitor/QueryWalker.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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) =>
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ICombinedFieldsQuery>(a => a.CombinedFields)
{
q => q.Query = null,
q => q.Query = string.Empty
};

protected override QueryContainer QueryInitializer => new CombinedFieldsQuery
{
Fields = Field<Project>(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<Project> 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<Project>(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<Project> q) => q
.CombinedFields(c => c
.Fields(Field<Project>(p => p.Description, 2.2).And("myOtherField^1.2"))
.Query("hello world")
);
}
}