Atualização Eficiente de Documentos em Massa no Elasticsearch com Scripts em .NET Core

Lidar com a necessidade de atualizar grandes volumes de dados no Elasticsearch pode ser um desafio de desempenho, especialmente quando as atualizações não são pontuais e afetam milhões de documentos. Uma abordagem comum, como recuperar documentos via cursor ou paginação e depois atualizá-los individualmente ou em pequenos lotes, torna-se inviável devido à sobrecarga de rede e processamanto. Nesses cenários, a funcionalidade de atualização em lote por consulta (_update_by_query) combinada com scripts se destaca como uma solução altamente eficiente.

Este método permite que o Elasticsearch execute a lógica de atualização direetamente no lado do servidor, minimizando a transferência de dados e maximizando a velocidade. O exemplo a seguir demonstra como implementar uma atualização em massa de documentos, modificando campos de data/timestamp, usando o cliente NEST em uma aplicação .NET Core.

Exemplo de Serviço de Atualização em Lote em C# com NEST

Considere uma situação onde precisamos atualizar periodicamente um campo de "data de processamanto" e seu respectivo timestamp para todos os documentos que atendam a um determinado critério, como uma categoria específica e uma data de última modificação antiga. O serviço abaixo ilustra como realizar essa operação em lotes de forma assíncrona.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Elasticsearch.Net;
using NEST;
using Microsoft.Extensions.Logging;

public class DocumentUpdaterService
{
    private readonly IElasticClient _elasticClient;
    private readonly ILogger<DocumentUpdaterService> _logger;

    public DocumentUpdaterService(IElasticClient elasticClient, ILogger<DocumentUpdaterService> logger)
    {
        _elasticClient = elasticClient ?? throw new ArgumentNullException(nameof(elasticClient));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>
    /// Realiza uma atualização em massa de documentos no Elasticsearch usando _update_by_query com script.
    /// </summary<
    /// <param name="indexName">O nome do índice onde os documentos serão atualizados.</param>
    /// <param name="categoryFilterId">ID da categoria para filtrar documentos.</param>
    /// <returns>O número total de documentos atualizados.</returns>
    public async Task<long> UpdateDocumentsProcessingTimeAsync(string indexName, int categoryFilterId)
    {
        long totalDocumentsUpdated = 0;
        int batchCount = 0;

        // Definimos uma data limite para identificar documentos "antigos" que precisam de atualização.
        // Aqui, consideramos documentos com "LastModified" há mais de um dia.
        var modificationThreshold = DateTime.UtcNow.AddDays(-1);

        _logger.LogInformation("Iniciando processo de atualização em massa no índice '{Index}' para categoria '{Category}'.", indexName, categoryFilterId);

        while (true)
        {
            batchCount++;
            var newProcessedTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            var newProcessDate = DateTime.UtcNow;

            var updateRequest = new UpdateByQueryRequest<MyDocument>(indexName)
            {
                Query = new QueryContainer(new BoolQuery
                {
                    Must = new List<QueryContainer>
                    {
                        new TermQuery { Field = new Field(d => d.CategoryId), Value = categoryFilterId },
                        new DateRangeQuery { Field = new Field(d => d.LastModified), LessThan = modificationThreshold }
                    }
                }),
                Script = new ScriptDescriptor()
                    .Source("ctx._source.processedTimestamp = params.newTimestamp; ctx._source.processDate = params.newDate;")
                    .Params(new Dictionary<string, object>
                    {
                        { "newTimestamp", newProcessedTimestamp },
                        { "newDate", newProcessDate }
                    }),
                WaitForCompletion = true, // Espera a conclusão da operação
                ScrollSize = 2000, // Número de documentos por lote de scroll
                Conflicts = Conflicts.Proceed // Continua mesmo em caso de conflitos de versão
            };

            var response = await _elasticClient.UpdateByQueryAsync(updateRequest);

            if (!response.IsValid)
            {
                _logger.LogError("Falha na execução do lote {Batch} da atualização em massa: {DebugInfo}", batchCount, response.DebugInformation);
                break; // Interrompe o processo em caso de erro
            }

            _logger.LogInformation("Lote {Batch} concluído. Documentos atualizados neste lote: {UpdatedCount}.", batchCount, response.Updated);
            totalDocumentsUpdated += response.Updated;

            if (response.Updated == 0)
            {
                // Nenhum documento foi correspondido ou atualizado neste lote,
                // indicando que todos os documentos relevantes já foram processados.
                break;
            }

            // Uma pequena pausa para evitar sobrecarregar o cluster, se necessário.
            await Task.Delay(TimeSpan.FromSeconds(5));
        }

        _logger.LogInformation("Processo de atualização em massa finalizado. Total de {Total} documentos atualizados em {Batches} lotes.", totalDocumentsUpdated, batchCount);
        return totalDocumentsUpdated;
    }
}

// Modelo de exemplo para os documentos no Elasticsearch
public class MyDocument
{
    public int CategoryId { get; set; }
    public long ProcessedTimestamp { get; set; }
    public DateTime ProcessDate { get; set; }
    public DateTime LastModified { get; set; } // Usado para filtrar documentos antigos
    public string Name { get; set; }
    // Outros campos do seu documento...
}

Estrutura DSL Equivalente

O exemplo em C# acima se traduz para uma requisição DSL (Domain Specific Language) do Elasticsearch de forma semelhante ao seguinte:

POST /my_documents-2024.03/_update_by_query?wait_for_completion=true
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "categoryId": {
              "value": 123
            }
          }
        },
        {
          "range": {
            "lastModified": {
              "lt": "2024-03-01T00:00:00Z"
            }
          }
        }
      ]
    }
  },
  "script": {
    "source": "ctx._source.processedTimestamp = params.newTimestamp; ctx._source.processDate = params.newDate;",
    "params": {
      "newTimestamp": 1710172800,
      "newDate": "2024-03-11T12:00:00"
    }
  },
  "conflicts": "proceed",
  "scroll_size": 2000
}

Tags: elasticsearch .NET Core NEST UpdateByQuery scripting

Publicado em 7-10 17:32