elasticsearch-net icon indicating copy to clipboard operation
elasticsearch-net copied to clipboard

JsonNetSerializer - Failed to serialize anonymous type: Nest.SearchDescriptor

Open MackMendes opened this issue 1 year ago • 4 comments

NEST/Elasticsearch.Net version: 7.17.5

Elasticsearch version: 7.17.2

.NET runtime version: 6.0.203

Operating system version: Windows 11

Description of the problem including expected versus actual behavior:

I'm trying to serialize an object that represents a Function Score. This function score has the following structure:

{   "size": 100,
    "query": {
        "bool": {
            "must": [
                {
                    "function_score": {
                        "functions": [
                            {
                                "live_scorer": {
                                    "target_field_name": "product_id",
                                    "endpoint": "{endpoint}",
                                    "params": "{\"parameter\": \"value\"}"
                                },
                                "weight": 1.0
                            }
                        ],
                        "score_mode": "max"
                    }
                }
            ]
        }
    }
}

To represent this structure, a class and interface were created like this:


public class LiveScorerScoreFunction : ILiveScorerScoreFunction
{
    public LiveScorerScoreFunction(Dictionary<string, object> fields, double weight)
    {
        this.Fields = fields;
        this.Weight = weight;
    }

    [DataMember(Name = "live_scorer")]
    public Dictionary<string, object> Fields { get; set; }

    [DataMember(Name = "filter")]
    public QueryContainer Filter { get; set ; }

    [DataMember(Name = "weight")]
    public double? Weight { get; set; }        
}

[JsonFormatter(typeof(LiveScorerFormatter))]
public interface ILiveScorerScoreFunction : IScoreFunction
{
    [DataMember(Name = "live_scorer")]
    [JsonFormatter(typeof(Dictionary<string, object>))]
    Dictionary<string, object> Fields { get; set; }
}

And about JsonFormatter, it was created like this with the following Serialize method:

public class LiveScorerFormatter : IJsonFormatter<ILiveScorerScoreFunction>
{
    private static readonly AutomataDictionary AutomataDictionary = new AutomataDictionary
    {
        { "params", 0 }
    };

    public ILiveScorerScoreFunction Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
    {
          ...
    }

    public void Serialize(ref JsonWriter writer, ILiveScorerScoreFunction value, IJsonFormatterResolver formatterResolver)
    {
        if (value == null
            || value.Fields == null
            || value.Fields.Count == 0)
        {
            writer.WriteNull();
            return;
        }

        var dictionaryFormatter = DynamicGenericResolver
            .Instance
            .GetFormatter<Dictionary<string, object>>();

        dictionaryFormatter.Serialize(ref writer, value.Fields, formatterResolver);
    }
}

This is how the query is being set up e.g.:

private IScoreFunction FunctionLiveScore(string parameterValue, double weight = 1)
{
    var liveScorerFields = new Dictionary<string, object> {
        { "target_field_name", "product_id"},
        { "endpoint", @"http://localhost:6639/v1/"},
        { "params", string.Format("{{ \"contextId\": \"{0}\" }}", parameterValue)}
    };

    return new LiveScorerScoreFunction(liveScorerFields, weight);
}

public SearchDescriptor<Product> CreateSearchDescriptor(string parameterValue)
{
    var queryContainer = Query<Product>
                         .FunctionScore(
                             fs => fs
                                 .Functions(new List<IScoreFunction>() { this.CreateFunctionLiveScore(parameterValue) })
                                 .ScoreMode(FunctionScoreMode.Max));

    return new SearchDescriptor<Product>()
        .Index("indexName")
        .Size(100)
        .DocValueFields(t => t.Field("product_id"))
        .Query(
            q => q.Bool(
                b => b
                    .Must(queryContainer)));
}

And when I try to run this code, I get the following error:

{
  "AnonymousType": "Nest.SearchDescriptor`1[[Domain.Model.Product, Domain.Model, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Nest, Version=7.0.0.0, Culture=neutral, PublicKeyToken=96c599bbe3e70f5d",
  "Message": "Failed to serialize anonymous type: Nest.SearchDescriptor`1[Domain.Model.Product].",
  "Data": {},
  "InnerException": {
    "ClassName": "System.Exception",
    "Message": "Can not write function score json for LiveScorerScoreFunction",
    "Data": {},
    "InnerException": null,
    "HelpURL": null,
    "StackTraceString": "   at Nest.ScoreFunctionJsonFormatter.Serialize(JsonWriter& writer, IScoreFunction value, IJsonFormatterResolver formatterResolver)\r\n   at Elasticsearch.Net.Utf8Json.Formatters.CollectionFormatterBase`4.Serialize(JsonWriter& writer, TCollection value, IJsonFormatterResolver formatterResolver)\r\n   at Elasticsearch.Net.Nest_IFunctionScoreQueryFormatter3.Serialize(JsonWriter& , IFunctionScoreQuery , IJsonFormatterResolver )\r\n   at Elasticsearch.Net.Nest_IQueryContainerFormatter1.Serialize(JsonWriter& , IQueryContainer , IJsonFormatterResolver )\r\n   at Nest.QueryContainerCollectionFormatter.Serialize(JsonWriter& writer, IEnumerable`1 value, IJsonFormatterResolver formatterResolver)\r\n   at Elasticsearch.Net.Nest_IBoolQueryFormatter2.Serialize(JsonWriter& , IBoolQuery , IJsonFormatterResolver )\r\n   at Elasticsearch.Net.Nest_IQueryContainerFormatter1.Serialize(JsonWriter& , IQueryContainer , IJsonFormatterResolver )\r\n   at Serialize(Byte[][] , Object[] , JsonWriter& , SearchDescriptor`1 , IJsonFormatterResolver )\r\n   at Elasticsearch.Net.Utf8Json.Resolvers.DynamicMethodAnonymousFormatter`1.Serialize(JsonWriter& writer, T value, IJsonFormatterResolver formatterResolver)",
    "RemoteStackTraceString": null,
    "RemoteStackIndex": 0,
    "ExceptionMethod": null,
    "HResult": -2146233088,
    "Source": "Nest",
    "WatsonBuckets": null
  },
  "HelpLink": null,
  "Source": "Elasticsearch.Net",
  "HResult": -2146233076,
  "StackTrace": "   at Elasticsearch.Net.Utf8Json.Resolvers.DynamicMethodAnonymousFormatter`1.Serialize(JsonWriter& writer, T value, IJsonFormatterResolver formatterResolver)\r\n   at Elasticsearch.Net.Utf8Json.JsonSerializer.SerializeUnsafe[T](T value, IJsonFormatterResolver resolver)\r\n   at Elasticsearch.Net.Utf8Json.JsonSerializer.Serialize[T](Stream stream, T value, IJsonFormatterResolver resolver)\r\n   at Elasticsearch.Net.DiagnosticsSerializerProxy.Serialize[T](T data, Stream stream, SerializationFormatting formatting)\r\n   at Elasticsearch.Net.ElasticsearchSerializerExtensions.SerializeToString[T](IElasticsearchSerializer serializer, T data, IMemoryStreamFactory memoryStreamFactory, SerializationFormatting formatting)\r\n   at Elasticsearch.Net.ElasticsearchSerializerExtensions.SerializeToString[T](IElasticsearchSerializer serializer, T data, SerializationFormatting formatting)\r\n   at Data.Elasticsearch.ElasticsearchCluster.SearchAsync(Int32 tenantId, String contextId, Boolean isFreeTextSearch) in C:\\Users\\username\\source\\Playground\\custom-function-score-elasticsearch-using-nest\\src\\Data.Elasticsearch\\ElasticsearchCluster.cs:line 49"
}

I tried an implementation based on the guidelines in the documentation: https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/custom-serialization.html and created the ConnectionSettings below however, the serializer it had created to be able to serialize this untriggered object and an empty JSON is returned.

Provide ConnectionSettings (if relevant):

new ConnectionSettings(pool,
sourceSerializer: (builtin, settings) =>
    new JsonNetSerializer(builtin, settings,
      () => new JsonSerializerSettings { NullValueHandling = NullValueHandling.Include },
      (connectionSettingsAwareContractResolver) => { new LiveScoreFunctionNetSerializer(connectionSettingsAwareContractResolver); }));

Expected behavior Should serialize the object using the JsonFormatter (LiveScorerFormatter) that was created. Or should trigger the custom serializer (LiveScoreFunctionNetSerializer) that was created, via ConnectionSettings.

MackMendes avatar Sep 19 '23 23:09 MackMendes

Hi @MackMendes

just to clarify. If you set a breakpoint on your custom Serialize function, it doesn't get hit?

flobernd avatar Sep 28 '23 11:09 flobernd

Hi @flobernd,

Thank you for your interaction on this issue. 🙂

Unfortunately, it doesn't hit the Serialize methods:

  • void Serialize<T>(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.Indented)
  • Task SerializeAsync<T>(T data, Stream stream, SerializationFormatting formatting = SerializationFormatting.None, CancellationToken cancellationToken = default)

We're implementing the IElasticsearchSerializer interface, as recommended on the document: https://www.elastic.co/guide/en/elasticsearch/client/net-api/7.17/custom-serialization.html#_injecting_a_new_serializer.

However, If I put the breakpoint in the constructor of the class, it is hit twice. And looking at the Call Stack, these hits come from the methods:

Nest.JsonNetSerializer.dll!Nest.JsonNetSerializer.ConnectionSettingsAwareSerializerBase.ConnectionSettingsAwareSerializerBase(Elasticsearch.Net.IElasticsearchSerializer builtinSerializer, Nest.IConnectionSettingsValues connectionSettings, System.Func<Newtonsoft.Json.JsonSerializerSettings> jsonSerializerSettingsFactory, System.Action<Nest.JsonNetSerializer.ConnectionSettingsAwareContractResolver> modifyContractResolver, System.Collections.Generic.IEnumerable<Newtonsoft.Json.JsonConverter> contractJsonConverters) Line 38 C#

Nest.JsonNetSerializer.dll!Nest.JsonNetSerializer.ConnectionSettingsAwareSerializerBase.ConnectionSettingsAwareSerializerBase(Elasticsearch.Net.IElasticsearchSerializer builtinSerializer, Nest.IConnectionSettingsValues connectionSettings, System.Func<Newtonsoft.Json.JsonSerializerSettings> jsonSerializerSettingsFactory, System.Action<Nest.JsonNetSerializer.ConnectionSettingsAwareContractResolver> modifyContractResolver, System.Collections.Generic.IEnumerable<Newtonsoft.Json.JsonConverter> contractJsonConverters) Line 39 C#

And then in the execution flow of the method:

await this.elasticClient.SearchAsync<Product>(searchDescriptor);

Or even if I try to serialize using the following method, it's never hit:

JObject.Parse(this.elasticClient.ConnectionSettings.RequestResponseSerializer.SerializeToString(searchDescriptor, SerializationFormatting.Indented))

The same behavior happens with Formatter (LiveScorerFormatter).

Do you have any idea what could be the problem?

In advance, thank you for your help.

MackMendes avatar Sep 29 '23 19:09 MackMendes

Hi @MackMendes,

please have a look at this comment: https://github.com/elastic/elasticsearch-net/issues/3517#issuecomment-473359744

This describes how to achieve your goal in a slightly different/easier way.

flobernd avatar Oct 05 '23 10:10 flobernd

Hi @flobernd,

I had already tried this approach and when I tried it the following error occurred:

Unhandled exception. Elasticsearch.Net.UnexpectedElasticsearchClientException: Can not write function score json for LiveScorerScoreFunction
 ---> System.Exception: Can not write function score json for LiveScorerScoreFunction
   at Nest.ScoreFunctionJsonFormatter.Serialize(JsonWriter& writer, IScoreFunction value, IJsonFormatterResolver formatterResolver)
   at Elasticsearch.Net.Utf8Json.Formatters.CollectionFormatterBase`4.Serialize(JsonWriter& writer, TCollection value, IJsonFormatterResolver formatterResolver)
   at Elasticsearch.Net.Nest_IFunctionScoreQueryFormatter3.Serialize(JsonWriter& , IFunctionScoreQuery , IJsonFormatterResolver )
   at Elasticsearch.Net.Nest_IQueryContainerFormatter1.Serialize(JsonWriter& , IQueryContainer , IJsonFormatterResolver )
   at Elasticsearch.Net.Nest_ISearchRequestFormatter4.Serialize(JsonWriter& , ISearchRequest , IJsonFormatterResolver )
   at Elasticsearch.Net.Utf8Json.JsonSerializer.SerializeUnsafe[T](T value, IJsonFormatterResolver resolver)
   at Elasticsearch.Net.Utf8Json.JsonSerializer.Serialize[T](Stream stream, T value, IJsonFormatterResolver resolver)
   at Elasticsearch.Net.DiagnosticsSerializerProxy.Serialize[T](T data, Stream stream, SerializationFormatting formatting)
   at Elasticsearch.Net.SerializableData`1.Write(Stream writableStream, IConnectionConfigurationValues settings)
   at Elasticsearch.Net.HttpConnection.SetContent(HttpRequestMessage message, RequestData requestData)
   at Elasticsearch.Net.HttpConnection.Request[TResponse](RequestData requestData)
   at Elasticsearch.Net.RequestPipeline.CallElasticsearch[TResponse](RequestData requestData)
   at Elasticsearch.Net.Transport`1.Request[TResponse](HttpMethod method, String path, PostData data, IRequestParameters requestParameters)
   --- End of inner exception stack trace ---
   at Elasticsearch.Net.Transport`1.Request[TResponse](HttpMethod method, String path, PostData data, IRequestParameters requestParameters)
   at Nest.ElasticClient.Search[TDocument](ISearchRequest request)
   at Data.Elasticsearch.ElasticsearchCluster.SearchAsync(Int32 tenantId, String contextId, Boolean isFreeTextSearch) in C:\Users\user-name\source\Playground\Elasticsearch-CustomScoreFunction\src\Data.Elasticsearch\ElasticsearchCluster.cs:line 21
   at Program.<Main>$(String[] args) in C:\Users\user-name\source\Playground\Elasticsearch-CustomScoreFunction\src\CustomScoreFunctionElasticsearch\Program.cs:line 10
   at Program.<Main>(String[] args)

I've put this test in my GitHub repository:

  • https://github.com/MackMendes/Elasticsearch-CustomScoreFunction

Do you have any idea what could be the problem?

MackMendes avatar Oct 06 '23 17:10 MackMendes