/* SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. * * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ using System; using System.Runtime.Serialization; using OpenSearch.Net.Utf8Json; using OpenSearch.Net.Utf8Json.Internal; namespace OpenSearch.Client { /// /// A query that only works on rank_feature fields and rank_features fields. Its goal is to boost the score of documents /// based on the values of numeric features. It is typically put in a should clause of a bool query so that its score /// is added to the score of the query. /// /// Compared to using function_score or other ways to modify the score, this query has the benefit of being able to efficiently /// skip non-competitive hits when track_total_hits is not set to true. Speedups may be spectacular. /// [JsonFormatter(typeof(RankFeatureQueryFormatter))] [InterfaceDataContract] public interface IRankFeatureQuery : IFieldNameQuery { /// IRankFeatureFunction Function { get; set; } } public class RankFeatureQuery : FieldNameQueryBase, IRankFeatureQuery { protected override bool Conditionless => IsConditionless(this); internal static bool IsConditionless(IRankFeatureQuery q) => q.Field.IsConditionless(); internal override void InternalWrapInContainer(IQueryContainer container) => container.RankFeature = this; /// public IRankFeatureFunction Function { get; set; } } public class RankFeatureQueryDescriptor : FieldNameQueryDescriptorBase, IRankFeatureQuery, T> , IRankFeatureQuery where T : class { IRankFeatureFunction IRankFeatureQuery.Function { get; set; } protected override bool Conditionless => RankFeatureQuery.IsConditionless(this); /// public RankFeatureQueryDescriptor Saturation(Func selector = null) => Assign(selector, (a, v) => a.Function = v.InvokeOrDefault(new RankFeatureSaturationFunctionDescriptor())); /// public RankFeatureQueryDescriptor Logarithm(Func selector) => Assign(selector, (a, v) => a.Function = v?.Invoke(new RankFeatureLogarithmFunctionDescriptor())); /// public RankFeatureQueryDescriptor Sigmoid(Func selector) => Assign(selector, (a, v) => a.Function = v?.Invoke(new RankFeatureSigmoidFunctionDescriptor())); /// public RankFeatureQueryDescriptor Linear() { Self.Function = new RankFeatureLinearFunction(); return this; } } /// /// A function to boost scores in a rank_feature query, using the values of rank features. /// public interface IRankFeatureFunction { } /// /// Gives a score that is equal to log(scaling_factor + S) where S is the value of the rank feature and scaling_factor is a configurable /// scaling factor. Scores are unbounded. /// This function only supports rank features that have a positive score impact. /// public interface IRankFeatureLogarithmFunction : IRankFeatureFunction { /// /// The scaling factor /// [DataMember(Name = "scaling_factor")] float ScalingFactor { get; set; } } /// public class RankFeatureLogarithmFunction : IRankFeatureLogarithmFunction { /// public float ScalingFactor { get; set; } } /// public class RankFeatureLogarithmFunctionDescriptor : DescriptorBase, IRankFeatureLogarithmFunction { float IRankFeatureLogarithmFunction.ScalingFactor { get; set; } /// public RankFeatureLogarithmFunctionDescriptor ScalingFactor(float scalingFactor) => Assign(scalingFactor, (a, v) => a.ScalingFactor = v); } /// /// Gives a score that is equal to S / (S + pivot) where S is the value of the rank feature and pivot is a configurable pivot value /// so that the result will be less than 0.5 if S is less than pivot and greater than 0.5 otherwise. Scores are always is (0, 1). /// If the rank feature has a negative score impact then the function will be computed as pivot / (S + pivot), which decreases when S increases. /// public interface IRankFeatureSaturationFunction : IRankFeatureFunction { [DataMember(Name = "pivot")] float? Pivot { get; set; } } /// public class RankFeatureSaturationFunction : IRankFeatureSaturationFunction { public float? Pivot { get; set; } } /// public class RankFeatureSaturationFunctionDescriptor : DescriptorBase, IRankFeatureSaturationFunction { float? IRankFeatureSaturationFunction.Pivot { get; set; } /// public RankFeatureSaturationFunctionDescriptor Pivot(float? pivot) => Assign(pivot, (a, v) => a.Pivot = v); } /// /// is an extension of saturation which adds a configurable exponent. Scores are computed as S^exp^ / (S^exp^ + pivot^exp^). /// Like for the saturation function, pivot is the value of S that gives a score of 0.5 and scores are in (0, 1). /// /// exponent must be positive, but is typically in [0.5, 1]. A good value should be computed via training. If you don’t have the opportunity /// to do so, we recommend that you stick to the saturation function instead. /// public interface IRankFeatureSigmoidFunction : IRankFeatureFunction { [DataMember(Name = "pivot")] float Pivot { get; set; } /// /// The exponent. Must be positive /// [DataMember(Name = "exponent")] float Exponent { get; set; } } /// public class RankFeatureSigmoidFunction : IRankFeatureSigmoidFunction { public float Pivot { get; set; } /// public float Exponent { get; set; } } /// public class RankFeatureSigmoidFunctionDescriptor : DescriptorBase, IRankFeatureSigmoidFunction { float IRankFeatureSigmoidFunction.Exponent { get; set; } float IRankFeatureSigmoidFunction.Pivot { get; set; } /// public RankFeatureSigmoidFunctionDescriptor Exponent(float exponent) => Assign(exponent, (a, v) => a.Exponent = v); /// public RankFeatureSigmoidFunctionDescriptor Pivot(float pivot) => Assign(pivot, (a, v) => a.Pivot = v); } /// /// Gives a score equal to the indexed value of S, where S is the value of the rank feature field. /// /// If a rank feature field is indexed with "positive_score_impact": true, its indexed value is equal to S and rounded to preserve /// only 9 significant bits for the precision. /// /// If a rank feature field is indexed with "positive_score_impact": false, its indexed value is equal to 1/S and rounded to /// preserve only 9 significant bits for the precision. /// public interface IRankFeatureLinearFunction : IRankFeatureFunction { } /// public class RankFeatureLinearFunction : IRankFeatureLinearFunction { } internal class RankFeatureQueryFormatter : IJsonFormatter { public void Serialize(ref JsonWriter writer, IRankFeatureQuery value, IJsonFormatterResolver formatterResolver) { if (value == null) { writer.WriteNull(); return; } writer.WriteBeginObject(); if (!string.IsNullOrEmpty(value.Name)) { writer.WritePropertyName("_name"); writer.WriteString(value.Name); writer.WriteValueSeparator(); } if (value.Boost.HasValue) { writer.WritePropertyName("boost"); writer.WriteDouble(value.Boost.Value); writer.WriteValueSeparator(); } writer.WritePropertyName("field"); var fieldFormatter = formatterResolver.GetFormatter(); fieldFormatter.Serialize(ref writer, value.Field, formatterResolver); if (value.Function != null) { writer.WriteValueSeparator(); switch (value.Function) { case IRankFeatureSigmoidFunction sigmoid: SerializeScoreFunction(ref writer, "sigmoid", sigmoid, formatterResolver); break; case IRankFeatureSaturationFunction saturation: SerializeScoreFunction(ref writer, "saturation", saturation, formatterResolver); break; case IRankFeatureLogarithmFunction log: SerializeScoreFunction(ref writer, "log", log, formatterResolver); break; case IRankFeatureLinearFunction log: SerializeScoreFunction(ref writer, "linear", log, formatterResolver); break; } } writer.WriteEndObject(); } private static void SerializeScoreFunction(ref JsonWriter writer, string name, TScoreFunction scoreFunction, IJsonFormatterResolver formatterResolver ) where TScoreFunction : IRankFeatureFunction { writer.WritePropertyName(name); formatterResolver.GetFormatter() .Serialize(ref writer, scoreFunction, formatterResolver); } private static IRankFeatureFunction DeserializeScoreFunction(ref JsonReader reader, IJsonFormatterResolver formatterResolver) where TScoreFunction : IRankFeatureFunction => formatterResolver.GetFormatter().Deserialize(ref reader, formatterResolver); private static readonly AutomataDictionary Fields = new AutomataDictionary { { "_name", 0 }, { "boost", 1 }, { "field", 2 }, { "saturation", 3 }, { "log", 4 }, { "sigmoid", 5 }, { "linear", 6 } }; public IRankFeatureQuery Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) { if (reader.ReadIsNull()) return null; var query = new RankFeatureQuery(); var count = 0; while (reader.ReadIsInObject(ref count)) { if (Fields.TryGetValue(reader.ReadPropertyNameSegmentRaw(), out var value)) { switch (value) { case 0: query.Name = reader.ReadString(); break; case 1: query.Boost = reader.ReadDouble(); break; case 2: query.Field = formatterResolver.GetFormatter().Deserialize(ref reader, formatterResolver); break; case 3: query.Function = DeserializeScoreFunction(ref reader, formatterResolver); break; case 4: query.Function = DeserializeScoreFunction(ref reader, formatterResolver); break; case 5: query.Function = DeserializeScoreFunction(ref reader, formatterResolver); break; case 6: query.Function = DeserializeScoreFunction(ref reader, formatterResolver); break; } } else reader.ReadNextBlock(); } return query; } } }