Индекс ElasticSearch работает из REST API, но не кода C # - PullRequest
0 голосов
/ 27 января 2019

Я пытаюсь проиндексировать данные, которые включают геопоинты в Elastic Search.Когда я индексирую с помощью кода, это не удается.Когда я индексирую через конечные точки REST, это удается.Но я не могу найти разницу между JSON, который я отправляю через конечную точку REST, и JSON, отправляемым при использовании кода.

Вот код для настройки индекса (как программы LINQPad):

async Task Main()
{
    var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));
    var connectionSettings = new ConnectionSettings(pool)
        .DefaultMappingFor<DataEntity>(m => m.IndexName("data").TypeName("_doc"));

    var client = new ElasticClient(connectionSettings);

    await client.CreateIndexAsync(
        "data",
        index => index.Mappings(mappings => mappings.Map<DataEntity>(mapping => mapping.AutoMap().Properties(
            properties => properties.GeoPoint(field => field.Name(x => x.Location))))));

//    var data = new DataEntity(new GeoLocationEntity(50, 30));
//            
//    var json = client.RequestResponseSerializer.SerializeToString(data);
//    json.Dump("JSON");
//            
//    var indexResult = await client.IndexDocumentAsync(data);
//    indexResult.DebugInformation.Dump("Debug Information");
}

public sealed class GeoLocationEntity
{
    [JsonConstructor]
    public GeoLocationEntity(
        double latitude,
        double longitude)
    {
        this.Latitude = latitude;
        this.Longitude = longitude;
    }

    [JsonProperty("lat")]
    public double Latitude { get; }

    [JsonProperty("lon")]
    public double Longitude { get; }
}

public sealed class DataEntity
{
    [JsonConstructor]
    public DataEntity(
        GeoLocationEntity location)
    {
        this.Location = location;
    }

    [JsonProperty("location")]
    public GeoLocationEntity Location { get; }
}

После этого мое отображение выглядит корректно, поскольку GET /data/_doc/_mapping возвращает:

{
  "data" : {
    "mappings" : {
      "_doc" : {
        "properties" : {
          "location" : {
            "type" : "geo_point"
          }
        }
      }
    }
  }
}

Я могу успешно добавить документы в индекс через консоль разработчика:

POST /data/_doc
{
  "location": {
    "lat": 88.59,
    "lon": -98.87
  }
}

Результатыin:

{
  "_index" : "data",
  "_type" : "_doc",
  "_id" : "RqpyjGgBZ27KOduFRIxL",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 0,
  "_primary_term" : 1
}

Но когда я раскомментирую код в программе LINQPad выше и выполняю, я получаю эту ошибку при индексации:

Invalid NEST response built from a unsuccessful low level call on POST: /data/_doc
# Audit trail of this API call:
 - [1] BadResponse: Node: http://localhost:9200/ Took: 00:00:00.0159927
# OriginalException: Elasticsearch.Net.ElasticsearchClientException: The remote server returned an error: (400) Bad Request.. Call: Status code 400 from: POST /data/_doc. ServerError: Type: mapper_parsing_exception Reason: "failed to parse" CausedBy: "Type: parse_exception Reason: "field must be either [lat], [lon] or [geohash]"" ---> System.Net.WebException: The remote server returned an error: (400) Bad Request.
   at System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   at Elasticsearch.Net.HttpWebRequestConnection.<>c__DisplayClass5_0`1.<RequestAsync>b__1(IAsyncResult r)
   at System.Threading.Tasks.TaskFactory`1.FromAsyncCoreLogic(IAsyncResult iar, Func`2 endFunction, Action`1 endAction, Task`1 promise, Boolean requiresSynchronization)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
   at Elasticsearch.Net.HttpWebRequestConnection.<RequestAsync>d__5`1.MoveNext()
   --- End of inner exception stack trace ---
# Request:
<Request stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>
# Response:
<Response stream not captured or already read to completion by serializer. Set DisableDirectStreaming() on ConnectionSettings to force it to be set on the response.>

Выведенный JSON выглядит так:

{
  "location": {
    "latitude": 50.0,
    "longitude": 30.0
  }
}

Таким образом, он соответствует структуре JSON, которая работает с консоли разработчика.

Чтобы обойти эту проблему, я написал пользовательский JsonConverter, который сериализует мойGeoLocationEntity объекты в формате {lat},{lon}:

public sealed class GeoLocationConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) =>
        objectType == typeof(GeoLocationEntity);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var token = JToken.Load(reader);

        if (!(token is JValue))
        {
            throw new JsonSerializationException("Token was not a primitive.");
        }

        var stringValue = (string)token;
        var split = stringValue.Split(',');
        var latitude = double.Parse(split[0]);
        var longitude = double.Parse(split[1]);

        return new GeoLocationEntity(latitude, longitude);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var geoLocation = (GeoLocationEntity)value;

        if (geoLocation == null)
        {
            writer.WriteNull();
            return;
        }

        var geoLocationValue = $"{geoLocation.Latitude},{geoLocation.Longitude}";
        writer.WriteValue(geoLocationValue);
    }
}

Применение этого JsonConverter к настройкам сериализатора избавило меня от проблемы.Тем не менее, я не хочу обходить эту проблему вот так.

Может кто-нибудь просветить меня, как решить эту проблему?

1 Ответ

0 голосов
/ 27 января 2019

Клиент высокого уровня 6.x Elasticsearch, NEST, усвоил зависимость Json.NET на

  • IL-сливающаяся сборка Json.NET
  • преобразование всех типов в internal
  • переименуйте их в Nest.*

На практике это означает, что клиент не имеет прямой зависимости от Json.NET (прочитайте сообщение о выпуске в блоге , чтобы понять, почему мы это сделали) и не знает о Json Типы .NET, включая JsonPropertyAttribute или JsonConverter.

Есть несколько способов решения этой проблемы. Для начала, следующие настройки могут быть полезны при разработке

var defaultIndex = "default-index";
var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));

var settings = new ConnectionSettings(pool)
    .DefaultMappingFor<DataEntity>(m => m
        .IndexName(defaultIndex)
        .TypeName("_doc")
    )
    .DisableDirectStreaming()
    .PrettyJson()
    .OnRequestCompleted(callDetails =>
    {
        if (callDetails.RequestBodyInBytes != null)
        {
            Console.WriteLine(
                $"{callDetails.HttpMethod} {callDetails.Uri} \n" +
                $"{Encoding.UTF8.GetString(callDetails.RequestBodyInBytes)}");
        }
        else
        {
            Console.WriteLine($"{callDetails.HttpMethod} {callDetails.Uri}");
        }

        Console.WriteLine();

        if (callDetails.ResponseBodyInBytes != null)
        {
            Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
                     $"{Encoding.UTF8.GetString(callDetails.ResponseBodyInBytes)}\n" +
                     $"{new string('-', 30)}\n");
        }
        else
        {
            Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
                     $"{new string('-', 30)}\n");
        }
    });

var client = new ElasticClient(settings);

Это запишет все запросы и ответы на консоль, чтобы вы могли видеть, что клиент отправляет и получает от Elasticsearch. .DisableDirectStreaming() буферизует байты запросов и ответов в памяти, чтобы сделать их доступными для делегата, переданного в .OnRequestCompleted(), так что это полезно для разработки, но вы, вероятно, не захотите использовать его в производстве, так как это приводит к снижению производительности.

Теперь решения:

1. Используйте PropertyNameAttribute

Вместо использования JsonPropertyAttribute вы можете использовать PropertyNameAttribute для именования свойств для сериализации

public sealed class GeoLocationEntity
{
    public GeoLocationEntity(
        double latitude,
        double longitude)
    {
        this.Latitude = latitude;
        this.Longitude = longitude;
    }

    [PropertyName("lat")]
    public double Latitude { get; }

    [PropertyName("lon")]
    public double Longitude { get; }
}

public sealed class DataEntity
{
    public DataEntity(
        GeoLocationEntity location)
    {
        this.Location = location;
    }

    [PropertyName("location")]
    public GeoLocationEntity Location { get; }
}

и использовать

if (client.IndexExists(defaultIndex).Exists)
    client.DeleteIndex(defaultIndex);


var createIndexResponse = client.CreateIndex(defaultIndex, c => c 
    .Mappings(m => m
        .Map<DataEntity>(mm => mm
            .AutoMap()
            .Properties(p => p
                .GeoPoint(g => g
                    .Name(n => n.Location)
                )
            )
        )
    )
);

var indexResponse = client.Index(
    new DataEntity(new GeoLocationEntity(88.59, -98.87)), 
    i => i.Refresh(Refresh.WaitFor)
);

var searchResponse = client.Search<DataEntity>(s => s
    .Query(q => q
        .MatchAll()
    )
);

PropertyNameAttribute действует аналогично тому, как вы обычно используете JsonPropertAttribute с Json.NET.

2. Используйте DataMemberAttribute

В этом случае это будет работать так же, как и PropertyNameAttribute, если вы предпочитаете, чтобы ваши POCO не относились к типам NEST (хотя я бы сказал, что POCO привязаны к Elasticsearch, поэтому привязывайте их к .NET Типы Elasticsearch, вероятно, не проблема).

3. Используйте Geolocation тип

Вы можете заменить тип GeoLocationEntity типом Nest GeoLocation, который сопоставляется с отображением типа поля geo_point. При использовании этого POCO на единицу меньше, и правильное отображение может быть выведено из свойства типа

public sealed class DataEntity
{
    public DataEntity(
        GeoLocation location)
    {
        this.Location = location;
    }

    [DataMember(Name = "location")]
    public GeoLocation Location { get; }
}

// ---

if (client.IndexExists(defaultIndex).Exists)
    client.DeleteIndex(defaultIndex);

var createIndexResponse = client.CreateIndex(defaultIndex, c => c 
    .Mappings(m => m
        .Map<DataEntity>(mm => mm
            .AutoMap()
        )
    )
);

var indexResponse = client.Index(
    new DataEntity(new GeoLocation(88.59, -98.87)), 
    i => i.Refresh(Refresh.WaitFor)
);

var searchResponse = client.Search<DataEntity>(s => s
    .Query(q => q
        .MatchAll()
    )
);

4. Подключение JsonNetSerializer

NEST позволяет подключить пользовательский сериализатор , чтобы заботиться о сериализации ваших типов. Отдельный пакет nuget, NEST.JsonNetSerializer , позволяет использовать Json.NET для сериализации ваших типов, при этом сериализатор делегирует обратно внутреннему сериализатору свойства, относящиеся к типам NEST.

Сначала вам нужно передать JsonNetSerializer в ConnectionSettings конструктор

var settings = new ConnectionSettings(pool, JsonNetSerializer.Default)

Тогда ваш исходный код будет работать как положено, без пользовательского JsonConverter

public sealed class GeoLocationEntity
{
    public GeoLocationEntity(
        double latitude,
        double longitude)
    {
        this.Latitude = latitude;
        this.Longitude = longitude;
    }

    [JsonProperty("lat")]
    public double Latitude { get; }

    [JsonProperty("lon")]
    public double Longitude { get; }
}

public sealed class DataEntity
{
    public DataEntity(
        GeoLocationEntity location)
    {
        this.Location = location;
    }

    [JsonProperty("location")]
    public GeoLocationEntity Location { get; }
}


// ---

if (client.IndexExists(defaultIndex).Exists)
    client.DeleteIndex(defaultIndex);


var createIndexResponse = client.CreateIndex(defaultIndex, c => c 
    .Mappings(m => m
        .Map<DataEntity>(mm => mm
            .AutoMap()
            .Properties(p => p
                .GeoPoint(g => g
                    .Name(n => n.Location)
                )
            )
        )
    )
);

var indexResponse = client.Index(
    new DataEntity(new GeoLocationEntity(88.59, -98.87)), 
    i => i.Refresh(Refresh.WaitFor)
);

var searchResponse = client.Search<DataEntity>(s => s
    .Query(q => q
        .MatchAll()
    )
);

Я перечислил эту опцию последним, потому что внутренне существуют издержки на производительность и распределение при передаче сериализации Json.NET таким способом. Он включен для обеспечения гибкости, но я бы рекомендовал использовать его только тогда, когда вам действительно нужно, например, завершить пользовательскую сериализацию POCO, где сериализованная структура не является обычной. Мы работаем над гораздо более быстрой сериализацией, которая снизит эти издержки в будущем.

...