From 36d921078dc7bfb34f56492fc3e0907bf1b8716d Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Tue, 15 Oct 2019 10:04:50 +0200 Subject: [PATCH] Search enhancement: pinned queries (#4143) * Search enhancement: pinned queries Addresses elastic/elasticsearch#44345 * update docs in test * remove params overloads on Ids that already convert to Ids * add note to IdsQuery --- .../Abstractions/Container/IQueryContainer.cs | 5 ++ .../Container/QueryContainer-Assignments.cs | 7 ++ .../Container/QueryContainerDescriptor.cs | 3 + src/Nest/QueryDsl/Query.cs | 4 ++ .../Specialized/Pinned/PinnedQuery.cs | 55 +++++++++++++++ src/Nest/QueryDsl/TermLevel/Ids/IdsQuery.cs | 1 + .../QueryDsl/Visitor/DslPrettyPrintVisitor.cs | 2 + src/Nest/QueryDsl/Visitor/QueryVisitor.cs | 4 ++ src/Nest/QueryDsl/Visitor/QueryWalker.cs | 1 + .../Pinned/PinnedQueryUsageTests.cs | 70 +++++++++++++++++++ 10 files changed, 152 insertions(+) create mode 100644 src/Nest/QueryDsl/Specialized/Pinned/PinnedQuery.cs create mode 100644 src/Tests/Tests/QueryDsl/Specialized/Pinned/PinnedQueryUsageTests.cs diff --git a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs index 9c93207d60b..4d3c329e390 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs @@ -171,6 +171,11 @@ public interface IQueryContainer [DataMember(Name = "distance_feature")] IDistanceFeatureQuery DistanceFeature { get; set; } + /// + [DataMember(Name = "pinned")] + IPinnedQuery Pinned { 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 6037a140510..92bcb150715 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs @@ -57,6 +57,7 @@ public partial class QueryContainer : IQueryContainer, IDescriptor private ITermsSetQuery _termsSet; private IWildcardQuery _wildcard; private IRankFeatureQuery _rankFeature; + private IPinnedQuery _pinned; [IgnoreDataMember] private IQueryContainer Self => this; @@ -360,6 +361,12 @@ IRankFeatureQuery IQueryContainer.RankFeature get => _rankFeature; set => _rankFeature = Set(value); } + IPinnedQuery IQueryContainer.Pinned + { + get => _pinned; + set => _pinned = 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 90360748570..75527d61459 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs @@ -475,5 +475,8 @@ public QueryContainer ParentId(Func, IParentIdQuery> /// public QueryContainer TermsSet(Func, ITermsSetQuery> selector) => WrapInContainer(selector, (query, container) => container.TermsSet = query); + + public QueryContainer Pinned(Func, IPinnedQuery> selector) => + WrapInContainer(selector, (query, container) => container.Pinned = query); } } diff --git a/src/Nest/QueryDsl/Query.cs b/src/Nest/QueryDsl/Query.cs index a83fa748fdf..ddca4ecff2e 100644 --- a/src/Nest/QueryDsl/Query.cs +++ b/src/Nest/QueryDsl/Query.cs @@ -190,5 +190,9 @@ public static QueryContainer Wildcard(Field field, string value, double? boost = public static QueryContainer Wildcard(Func, IWildcardQuery> selector) => new QueryContainerDescriptor().Wildcard(selector); + + public static QueryContainer Pinned(Func, IPinnedQuery> selector) => + new QueryContainerDescriptor().Pinned(selector); + } } diff --git a/src/Nest/QueryDsl/Specialized/Pinned/PinnedQuery.cs b/src/Nest/QueryDsl/Specialized/Pinned/PinnedQuery.cs new file mode 100644 index 00000000000..1e92d1077e9 --- /dev/null +++ b/src/Nest/QueryDsl/Specialized/Pinned/PinnedQuery.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Elasticsearch.Net.Utf8Json; + +namespace Nest +{ + [InterfaceDataContract] + [ReadAs(typeof(PinnedQuery))] + public interface IPinnedQuery : IQuery + { + [DataMember(Name = "ids")] + IEnumerable Ids { get; set; } + + [DataMember(Name = "organic")] + QueryContainer Organic { get; set; } + } + + public class PinnedQuery : QueryBase, IPinnedQuery + { + public IEnumerable Ids { get; set; } + + public QueryContainer Organic { get; set; } + + protected override bool Conditionless => IsConditionless(this); + + internal override void InternalWrapInContainer(IQueryContainer c) => c.Pinned = this; + + internal static bool IsConditionless(IPinnedQuery q) => !q.Ids.HasAny() && q.Organic.IsConditionless(); + } + + public class PinnedQueryDescriptor + : QueryDescriptorBase, IPinnedQuery> + , IPinnedQuery + where T : class + { + protected override bool Conditionless => PinnedQuery.IsConditionless(this); + IEnumerable IPinnedQuery.Ids { get; set; } + QueryContainer IPinnedQuery.Organic { get; set; } + + public PinnedQueryDescriptor Ids(params Id[] ids) => Assign(ids, (a, v) => a.Ids = v); + + public PinnedQueryDescriptor Ids(IEnumerable ids) => Ids(ids?.ToArray()); + + public PinnedQueryDescriptor Ids(IEnumerable ids) => Ids(ids.ToArray()); + + public PinnedQueryDescriptor Ids(IEnumerable ids) => Ids(ids.ToArray()); + + public PinnedQueryDescriptor Ids(IEnumerable ids) => Ids(ids.ToArray()); + + public PinnedQueryDescriptor Organic(Func, QueryContainer> selector) => + Assign(selector, (a, v) => a.Organic = v?.Invoke(new QueryContainerDescriptor())); + } +} diff --git a/src/Nest/QueryDsl/TermLevel/Ids/IdsQuery.cs b/src/Nest/QueryDsl/TermLevel/Ids/IdsQuery.cs index e0725632ffb..648227a70a5 100644 --- a/src/Nest/QueryDsl/TermLevel/Ids/IdsQuery.cs +++ b/src/Nest/QueryDsl/TermLevel/Ids/IdsQuery.cs @@ -35,6 +35,7 @@ public class IdsQueryDescriptor public IdsQueryDescriptor Values(IEnumerable values) => Values(values?.ToArray()); + // TODO 8.x remove params on Values already implicitly converting to Id public IdsQueryDescriptor Values(params string[] values) => Assign(values?.Select(v => (Id)v), (a, v) => a.Values = v); public IdsQueryDescriptor Values(IEnumerable values) => Values(values.ToArray()); diff --git a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs index 16b113f21db..89178348149 100644 --- a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs @@ -203,6 +203,8 @@ public virtual void Visit(IGeoShapeQuery query) public virtual void Visit(ITermsSetQuery query) => Write("terms_set"); + public virtual void Visit(IPinnedQuery query) => Write("pinned"); + 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 673efcd7c3d..daa99edad09 100644 --- a/src/Nest/QueryDsl/Visitor/QueryVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/QueryVisitor.cs @@ -141,6 +141,8 @@ public interface IQueryVisitor void Visit(IParentIdQuery query); void Visit(ITermsSetQuery query); + + void Visit(IPinnedQuery query); } public class QueryVisitor : IQueryVisitor @@ -271,6 +273,8 @@ public virtual void Visit(IParentIdQuery query) { } public virtual void Visit(ITermsSetQuery query) { } + public virtual void Visit(IPinnedQuery query) { } + public virtual void Visit(IQueryVisitor visitor) { } } } diff --git a/src/Nest/QueryDsl/Visitor/QueryWalker.cs b/src/Nest/QueryDsl/Visitor/QueryWalker.cs index 4632b245531..86eb5d67e96 100644 --- a/src/Nest/QueryDsl/Visitor/QueryWalker.cs +++ b/src/Nest/QueryDsl/Visitor/QueryWalker.cs @@ -54,6 +54,7 @@ public void Walk(IQueryContainer qd, IQueryVisitor visitor) VisitQuery(qd.Percolate, visitor, (v, d) => v.Visit(d)); 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.Bool, visitor, (v, d) => { diff --git a/src/Tests/Tests/QueryDsl/Specialized/Pinned/PinnedQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Specialized/Pinned/PinnedQueryUsageTests.cs new file mode 100644 index 00000000000..ae35e22e9b2 --- /dev/null +++ b/src/Tests/Tests/QueryDsl/Specialized/Pinned/PinnedQueryUsageTests.cs @@ -0,0 +1,70 @@ +using System; +using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; +using Tests.Framework.EndpointTests.TestState; + +#pragma warning disable 618 //Testing an obsolete method + +namespace Tests.QueryDsl.Specialized.Pinned +{ + /** + * Promotes selected documents to rank higher than those matching a given query. This feature is typically used to + * guide searchers to curated documents that are promoted over and above any "organic" matches for a search. The promoted or "pinned" + * documents are identified using the document IDs stored in the _id field. + * See the Elasticsearch documentation on {ref_current}/query-dsl-pinned-query.html[pinned query] for more details. + */ + public class PinnedQueryUsageTests : QueryDslUsageTestsBase + { + public PinnedQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.Pinned) + { + q => + { + q.Ids = null; + q.Organic = null; + }, + q => + { + q.Ids = Array.Empty(); + q.Organic = ConditionlessQuery; + }, + }; + + protected override NotConditionlessWhen NotConditionlessWhen => new NotConditionlessWhen(a => a.Pinned) + { + q => q.Organic = VerbatimQuery, + }; + + protected override QueryContainer QueryInitializer => new PinnedQuery() + { + Name = "named_query", + Boost = 1.1, + Organic = new MatchAllQuery { Name = "organic_query" }, + Ids = new Id[] { 1,11,22 }, + }; + + protected override object QueryJson => new + { + pinned = new + { + _name = "named_query", + boost = 1.1, + organic = new + { + match_all = new { _name = "organic_query" } + }, + ids = new [] { 1, 11, 22}, + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .Pinned(c => c + .Name("named_query") + .Boost(1.1) + .Organic(qq => qq.MatchAll(m => m.Name("organic_query"))) + .Ids(1, 11, 22) + ); + } +}