Проблема производительности Elasticsearch / Nest - PullRequest
0 голосов
/ 04 апреля 2019

Я заметил странную вещь в поведении свойства ISearchResponse.HitsMetadata.Total в библиотеке NEST.Всякий раз, когда я удаляю документ асинхронно и хочу немедленно извлечь оставшиеся документы из Elasticsearch, поле HitsMetadata.Total, доступное для объекта ISearchResponse, почти никогда не обновляется корректно.Обычно он указывает общее количество документов на момент, предшествующий операции удаления.Поведение возвращается к нормальному состоянию, когда я приостанавливаю выполнение запроса, по крайней мере, на 700 миллисекунд, как если бы NEST (или, возможно, сам Elasticsearch) требовалось больше времени для обновления состояния свойства.Я новичок в использовании NEST и Elasticsearch, поэтому возможно, что я делаю что-то здесь не так, или я не совсем понимаю работу библиотеки, но я потратил довольно много времени на эту проблему и не могу понятьвокруг него.В результате метаданные пагинации, которые я отправляю клиенту, вычисляются неправильно.Я использую NEST 6.6.0 и Elasticsearch 6.6.2.

Действие DELETE:

[HttpDelete("errors/{index}/{logeventId}")]
public async Task<IActionResult> DeleteErrorLog([FromRoute] string index, [FromRoute] string logeventId)
{
    if (string.IsNullOrEmpty(index))
    {
        return BadRequest();
    }

    if (string.IsNullOrEmpty(logeventId)) 
    {
        return BadRequest();
    }

    var getResponse = await _client.GetAsync<Logevent>(new GetRequest(index, typeof(Logevent), logeventId));

    if(!getResponse.Found)
    {
        return NotFound();
    }

    var deleteResponse = await _client.DeleteAsync(new DeleteRequest(index, typeof(Logevent), logeventId));

    if (!deleteResponse.IsValid)
    {
        throw new Exception($"Deleting document id {logeventId} failed");
    }

    return NoContent();

}

Действие GET:

[HttpGet("errors/{index}", Name = "GetErrors")]
public async Task<IActionResult> GetErrorLogs([FromRoute] string index, 
    [FromQuery]int pageNumber = 1, [FromQuery] int pageSize = 5)
{
    if (string.IsNullOrEmpty(index))
    {
        return BadRequest();
    }

    if(pageSize > MAX_PAGE_SIZE || pageSize < 1)
    {
        pageSize = 5;
    }

    if(pageNumber < 1)
    {
        pageNumber = 1;
    }

    var from = (pageNumber - 1) * pageSize;

    ISearchResponse<Logevent> searchResponse = await GetSearchResponse(index, from, pageSize);

    if (searchResponse.Hits.Count == 0)
    {
        return NotFound();
    }

    int totalPages = GetTotalPages(searchResponse, pageSize);

    var previousPageLink = pageNumber > 1 ? 
        CreateGetLogsForIndexResourceUri(ResourceUriType.PreviousPage, pageNumber, pageSize, "GetErrors") : null;

    var nextPageLink = pageNumber < totalPages ? 
        CreateGetLogsForIndexResourceUri(ResourceUriType.NextPage, pageNumber, pageSize, "GetErrors") : null;

    /* HERE, WHEN EXECUTED IMMMEDIATELY (UP TO 700 MILISSECONDS, THE 
       totalCount FIELD GETS MISCALCULATED AS IT RETURNS THE VALUE PRECEDING 
       THE DELETION OF A DOCUMENT 
    */
    var totalCount = searchResponse.HitsMetadata.Total;
    var count = searchResponse.Hits.Count;

    var paginationMetadata = new
    {
        totalCount = searchResponse.HitsMetadata.Total,
        totalPages,
        pageSize,
        currentPage = pageNumber,
        previousPageLink,
        nextPageLink
    };

    Response.Headers.Add("X-Pagination", Newtonsoft.Json.JsonConvert.SerializeObject(paginationMetadata));

    var logeventsDtos = Mapper.Map<IEnumerable<LogeventDto>>(searchResponse.Hits);

    return Ok(logeventsDtos);
}

GetSearchResponseMethod:

private async Task<ISearchResponse<Logevent>> GetSearchResponse(string index, int from, int pageSize)
{
    return await _client.SearchAsync<Logevent>(s =>
             s.Index(index).From(from).Size(pageSize).Query(q => q.MatchAll()));

} 

Код на стороне клиента, инициирующий действия на стороне сервера:

async deleteLogevent(item){
    this.deleteDialog = false;
    let logeventId = item.logeventId;
    let level = this.defaultSelected.name;
    let index = 'logstash'.concat('-', this.defaultSelected.value, '-', this.date);

    LogsService.deleteLogevent(level, index, logeventId).then(response => {
      if(response.status == 204){
        let logeventIndex = this.logs.findIndex(element => {return element.logeventId === item.logeventId});
        this.logs.splice(logeventIndex, 1);
        LogsService.getLogs(level, index, this.pageNumber).then(reloadResponse => {
          this.logs.splice(0);
          reloadResponse.data.forEach(element => {
          this.logs.push(element)
          });
          this.setPaginationMetadata(reloadResponse.headers["x-pagination"]);
        })
      }
    }).catch(error => {

    })

1 Ответ

0 голосов
/ 04 апреля 2019

Это нормальное и ожидаемое поведение для Elasticsearch. Изменения в таких операциях, как индексация, обновление и удаление, не отражаются в ответах на поисковые запросы, пока не произойдет интервал обновления. Сообщение в блоге Майка МакКэндлеса о том, как Lucene обрабатывает удаленные документы уже несколько лет, но все еще актуально. Онлайн-руководство по разделу Elasticsearch Поиск в реальном времени также является хорошим ресурсом.

Вот пример, демонстрирующий поведение

private static void Main()
{
    var defaultIndex = "refresh_example";
    var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));

    var settings = new ConnectionSettings(pool)
        .DefaultIndex(defaultIndex)
        .DefaultTypeName("_doc");

    var client = new ElasticClient(settings);

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

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

    var indexResponse = client.IndexDocument(new Document 
    {
        Id = 1,
        Name = "foo"
    });

    // hit count is likely to be 0 here because no refresh interval has occurred
    var searchResponse = client.Search<Document>();
    Console.WriteLine($"search hit count after index no refresh: {searchResponse.Hits.Count}");

    // a get for the exact document will return it however.
    var getResponse = client.Get<Document>(1);
    Console.WriteLine($"get document with id 1, name is: {getResponse.Source.Name}");

    // use refresh API to refresh the index
    var refreshResponse = client.Refresh(defaultIndex);

    // now the hit count is 1
    searchResponse = client.Search<Document>();
    Console.WriteLine($"search hit count after refresh: {searchResponse.Hits.Count}");

    // index another document, and refresh at the same time
    indexResponse = client.Index(new Document
    {
        Id = 2,
        Name = "bar"
    }, i => i.Refresh(Refresh.WaitFor));

    // now the hit count is 2
    searchResponse = client.Search<Document>();
    Console.WriteLine($"search hit count after index with refresh: {searchResponse.Hits.Count}");

    // now delete document with id 1
    var deleteResponse = client.Delete<Document>(1);
    Console.WriteLine($"document with id 1 deleted");

    // hit count is still 2
    searchResponse = client.Search<Document>();
    Console.WriteLine($"search hit count before refresh: {searchResponse.Hits.Count}");

    // refresh
    refreshResponse = client.Refresh(defaultIndex);

    // hit count is 1
    searchResponse = client.Search<Document>();
    Console.WriteLine($"search hit count after refresh: {searchResponse.Hits.Count}");
}

public class Document 
{
    public int Id { get; set; }

    public string Name { get;set; }
}

Вот что написано на консоли

search hit count after index no refresh: 0
get document with id 1, name is: foo
search hit count after refresh: 1
search hit count after index with refresh: 2
document with id 1 deleted
search hit count before refresh: 2
search hit count after refresh: 1

Возможно, вы подумали: «Почему бы мне просто не обновить каждую операцию?». Причина не в производительности; когда вы вызываете API обновления или задаете обновление как часть операции, записывается и открывается новый сегмент, который использует системные ресурсы, должен быть зафиксирован на диске и, вероятно, позднее объединен с другими сегментами. Постоянное обращение к обновлению создаст много сегментов. Однако полезно использовать в тестах функцию refresh для подтверждения.

Лучше всего написать ваше приложение для обработки около природы в реальном времени с удалением и поиском. Для нумерации страниц это аналогичный сценарий и для других хранилищ данных.

...