From dd6c997820dd62f460d7d7483a5dcc8b3769cac9 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Tue, 9 Jul 2019 13:58:49 +1000 Subject: [PATCH] Add script_score query support (#3919) This commit adds script_score query support to the high level client Closes #3843 (cherry picked from commit a4741949bd50da2210d7699f0485961cf09fecd8) --- docs/query-dsl.asciidoc | 4 + .../function-score-query-usage.asciidoc | 57 ++++++-- .../script-score-query-usage.asciidoc | 102 +++++++++++++++ .../DocGenerator/StringExtensions.cs | 3 +- .../Abstractions/Container/IQueryContainer.cs | 4 + .../Container/QueryContainer-Assignments.cs | 7 + .../Container/QueryContainerDescriptor.cs | 3 + src/Nest/QueryDsl/Query.cs | 4 + .../ScriptScore/ScriptScoreQuery.cs | 77 +++++++++++ .../QueryDsl/Visitor/DslPrettyPrintVisitor.cs | 2 + src/Nest/QueryDsl/Visitor/QueryVisitor.cs | 4 + src/Nest/QueryDsl/Visitor/QueryWalker.cs | 1 + .../ScriptScore/ScriptScoreQueryUsageTests.cs | 122 ++++++++++++++++++ 13 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 docs/query-dsl/specialized/script-score/script-score-query-usage.asciidoc create mode 100644 src/Nest/QueryDsl/Specialized/ScriptScore/ScriptScoreQuery.cs create mode 100644 src/Tests/Tests/QueryDsl/Specialized/ScriptScore/ScriptScoreQueryUsageTests.cs diff --git a/docs/query-dsl.asciidoc b/docs/query-dsl.asciidoc index f007d3898e2..cd7bbc48004 100644 --- a/docs/query-dsl.asciidoc +++ b/docs/query-dsl.asciidoc @@ -275,6 +275,8 @@ Specialized types of queries that do not fit into other groups * <> +* <> + See the Elasticsearch documentation on {ref_current}/specialized-queries.html[Specialized queries] for more details. :includes-from-dirs: query-dsl/specialized @@ -287,6 +289,8 @@ include::query-dsl/specialized/percolate/percolate-query-usage.asciidoc[] include::query-dsl/specialized/script/script-query-usage.asciidoc[] +include::query-dsl/specialized/script-score/script-score-query-usage.asciidoc[] + [[span-queries]] == Span queries diff --git a/docs/query-dsl/compound/function-score/function-score-query-usage.asciidoc b/docs/query-dsl/compound/function-score/function-score-query-usage.asciidoc index 0a35c4916b3..7cd30fba882 100644 --- a/docs/query-dsl/compound/function-score/function-score-query-usage.asciidoc +++ b/docs/query-dsl/compound/function-score/function-score-query-usage.asciidoc @@ -29,15 +29,36 @@ q .MaxBoost(20.0) .MinScore(1.0) .Functions(f => f - .Exponential(b => b.Field(p => p.NumberOfCommits).Decay(0.5).Origin(1.0).Scale(0.1).Weight(2.1)) + .Exponential(b => b + .Field(p => p.NumberOfCommits) + .Decay(0.5) + .Origin(1.0) + .Scale(0.1) + .Weight(2.1) + .Filter(fi => fi + .Range(r => r + .Field(p => p.NumberOfContributors) + .GreaterThan(10) + ) + ) + ) .GaussDate(b => b.Field(p => p.LastActivity).Origin(DateMath.Now).Decay(0.5).Scale("1d")) - .LinearGeoLocation(b => - b.Field(p => p.LocationPoint).Origin(new GeoLocation(70, -70)).Scale(Distance.Miles(1)).MultiValueMode(MultiValueMode.Average)) + .LinearGeoLocation(b => b + .Field(p => p.LocationPoint) + .Origin(new GeoLocation(70, -70)) + .Scale(Distance.Miles(1)) + .MultiValueMode(MultiValueMode.Average) + ) .FieldValueFactor(b => b.Field(p => p.NumberOfContributors).Factor(1.1).Missing(0.1).Modifier(FieldValueFactorModifier.Square)) .RandomScore(r => r.Seed(1337).Field("_seq_no")) .RandomScore(r => r.Seed("randomstring").Field("_seq_no")) .Weight(1.0) - .ScriptScore(s => s.Script(ss => ss.Source("Math.log(2 + doc['numberOfCommits'].value)"))) + .ScriptScore(s => s + .Script(ss => ss + .Source("Math.log(2 + doc['numberOfCommits'].value)") + ) + .Weight(2) + ) ) ) ---- @@ -57,7 +78,19 @@ new FunctionScoreQuery() MinScore = 1.0, Functions = new List { - new ExponentialDecayFunction { Origin = 1.0, Decay = 0.5, Field = Field(p => p.NumberOfCommits), Scale = 0.1, Weight = 2.1 }, + new ExponentialDecayFunction + { + Origin = 1.0, + Decay = 0.5, + Field = Field(p => p.NumberOfCommits), + Scale = 0.1, + Weight = 2.1, + Filter = new NumericRangeQuery + { + Field = Field(f => f.NumberOfContributors), + GreaterThan = 10 + } + }, new GaussDateDecayFunction { Origin = DateMath.Now, Field = Field(p => p.LastActivity), Decay = 0.5, Scale = TimeSpan.FromDays(1) }, new LinearGeoDecayFunction @@ -72,7 +105,7 @@ new FunctionScoreQuery() new RandomScoreFunction { Seed = 1337, Field = "_seq_no" }, new RandomScoreFunction { Seed = "randomstring", Field = "_seq_no" }, new WeightFunction { Weight = 1.0 }, - new ScriptScoreFunction { Script = new InlineScript("Math.log(2 + doc['numberOfCommits'].value)") } + new ScriptScoreFunction { Script = new InlineScript("Math.log(2 + doc['numberOfCommits'].value)"), Weight = 2.0 } } } ---- @@ -94,7 +127,14 @@ new FunctionScoreQuery() "decay": 0.5 } }, - "weight": 2.1 + "weight": 2.1, + "filter": { + "range": { + "numberOfContributors": { + "gt": 10.0 + } + } + } }, { "gauss": { @@ -145,7 +185,8 @@ new FunctionScoreQuery() "script": { "source": "Math.log(2 + doc['numberOfCommits'].value)" } - } + }, + "weight": 2.0 } ], "max_boost": 20.0, diff --git a/docs/query-dsl/specialized/script-score/script-score-query-usage.asciidoc b/docs/query-dsl/specialized/script-score/script-score-query-usage.asciidoc new file mode 100644 index 00000000000..c99e29fb06a --- /dev/null +++ b/docs/query-dsl/specialized/script-score/script-score-query-usage.asciidoc @@ -0,0 +1,102 @@ +:ref_current: https://www.elastic.co/guide/en/elasticsearch/reference/7.0 + +:github: https://github.com/elastic/elasticsearch-net + +:nuget: https://www.nuget.org/packages + +//// +IMPORTANT NOTE +============== +This file has been generated from https://github.com/elastic/elasticsearch-net/tree/master/src/Tests/Tests/QueryDsl/Specialized/ScriptScore/ScriptScoreQueryUsageTests.cs. +If you wish to submit a PR for any spelling mistakes, typos or grammatical errors for this file, +please modify the original csharp file found at the link and submit the PR with that change. Thanks! +//// + +[[script-score-query-usage]] +=== Script Score Query Usage + +A query allowing you to modify the score of documents that are retrieved by a query. +This can be useful if, for example, a score function is computationally expensive and +it is sufficient to compute the score on a filtered set of documents. + +See the Elasticsearch documentation on {ref_current}/query-dsl-script-score-query.html[script_score query] for more details. + +==== Fluent DSL example + +[source,csharp] +---- +q +.ScriptScore(sn => sn + .Name("named_query") + .Boost(1.1) + .Query(qq => qq + .Range(r => r + .Field(f => f.NumberOfCommits) + .GreaterThan(50) + ) + ) + .Script(s => s + .Source(_scriptScoreSource) + .Params(p => p + .Add("origin", 100) + .Add("scale", 10) + .Add("decay", 0.5) + .Add("offset", 0) + ) + ) +) +---- + +==== Object Initializer syntax example + +[source,csharp] +---- +new ScriptScoreQuery +{ + Name = "named_query", + Boost = 1.1, + Query = new NumericRangeQuery + { + Field = Infer.Field(f => f.NumberOfCommits), + GreaterThan = 50 + }, + Script = new InlineScript(_scriptScoreSource) + { + Params = new Dictionary + { + { "origin", 100 }, + { "scale", 10 }, + { "decay", 0.5 }, + { "offset", 0 } + } + }, +} +---- + +[source,javascript] +.Example json output +---- +{ + "script_score": { + "_name": "named_query", + "boost": 1.1, + "query": { + "range": { + "numberOfCommits": { + "gt": 50.0 + } + } + }, + "script": { + "source": "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['numberOfCommits'].value)", + "params": { + "origin": 100, + "scale": 10, + "decay": 0.5, + "offset": 0 + } + } + } +} +---- + diff --git a/src/CodeGeneration/DocGenerator/StringExtensions.cs b/src/CodeGeneration/DocGenerator/StringExtensions.cs index 0e461d1ffff..25fe8da0987 100644 --- a/src/CodeGeneration/DocGenerator/StringExtensions.cs +++ b/src/CodeGeneration/DocGenerator/StringExtensions.cs @@ -114,7 +114,8 @@ public static class StringExtensions new []{ new [] {8.2, 18.2}, new [] {8.2, -18.8}, new [] {-8.8, -10.8}, new [] {8.8, 18.2}} }" }, - { "ProjectFilterExpectedJson", "new {term = new {type = new {value = \"project\"}}}" } + { "ProjectFilterExpectedJson", "new {term = new {type = new {value = \"project\"}}}" }, + { "_scriptScoreSource", "\"decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['numberOfCommits'].value)\""} }; private static readonly Regex LeadingSpacesAndAsterisk = new Regex(@"^(?[ \t]*\*\s?).*", RegexOptions.Compiled); diff --git a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs index 0a28e8d6181..81b7aa43690 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/IQueryContainer.cs @@ -115,6 +115,10 @@ public interface IQueryContainer [DataMember(Name ="script")] IScriptQuery Script { get; set; } + /// + [DataMember(Name ="script_score")] + IScriptScoreQuery ScriptScore { get; set; } + [DataMember(Name ="simple_query_string")] ISimpleQueryStringQuery SimpleQueryString { get; set; } diff --git a/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs b/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs index 15024c1fa55..0481cd3efeb 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainer-Assignments.cs @@ -38,6 +38,7 @@ public partial class QueryContainer : IQueryContainer, IDescriptor private IRawQuery _raw; private IRegexpQuery _regexp; private IScriptQuery _script; + private IScriptScoreQuery _scriptScore; private ISimpleQueryStringQuery _simpleQueryString; private ISpanContainingQuery _spanContaining; private ISpanFieldMaskingQuery _spanFieldMasking; @@ -251,6 +252,12 @@ IScriptQuery IQueryContainer.Script set => _script = Set(value); } + IScriptScoreQuery IQueryContainer.ScriptScore + { + get => _scriptScore; + set => _scriptScore = Set(value); + } + ISimpleQueryStringQuery IQueryContainer.SimpleQueryString { get => _simpleQueryString; diff --git a/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs b/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs index 2cd57ad0b19..ef57d8f59ae 100644 --- a/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs +++ b/src/Nest/QueryDsl/Abstractions/Container/QueryContainerDescriptor.cs @@ -437,6 +437,9 @@ public QueryContainer FunctionScore(Func, IFunct public QueryContainer Script(Func, IScriptQuery> selector) => WrapInContainer(selector, (query, container) => container.Script = query); + public QueryContainer ScriptScore(Func, IScriptScoreQuery> selector) => + WrapInContainer(selector, (query, container) => container.ScriptScore = query); + public QueryContainer Exists(Func, IExistsQuery> selector) => WrapInContainer(selector, (query, container) => container.Exists = query); diff --git a/src/Nest/QueryDsl/Query.cs b/src/Nest/QueryDsl/Query.cs index 048f1f41716..9917545ad76 100644 --- a/src/Nest/QueryDsl/Query.cs +++ b/src/Nest/QueryDsl/Query.cs @@ -117,6 +117,10 @@ public static QueryContainer Regexp(Func, IRegexpQuery> public static QueryContainer Script(Func, IScriptQuery> selector) => new QueryContainerDescriptor().Script(selector); + /// + public static QueryContainer ScriptScore(Func, IScriptScoreQuery> selector) => + new QueryContainerDescriptor().ScriptScore(selector); + public static QueryContainer SimpleQueryString(Func, ISimpleQueryStringQuery> selector) => new QueryContainerDescriptor().SimpleQueryString(selector); diff --git a/src/Nest/QueryDsl/Specialized/ScriptScore/ScriptScoreQuery.cs b/src/Nest/QueryDsl/Specialized/ScriptScore/ScriptScoreQuery.cs new file mode 100644 index 00000000000..b0e8d73bb1d --- /dev/null +++ b/src/Nest/QueryDsl/Specialized/ScriptScore/ScriptScoreQuery.cs @@ -0,0 +1,77 @@ +using System; +using System.Runtime.Serialization; +using Elasticsearch.Net.Utf8Json; + +namespace Nest +{ + /// + /// A query allowing you to modify the score of documents that are retrieved by a query. + /// This can be useful if, for example, a score function is computationally expensive and it is sufficient to + /// compute the score on a filtered set of documents. + /// + [ReadAs(typeof(ScriptScoreQuery))] + [InterfaceDataContract] + public interface IScriptScoreQuery : IQuery + { + /// + /// The query to execute + /// + [DataMember(Name = "query")] + QueryContainer Query { get; set; } + + /// + /// The script to execute + /// + [DataMember(Name = "script")] + IScript Script { get; set; } + } + + /// + public class ScriptScoreQuery : QueryBase, IScriptScoreQuery + { + /// + public QueryContainer Query { get; set; } + + /// + public IScript Script { get; set; } + + protected override bool Conditionless => IsConditionless(this); + + internal override void InternalWrapInContainer(IQueryContainer c) => c.ScriptScore = this; + + internal static bool IsConditionless(IScriptScoreQuery q) + { + if (q.Script == null || q.Query.IsConditionless()) + return true; + + switch (q.Script) + { + case IInlineScript inlineScript: + return inlineScript.Source.IsNullOrEmpty(); + case IIndexedScript indexedScript: + return indexedScript.Id.IsNullOrEmpty(); + } + + return false; + } + } + + /// + public class ScriptScoreQueryDescriptor + : QueryDescriptorBase, IScriptScoreQuery> + , IScriptScoreQuery where T : class + { + protected override bool Conditionless => ScriptScoreQuery.IsConditionless(this); + QueryContainer IScriptScoreQuery.Query { get; set; } + + IScript IScriptScoreQuery.Script { get; set; } + + /// + public ScriptScoreQueryDescriptor Query(Func, QueryContainer> selector) => + Assign(selector, (a, v) => a.Query = v?.Invoke(new QueryContainerDescriptor())); + + /// + public ScriptScoreQueryDescriptor Script(Func selector) => + Assign(selector, (a, v) => a.Script = v?.Invoke(new ScriptDescriptor())); + } +} diff --git a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs index b06a76780e7..68f0c599872 100644 --- a/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/DslPrettyPrintVisitor.cs @@ -187,6 +187,8 @@ public virtual void Visit(IGeoShapeQuery query) public virtual void Visit(IScriptQuery query) => Write("script"); + public virtual void Visit(IScriptScoreQuery query) => Write("script_score"); + public virtual void Visit(IRawQuery query) => Write("raw"); public virtual void Visit(IPercolateQuery query) => Write("percolate"); diff --git a/src/Nest/QueryDsl/Visitor/QueryVisitor.cs b/src/Nest/QueryDsl/Visitor/QueryVisitor.cs index dd5f757cb9e..492f2bc7bae 100644 --- a/src/Nest/QueryDsl/Visitor/QueryVisitor.cs +++ b/src/Nest/QueryDsl/Visitor/QueryVisitor.cs @@ -86,6 +86,8 @@ public interface IQueryVisitor void Visit(IScriptQuery query); + void Visit(IScriptScoreQuery query); + void Visit(IGeoPolygonQuery query); void Visit(IGeoDistanceQuery query); @@ -237,6 +239,8 @@ public virtual void Visit(ITermsQuery query) { } public virtual void Visit(IScriptQuery query) { } + public virtual void Visit(IScriptScoreQuery query) { } + public virtual void Visit(IGeoPolygonQuery query) { } public virtual void Visit(IGeoDistanceQuery query) { } diff --git a/src/Nest/QueryDsl/Visitor/QueryWalker.cs b/src/Nest/QueryDsl/Visitor/QueryWalker.cs index dc7e8ab7834..0af0664ffec 100644 --- a/src/Nest/QueryDsl/Visitor/QueryWalker.cs +++ b/src/Nest/QueryDsl/Visitor/QueryWalker.cs @@ -44,6 +44,7 @@ public void Walk(IQueryContainer qd, IQueryVisitor visitor) VisitQuery(qd.MatchPhrase, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.MatchPhrasePrefix, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.Script, visitor, (v, d) => v.Visit(d)); + VisitQuery(qd.ScriptScore, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.Exists, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.GeoPolygon, visitor, (v, d) => v.Visit(d)); VisitQuery(qd.GeoDistance, visitor, (v, d) => v.Visit(d)); diff --git a/src/Tests/Tests/QueryDsl/Specialized/ScriptScore/ScriptScoreQueryUsageTests.cs b/src/Tests/Tests/QueryDsl/Specialized/ScriptScore/ScriptScoreQueryUsageTests.cs new file mode 100644 index 00000000000..66ba1f04a48 --- /dev/null +++ b/src/Tests/Tests/QueryDsl/Specialized/ScriptScore/ScriptScoreQueryUsageTests.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using Nest; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; +using Tests.Framework.EndpointTests.TestState; + +namespace Tests.QueryDsl.Specialized.ScriptScore +{ + /** + * A query allowing you to modify the score of documents that are retrieved by a query. + * This can be useful if, for example, a score function is computationally expensive and + * it is sufficient to compute the score on a filtered set of documents. + * + * See the Elasticsearch documentation on {ref_current}/query-dsl-script-score-query.html[script_score query] for more details. + */ + public class ScriptScoreQueryUsageTests : QueryDslUsageTestsBase + { + private static readonly string _scriptScoreSource = "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['numberOfCommits'].value)"; + + public ScriptScoreQueryUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override ConditionlessWhen ConditionlessWhen => new ConditionlessWhen(a => a.ScriptScore) + { + q => + { + q.Query = null; + }, + q => + { + q.Script = null; + }, + q => + { + q.Script = new InlineScript(null); + }, + q => + { + q.Script = new InlineScript(""); + }, + q => + { + q.Script = new IndexedScript(null); + }, + q => + { + q.Script = new IndexedScript(""); + } + }; + + protected override QueryContainer QueryInitializer => new ScriptScoreQuery + { + Name = "named_query", + Boost = 1.1, + Query = new NumericRangeQuery + { + Field = Infer.Field(f => f.NumberOfCommits), + GreaterThan = 50 + }, + Script = new InlineScript(_scriptScoreSource) + { + Params = new Dictionary + { + { "origin", 100 }, + { "scale", 10 }, + { "decay", 0.5 }, + { "offset", 0 } + } + }, + }; + + protected override object QueryJson => new + { + script_score = new + { + _name = "named_query", + boost = 1.1, + query = new + { + range = new + { + numberOfCommits = new + { + gt = 50.0 + } + } + }, + script = new + { + source = _scriptScoreSource, + @params = new + { + origin = 100, + scale = 10, + decay = 0.5, + offset = 0 + } + } + } + }; + + protected override QueryContainer QueryFluent(QueryContainerDescriptor q) => q + .ScriptScore(sn => sn + .Name("named_query") + .Boost(1.1) + .Query(qq => qq + .Range(r => r + .Field(f => f.NumberOfCommits) + .GreaterThan(50) + ) + ) + .Script(s => s + .Source(_scriptScoreSource) + .Params(p => p + .Add("origin", 100) + .Add("scale", 10) + .Add("decay", 0.5) + .Add("offset", 0) + ) + ) + ); + } +}