<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Benjamin Trent - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Benjamin Trent - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/pt/search-labs/author/benjamin-trent</link>
    </image>
    <link>https://www.elastic.co/pt/search-labs/author/benjamin-trent</link>
    <atom:link href="https://www.elastic.co/pt/search-labs/rss/author/benjamin-trent.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[pt]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 05:13:22 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Redimensionamento de modelos de interação tardia no Elasticsearch - parte 2]]></title>
    <description><![CDATA[Este artigo explora técnicas para redimensionar vetores de interação tardia para cargas de trabalho de produção em grande escala, como a redução do uso de espaço em disco e a melhoria da eficiência computacional.]]></description>
    <content:encoded><![CDATA[<p>Em nosso <a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">blog anterior sobre o ColPali</a>, exploramos como criar aplicações de busca visual com o Elasticsearch. O foco principal foi o valor que modelos como o ColPali agregam às aplicações, mas eles apresentam desvantagens de performance em comparação com a busca vetorial usando bi-encoders, como o E5.</p><p>Partindo dos exemplos da <a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">parte 1</a>, este post explora como usar diferentes técnicas e o conjunto avançado de ferramentas de busca vetorial do Elasticsearch para preparar vetores de interação tardia para cargas de trabalho de produção em grande escala.</p><p>Os exemplos completos de código estão disponíveis no <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/colpali">GitHub.</a></p><h2>Desafios dos modelos de interação tardia</h2><p>O ColPali cria mais de 1.000 vetores por página para os documentos do nosso índice.</p><p>Isso resulta em dois desafios ao trabalhar com vetores de interação tardia:</p><ol><li><p>Espaço em disco: salvar todos esses vetores em disco gera um volume significativo de armazenamento, o que se torna caro em ambientes de grande escala.</p></li><li><p>Computação: ao classificar documentos usando a comparação <code>maxSimDotProduct()</code>, precisamos comparar todos esses vetores de cada documento com os N vetores da consulta.</p></li></ol><p>Vamos analisar algumas técnicas para lidar com esses desafios.</p><h2>Técnicas para otimizar modelos de interação tardia</h2><h3>Vetores de bits</h3><p>Para reduzir o espaço em disco, podemos comprimir as imagens em vetores de bits. Podemos usar uma função simples em Python para transformar nossos multivetores em vetores de bits:</p>def to_bit_vectors(embeddings: list) -&gt; list:
    return [
        np.packbits(np.where(np.array(embedding) &gt; 0, 1, 0))
        .astype(np.int8)
        .tobytes()
        .hex()
        for embedding in embeddings
    ]<p>O conceito central da função é simples: valores acima de 0 se tornam 1, e valores abaixo de 0 se tornam 0. Isso resulta em uma matriz de 0s e 1s, que depois transformamos em uma string hexadecimal que representa nosso vetor de bits.</p><p>Para o mapeamento de índice, configuramos o parâmetro <code>element_type</code> para <code>bit</code>:</p>mappings = {
    "mappings": {
        "properties": {
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)<p>Depois de gravar todos os novos vetores de bits no índice, podemos ranquear nossos vetores de bits usando o código a seguir:</p>query = "What do companies use for recruiting?"
query_vector = to_bit_vectors(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimInvHamming(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcaf0c8f999d68701/6a17f46696142a6f46eb1c56/51b989446e4099745971e1eac27d147a78d13e0a-1600x480.png" alt="" /><p>Ao abrir mão de um pouco de precisão, isso nos permite usar a distância de Hamming (<code>maxSimInvHamming(...)</code>), que consegue explorar otimizações como bit masks, SIMD, entre outras. Saiba mais sobre <a href="https://www.elastic.co/search-labs/blog/bit-vectors-in-elasticsearch">vetores de bits e distância de Hamming em nosso blog</a>.</p><p>Como alternativa, podemos não converter o vetor de consulta em vetores de bits e realizar a busca usando o vetor de interação tardia com fidelidade total:</p>query = "What do companies use for recruiting?"
query_vector = create_col_pali_query_vectors(query)
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2fa6c89fb451b6e0/6a17f468af47b65da9cde0d9/89f3c795c6dc44b2f7d46801320288316b47e1b2-1600x488.png" alt="Resultados do uso de vetores de bits para otimizar modelos de interação tardia" /><p>Nesse caso, os vetores são comparados usando uma função de similaridade assimétrica.</p><p></p><p>Vamos considerar uma distância de Hamming padrão entre dois vetores de bits. Suponha que temos um vetor de documento <em>D:</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4b6ece1ec4dedac/6a17f4694b055d248143234e/366a70a23e37d403788327b7aefc873fd4482f5e-1235x86.png" alt="" /><p>E um vetor de consulta <em>Q:</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7721df9c8f0d84f1/6a17f46b3e03d77cf54f2dcb/978ec31e0afbc0eaebf548a015b628c0cad84625-1247x84.png" alt="" /><p></p><p>A quantização binária simples transforma o vetor <em>D</em> em <code>10101101</code> e o vetor <em>Q</em> em <code>11111011</code>. Para calcular a distância de Hamming, precisamos apenas de operações diretas em bits, algo extremamente rápido. Nesse caso, a distância de Hamming é <code>01010110</code>, que possui uma contagem de bits igual a 4.Assim, a pontuação passa a ser o inverso dessa distância de Hamming. Lembre-se de que vetores mais semelhantes têm uma distância de Hamming MENOR, portanto inverter esse valor permite que vetores mais semelhantes recebam pontuações mais altas. Especificamente aqui, a pontuação seria 1/4 = <code>0.25</code>.</p><p>No entanto, observe que perdemos a magnitude de cada dimensão. Um <code>1</code> é um <code>1</code>. Assim, para <em>Q</em>, a diferença entre <code>0.01</code> e <code>0.79</code> desaparece. Como estamos simplesmente quantizando de acordo com <code>&gt;0</code>, podemos aplicar um pequeno truque em que o vetor Q não é quantizado. Isso não permite a matemática bit a bit extremamente rápida, mas mantém o custo de armazenamento baixo, já que D continua quantizado.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blted50315518e2d599/6a17f46c414c6463709452d3/508e8498ba969534d2a0131d431c75735fc27cb9-1399x611.png" alt="" /><p>Em resumo, essa abordagem preserva as informações contidas em <em>Q</em>, aumentando a qualidade da estimativa de distância e mantendo baixo o custo de armazenamento.</p><p>O uso de vetores de bits permite economizar significativamente espaço em disco e reduzir a carga computacional no momento da consulta. Mas ainda há mais que podemos fazer.</p><h3>Vetores médios</h3><p>Para redimensionar a busca para centenas de milhares de documentos, mesmo os ganhos de desempenho proporcionados pelos vetores de bits não serão suficientes. Para dar conta desse tipo de carga, será necessário aproveitar a estrutura de índice HNSW do Elasticsearch para busca vetorial.</p><p>O ColPali gera cerca de 1.000 vetores por documento, o que é excessivo para adicionar ao grafo HNSW. Portanto, precisamos reduzir a quantidade de vetores. Para isso, podemos criar uma única representação do significado do documento calculando a média de todos os vetores do documento produzidos pelo ColPali quando incorporamos a imagem.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc053425fbfcf8de8/6a17f46f148009faf1b488aa/7c3b4dffb70bd95f67deb35f8c01e73c5286c2ab-1476x1102.png" alt="Vetor médio considerando todos os vetores de interação tardia" /><p>No momento, isso não é possível diretamente no Elastic, sendo necessário pré-processar os vetores antes de ingeri-los no Elasticsearch. </p><p>Isso pode ser feito com o Logstash ou com pipelines de ingestão, mas aqui usaremos uma função simples em Python:</p>def to_avg_vector(vectors):
    vectors_array = np.array(vectors)
    
    avg_vector = np.mean(vectors_array, axis=0)
    
    norm = np.linalg.norm(avg_vector)
    if norm &gt; 0:
        normalized_avg_vector = avg_vector / norm
    else:
        normalized_avg_vector = avg_vector

    return normalized_avg_vector.tolist()<p>Também normalizamos o vetor para que possamos usar a similaridade por produto escalar.</p><p>Depois de transformar todos os vetores do ColPali em vetores médios, podemos indexá-los no campo dense_vector:</p>mappings = {
    "mappings": {
        "properties": {
            "avg_vector": {
                "type": "dense_vector",
                "dims": 128,
                "index": True,
                "similarity": "dot_product"
            },
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)<p>Precisamos considerar que isso aumentará o uso total de disco, pois estamos salvando mais informações junto com nossos vetores de interação tardia. Além disso, usaremos RAM adicional para manter o grafo HNSW, o que nos permite redimensionar a busca para bilhões de vetores. Para reduzir o uso de RAM, podemos recorrer ao nosso conhecido <a href="https://www.elastic.co/search-labs/blog/optimized-scalar-quantization-elasticsearch">recurso BBQ</a>. Com isso, obtemos resultados de busca rápidos em conjuntos de dados massivos que, de outra forma, não seriam viáveis.</p><p>Agora, simplesmente fazemos a busca com a consulta knn para encontrar os documentos mais relevantes.</p>query = "What do companies use for recruiting?"
query_vector = to_avg_vector(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "knn": {
        "field": "avg_vector",
        "query_vector": query_vector,
        "k": 10,
        "num_candidates": 100
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac6b7fd444f935/6a17f471a29299d76ad02dc0/a500f9907020f032c3b1c7a48ed8c759dc2a1dd4-1600x498.png" alt="" /><p>O que antes era a melhor correspondência acabou caindo para a 3ª posição.</p><p>Para corrigir esse problema, podemos usar uma recuperação em vários estágios. No primeiro estágio, usamos a consulta knn para buscar os melhores candidatos para a consulta em meio a milhões de documentos. No segundo estágio, fazemos a reclassificação apenas dos top k (neste caso, 10) usando a maior fidelidade dos vetores de interação tardia do ColPali. </p>query = "What do companies use for recruiting?"
col_pali_vector = create_col_pali_query_vectors(query)
avg_vector = to_avg_vector(col_pali_vector)
es_query = {
  "_source": False,
  "retriever": {
    "rescorer": {
      "retriever": {
        "knn": {
          "field": "avg_vector",
          "query_vector": avg_vector,
          "k": 10,
          "num_candidates": 100
        }
      },
      "rescore": {
        "window_size": 10,
        "query": {
          "rescore_query": {
            "script_score": {
              "query": {
                "match_all": {}
              },
              "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                  "query_vector": col_pali_vector
                }
              }
            }
          }
        }
      }
    }
  },
  "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt265edd5f37be40b7/6a17f4733e9e45583dba15e6/797f490860af87fcf29f34668a5c4511419c6fd0-1600x501.png" alt="Resultados do uso de vetores médios para otimizar modelos de interação tardia" /><p>Aqui, estamos usando o <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.18/retriever.html#rescorer-retriever">rescore retriever,</a> introduzido na versão 8.18, para reclassificar nossos resultados. Após a reclassificação, vemos que a melhor correspondência volta a ocupar a primeira posição. </p><p>Observação: em uma aplicação de produção, podemos usar um valor de k muito maior que 10, já que a função max sim ainda é relativamente eficiente em termos de performance.</p><h3>Agrupamento de tokens</h3><p>O agrupamento de tokens reduz o comprimento da sequência de embeddings multivetoriais ao agrupar informações redundantes, como regiões de fundo branco. Essa técnica diminui o número de embeddings ao mesmo tempo que preserva a maior parte do sinal da página.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3d643e6ac908f640/6a17f4754b055d3783432352/09eae0b768f4b450e555d7e53e57561bd52de2c0-1412x1056.png" alt="Agrupamento de tokens para otimizar modelos de interação tardia" /><p>O agrupamento de tokens funciona reunindo embeddings de tokens semelhantes dentro de um documento em clusters, usando um algoritmo de clusterização. Em seguida, calcula-se a média dos vetores em cada cluster para criar uma única representação agregada. Esse vetor agregado substitui os tokens originais do grupo, reduzindo o número total de vetores sem perda significativa do sinal do documento.</p><p>O artigo do ColPali propõe um valor inicial de agrupamento factor igual a 3 para a maioria dos conjuntos de dados, mantendo 97,8% da performance original ao mesmo tempo que reduz o número total de vetores em 66,7%. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d78a15c6944a1ea/6a17f477414c64a2929452d9/343cc9eaf54af8c7a125d4115838f3c7d5659de0-1600x1007.png" alt="Fator de agrupamento para otimizar modelos de interação tardia" /><p>Mas é preciso cautela: o conjunto de dados Shift, que contém documentos muito densos e com grande volume de texto e pouco espaço em branco, apresenta uma queda rápida de desempenho conforme o fator de agrupamento aumenta.</p><p>Para criar os vetores agrupados, podemos usar a biblioteca colpali_engine:</p>from colpali_engine.compression.token_pooling import HierarchicalTokenPooler

pooler = HierarchicalTokenPooler(pool_factor=3) # test on your data for a good pool_factor

def pool_vectors(embedding: list) -&gt; list:
    tensor = torch.tensor(embedding).unsqueeze(0)
    pooled = pooler.pool_embeddings(tensor)
    return pooled.squeeze(0).tolist()<p>Agora temos um vetor que teve suas dimensões reduzidas em cerca de 66,7%. Nós o indexamos normalmente e conseguimos realizar buscas usando nossa função <code>maxSimDotProduct()</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc460c14031814ede/6a17f4794202292bf629f72d/2412c5db7d79a590b96d42fe01f140c42f010612-1600x481.png" alt="Resultados dos modelos de interação tardia" /><p>Conseguimos obter bons resultados de busca ao custo de uma leve perda de precisão nos resultados.</p><p>Dica: com um pool_factor mais alto (100-200), também é possível encontrar um meio-termo entre a solução de vetor médio e a abordagem discutida aqui. Com cerca de 5 a 10 vetores por documento, torna-se viável indexá-los em um campo aninhado para aproveitar o índice HNSW.</p><h2>Cross-encoder vs. interação tardia vs. bi-encoder</h2><p>Com tudo o que aprendemos até aqui, onde isso posiciona os modelos de interação tardia, como ColPali ou ColBERT, em comparação com outras técnicas de recuperação baseadas em IA?</p><p>Embora a função max sim seja mais barata do que o uso de cross-encoders, ela ainda exige muito mais comparações e processamento do que a busca vetorial com bi-encoders, em que comparamos apenas dois vetores para cada par consulta-documento. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbfd97f3bba3e5b17/6a17f47be31791b0052d5943/75e1fc9e601aa7a6e88f137565c1919166c7a71c-1480x458.png" alt="Cross-encoder vs. modelos de interação tardia vs. bi-encoder" /><p>Por isso, nossa recomendação para modelos de interação tardia é, em geral, usá-los apenas para reclassificação dos primeiros k resultados de busca. Também refletimos isso no nome do tipo de campo: rank_vectors.</p><p>Mas e o cross-encoder? Modelos de interação tardia são melhores por serem mais baratos de executar no momento da consulta? Como acontece com frequência, a resposta é: depende. Os cross-encoders normalmente produzem resultados de maior qualidade, mas exigem muito processamento, já que cada par consulta-documento precisa passar completamente pelo modelo transformer. Eles também se beneficiam do fato de não exigirem indexação de vetores e poderem operar de forma stateless. Isso resulta em:</p><ul><li><p>Menor uso de espaço em disco</p></li><li><p>Um sistema mais simples</p></li><li><p>Maior qualidade nos resultados de busca</p></li><li><p>Maior latência, o que limita a profundidade da reclassificação</p></li></ul><p>Por outro lado, os modelos de interação tardia podem deslocar parte desse custo computacional para o momento da indexação, tornando a consulta mais barata. O preço a pagar é a necessidade de indexar vetores, o que torna os pipelines de indexação mais complexos e exige mais espaço em disco para armazená-los.</p><p>No caso específico do ColPali, a análise de informações provenientes de imagens é muito custosa, já que elas contêm grandes volumes de dados. Nesse cenário, o equilíbrio pende a favor do uso de um modelo de interação tardia como o ColPali, pois avaliar essas informações no momento da consulta seria lento demais e exigiria muitos recursos. </p><p>Já para um modelo de interação tardia como o ColBERT, que trabalha com dados textuais, assim como a maioria dos cross-encoders, por exemplo o elastic-rerank-v1, a decisão pode favorecer o uso do cross-encoder, aproveitando a economia de disco e a maior simplicidade operacional.</p><p>Recomendamos que você avalie esses prós e contras no seu caso de uso e experimente as diferentes ferramentas que o Elasticsearch oferece para criar as melhores aplicações de busca.</p><h2>Conclusão</h2><p>Neste blog, exploramos várias técnicas para otimizar modelos de interação tardia, como o ColPali, para busca vetorial em grande escala no Elasticsearch. Embora esses modelos ofereçam um forte equilíbrio entre eficiência de recuperação e qualidade de classificação, eles também introduzem desafios relacionados a armazenamento e computação.</p><p>Para enfrentar esses desafios, analisamos diferentes abordagens e técnicas:</p><ul><li><p><strong>Vetores de bits</strong> para reduzir significativamente o uso de espaço em disco, ao mesmo tempo que aproveitam computações de similaridade eficientes, como a distância de Hamming ou a similaridade máxima assimétrica.</p></li><li><p><strong>Vetores médios</strong> para comprimir múltiplos embeddings em uma única representação densa, permitindo recuperação eficiente com indexação HNSW.</p></li><li><p><strong>Agrupamento de tokens</strong> para unir embeddings redundantes de forma inteligente, mantendo a integridade semântica e reduzindo a sobrecarga computacional no momento da consulta.</p></li></ul><p>O Elasticsearch oferece um conjunto poderoso de ferramentas para personalizar e otimizar aplicações de busca de acordo com suas necessidades. Seja priorizando velocidade de recuperação, qualidade de ranqueamento ou eficiência de armazenamento, essas técnicas permitem equilibrar desempenho e qualidade conforme as exigências de aplicações do mundo real.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/late-interaction-model-colpali-scale</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/late-interaction-model-colpali-scale</guid>
    <category><![CDATA[Relevância]]></category>
    <category><![CDATA[Banco de dados vetorial]]></category>
    <dc:creator><![CDATA[Peter Straßer,Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt97a536033e6b0a56/6a17f47dfbc5f88c86491c0d/c780b78a07573f2df1cfef8b29a7109f839b0ab3-1200x628.png" length="0" type="image/png"/>
    <pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Erros de concorrência no Lucene: Como corrigir falhas de concorrência otimista]]></title>
    <description><![CDATA[Graças ao Fray, um framework de teste de concorrência determinístico do PASTA Lab da CMU, rastreamos e corrigimos um bug complexo do Lucene.]]></description>
    <content:encoded><![CDATA[<p>Sim, mais um blog sobre correção de bugs. Mas esta história tem uma reviravolta: um herói do código aberto surge e salva o dia. </p><p>Depurar erros de concorrência não é tarefa fácil, mas vamos abordar o assunto. Apresentamos o Fray, uma estrutura de teste de concorrência determinística do PASTA Lab da CMU, que transforma falhas instáveis em falhas reproduzíveis de forma confiável. Graças ao engenhoso design de trava sombreada e ao controle preciso da rosca do Fray, conseguimos localizar e finalmente eliminar um bug complexo do Lucene. Este artigo explora como ferramentas e projetos de código aberto estão tornando a depuração de concorrência menos trabalhosa — e o mundo do software muito melhor.</p><h2>Erros de concorrência: o pesadelo dos engenheiros de software</h2><p>Os piores bugs relacionados à concorrência são os seguintes. Além de serem difíceis de consertar, o mais complicado é justamente fazê-los falhar de forma consistente. Tomemos como exemplo esta falha de teste, <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat#testGlobalVersions</code></a>. Isso gera várias threads de escrita e atualização de documentos, desafiando o modelo de concorrência otimista do Lucene. Este teste revelou uma condição de corrida no controle de concorrência otimista. Ou seja, uma operação em um documento pode alegar falsamente ser a última em uma sequência de operações 😱. Ou seja, em certas condições, uma operação de atualização ou exclusão pode, na verdade, ter sucesso quando deveria ter falhado, considerando restrições de concorrência otimistas.</p><p></p>org.apache.lucene.sandbox.codecs.idversion.TestIDVersionPostingsFormat &gt; testGlobalVersions FAILED
    java.lang.AssertionError: maxSeqNo must be greater or equal to 7442 but was 7441
        at __randomizedtesting.SeedInfo.seed([B97A2BDBC7E40BF6:B4D76006D5101E6]:0)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DocumentsWriterDeleteQueue.close(DocumentsWriterDeleteQueue.java:325)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DocumentsWriter.flushAllThreads(DocumentsWriter.java:659)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.IndexWriter.getReader(IndexWriter.java:576)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenFromWriter(StandardDirectoryReader.java:381)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenIfChanged(StandardDirectoryReader.java:355)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenIfChanged(StandardDirectoryReader.java:345)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DirectoryReader.openIfChanged(DirectoryReader.java:170)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.SearcherManager.refreshIfNeeded(SearcherManager.java:144)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.SearcherManager.refreshIfNeeded(SearcherManager.java:52)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.ReferenceManager.doMaybeRefresh(ReferenceManager.java:167)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.ReferenceManager.maybeRefresh(ReferenceManager.java:213)<p>Pedimos desculpas àqueles que detestam rastreamentos de pilha do Java. Note que "excluir" não significa necessariamente "excluir". Também pode indicar uma "atualização" do documento, já que os segmentos do Lucene são somente leitura.
</p><p>O Apache Lucene gerencia cada thread que está escrevendo documentos através da classe <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> . Esta classe criará ou reutilizará threads para escrita de documentos e cada ação de escrita controla suas informações dentro da classe <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java"><code>DocumentsWriterPerThread</code></a> (DWPT). Além disso, o escritor mantém o controle de quais documentos são excluídos no <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> (DWDQ). Essas estruturas mantêm todas as ações de mutação de documentos na memória e são periodicamente liberadas, liberando recursos na memória e persistindo as estruturas em disco.</p><p></p><p>Na tentativa de evitar <a href="https://en.wikipedia.org/wiki/Blocking_(computing)">o bloqueio de threads</a> e garantir alto desempenho em sistemas concorrentes, o Apache Lucene tenta <a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html">sincronizar</a> apenas em seções críticas. Embora isso possa ser bom na prática, como em qualquer sistema concorrente, existem problemas.</p><h2>
Uma falsa esperança</h2><p>Minha investigação inicial apontou para algumas seções críticas que não estavam devidamente sincronizadas. Todas as interações com um dado <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> são controladas pelo seu envolvente <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a>. Assim, embora os métodos individuais possam não estar devidamente sincronizados no <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a>, o seu acesso ao mundo está (ou deveria estar). (Não vamos entrar em detalhes sobre como isso complica as questões de propriedade e acesso — é um projeto de longa data escrito por muitos colaboradores.) Dê um desconto para isso.)</p><p></p><p>No entanto, encontrei <a href="https://github.com/apache/lucene/blob/40060f8b7080d06a218518445a0a1dfc520c812a/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java#L572-L576">um ponto durante a descarga</a> que não estava sincronizado.</p>// Advance the queue, meaning create a new one to keep track of deletes
// Since we have been flushed, let's starting tracking again
DocumentsWriterDeleteQueue newQueue = documentsWriter.deleteQueue.advanceQueue(perThreadPool.size());
// OK, get the current new maximum sequence number for optimistic concurrency
seqNo = documentsWriter.deleteQueue.getMaxSeqNo();
// Reset to the new queue
documentsWriter.resetDeleteQueue(newQueue);<p>Essas ações não são sincronizadas em uma única operação atômica. Significa que, entre a criação de <code>newQueue</code> e a chamada de <code>getMaxSeqNo</code>, outro código poderia ter sido executado incrementando o número de sequência na classe <code>documentsWriter</code> . Encontrei o bug!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc55033280b45913/6a1706b4dc55dec27fe00d30/3f2617a102d28735e755a7539b047c28beda41be-1600x453.jpg" alt="" /><p>
Mas, como acontece com a maioria dos bugs complexos, encontrar a causa raiz não foi simples. Foi então que um herói entrou em cena.</p><h2>Um herói na batalha</h2><p>Apresentamos nosso herói: <a href="https://aoli.al/">Ao Li</a> e seus colegas do Laboratório PASTA. Deixarei que ele explique como eles salvaram o dia com a ajuda de Fray.</p><p><a href="https://github.com/cmu-pasta/fray">Fray</a> é uma estrutura de teste de concorrência determinística desenvolvida por pesquisadores do <a href="https://pastalab.org/">PASTA Lab</a>, da Universidade Carnegie Mellon. A motivação por trás da criação do Fray surge de uma lacuna notável entre a academia e a indústria: embora os testes de concorrência determinísticos tenham sido amplamente estudados em pesquisas acadêmicas por mais de 20 anos, os profissionais continuam a depender de testes de estresse — um método amplamente reconhecido como não confiável e instável — para testar seus programas concorrentes. Assim, nosso objetivo principal era projetar e implementar uma estrutura de teste de concorrência determinística, tendo como metas a generalidade e a aplicabilidade prática.</p><p></p><h2>A ideia central</h2><p>Em sua essência, o Fray se baseia em um princípio simples, porém poderoso: a execução sequencial. O modelo de concorrência do Java oferece uma <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.3">propriedade</a>fundamental: se um programa estiver livre de condições de corrida (data races), todas as execuções parecerão sequencialmente consistentes. Isso significa que o comportamento do programa pode ser representado como uma sequência de instruções do programa.</p><p>O Fray funciona executando o programa alvo de forma sequencial: a cada passo, ele pausa todas as threads, exceto uma, permitindo que o Fray controle com precisão o agendamento das threads. Os threads são selecionados aleatoriamente para simular a concorrência, mas as escolhas são registradas para posterior reprodução determinística. Para otimizar a execução, o Fray realiza trocas de contexto apenas quando uma thread está prestes a executar uma instrução de sincronização, como bloqueio ou acesso atômico/volátil. Uma propriedade interessante da ausência de condições de corrida em relação aos dados é que essa troca de contexto limitada é suficiente para explorar todos os comportamentos observáveis devido a qualquer intercalação de threads (<a href="https://arxiv.org/abs/2501.12618">nosso artigo</a> contém um esboço da demonstração).</p><p></p><h2>O desafio: controlar o agendamento de threads</h2><p>Embora a ideia central pareça simples, a implementação do Fray apresentou desafios significativos. Para controlar o agendamento de threads, o Fray precisa gerenciar a execução de cada thread da aplicação. À primeira vista, isso pode parecer simples: substituir primitivas de concorrência por implementações personalizadas. No entanto, o controle de concorrência na JVM é complexo, envolvendo uma combinação de <a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html">instruções de bytecode</a>, <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html">bibliotecas de alto nível</a> e <a href="https://github.com/openjdk/jdk/blob/b720517cb33c2119ec6ed85504bce321de748228/src/java.base/share/classes/java/lang/Object.java#L394">métodos nativos</a>.</p><p></p><p>Isso acabou se revelando um verdadeiro labirinto:</p><p></p><ul><li><p>Por exemplo, cada instrução <code>MONITORENTER</code> deve ter uma correspondente <code>MONITOREXIT</code> no mesmo método. Se Fray substituir <code>MONITORENTER</code> por uma chamada de método para um stub/mock, também precisará substituir <code>MONITOREXIT</code>.</p></li><li><p>Em código que faz uso de <code>object.wait/notify</code>, se <code>MONITORENTER</code> for substituído, o <code>object.wait</code> correspondente também deverá ser substituído. Esta cadeia de substituição estende-se até <code>object.notify</code> e além.</p></li><li><p>A JVM invoca certos métodos relacionados à concorrência (por exemplo, <code>object.notify</code> quando uma thread termina) dentro do código nativo. Substituir essas operações exigiria modificar a própria JVM.</p></li><li><p>Funções da JVM, como carregadores de classe e threads de coleta de lixo (GC), também usam primitivas de concorrência. Modificar esses elementos primitivos pode criar incompatibilidades com essas funções da JVM.</p></li><li><p>A substituição de primitivas de concorrência no JDK frequentemente resulta em falhas da JVM durante sua fase de inicialização.</p></li></ul><p></p><p>Esses desafios deixaram claro que uma substituição completa das primitivas de concorrência não era viável.</p><h2>
Nossa solução: design de fechadura sombreada</h2><p>Para lidar com esses desafios, o Fray utiliza um novo mecanismo de bloqueio sombra para orquestrar a execução de threads sem substituir as primitivas de concorrência. Os bloqueios de sombra atuam como intermediários que orientam a execução das threads. Por exemplo, antes de adquirir um bloqueio, um thread de aplicação deve interagir com seu bloqueio sombra correspondente. O mecanismo de travamento por sombra determina se a rosca consegue obter a trava. Se a thread não puder prosseguir, o bloqueio sombra a bloqueia e permite que outras threads sejam executadas, evitando impasses e permitindo concorrência controlada. Este design permite que o Fray controle o entrelaçamento de threads de forma transparente, preservando a correção da semântica de concorrência. Cada primitiva de concorrência é cuidadosamente modelada dentro da estrutura de bloqueio sombra para garantir solidez e completude. Mais detalhes técnicos podem ser encontrados em nosso artigo.</p><p></p><p>Além disso, este projeto foi concebido para ser à prova de futuro. Ao exigir apenas a instrumentação de bloqueios de sombra em torno de primitivas de concorrência, garante-se a compatibilidade com versões mais recentes da JVM. Isso é viável porque as interfaces das primitivas de concorrência na JVM são relativamente estáveis e permaneceram inalteradas por anos.</p><h2>
Testando Fray</h2><p>Após a construção do Fray, o próximo passo foi a avaliação. Felizmente, muitas aplicações, como o Apache Lucene, já incluem testes de concorrência. Esses testes de concorrência são testes JUnit regulares que criam várias threads, executam algum trabalho e, em seguida (geralmente), esperam que essas threads terminem para então verificar alguma propriedade. Na maioria das vezes, esses testes são aprovados porque utilizam apenas uma intercalação. Pior ainda, alguns testes falham apenas ocasionalmente no ambiente CI/CD, como descrito anteriormente, tornando essas falhas extremamente difíceis de depurar. Ao executarmos os mesmos testes com o Fray, descobrimos inúmeros bugs. Notavelmente, Fray redescobriu bugs relatados anteriormente que permaneceram sem correção devido à falta de uma reprodução confiável, incluindo o foco deste blog: <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat.testGlobalVersions</code></a>. Felizmente, com o Fray, podemos reproduzi-los de forma determinística e fornecer aos desenvolvedores informações detalhadas, permitindo que eles reproduzam e corrijam o problema de forma confiável.</p><p></p><h2>Próximos passos para Fray</h2><p>Estamos muito satisfeitos em saber, por parte dos desenvolvedores da Elastic, que o Fray tem sido útil na depuração de bugs de concorrência. Continuaremos trabalhando no Fray para torná-lo disponível para mais desenvolvedores.</p><p>Nossos objetivos de curto prazo incluem aprimorar a capacidade do Fray de reproduzir deterministicamente a programação, mesmo na presença de outras operações não determinísticas, como um gerador de valores aleatórios ou o uso de <code>object.hashcode</code>. Nosso objetivo também é melhorar a usabilidade do Fray, permitindo que os desenvolvedores analisem e depurem testes de concorrência existentes sem qualquer intervenção manual. E, mais importante ainda, se você estiver enfrentando dificuldades para depurar ou testar problemas de concorrência em seu programa, gostaríamos muito de ouvir sua opinião. Por favor, não hesite em criar uma issue no <a href="https://github.com/cmu-pasta/fray">repositório do Fray no Github</a>.</p><p></p><h2>Hora de corrigir o bug de concorrência</h2><p>Graças a Ao Li e ao laboratório PASTA, agora temos uma instância confiável que falha neste teste! Finalmente podemos resolver isso. A questão principal residia em como <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThreadPool.java"><code>DocumentsWriterPerThreadPool</code></a> permitia a reutilização de threads e recursos.</p>1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t0 20 getNextSequenceNumber 1 called from stack:
&lt;snip&gt;...&lt;/snip&gt;
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_1, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_5, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_2, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_6, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_4, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]<p>Aqui podemos ver cada thread sendo criada, fazendo referência à fila de exclusão inicial na geração 0.</p><p>Então, o avanço da fila ocorrerá no momento do flush, visualizando corretamente as 7 ações anteriores na fila.</p>1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t020 advanceQueue called from stack with maxSeq 9 lastSeqNo: 1 maxNumPendingOps: 7:
&lt;snip&gt;...&lt;/snip&gt;
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t525 getNextSequenceNumber 2 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t828 getNextSequenceNumber 3 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t727 getNextSequenceNumber 4 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t626 getNextSequenceNumber 5 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t424 getNextSequenceNumber 6 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t323 getNextSequenceNumber 7 called from stack:<p>Mas, antes que todas as threads terminem de ser processadas, duas são reutilizadas para um documento adicional:</p>1&gt; getAndLock: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; getAndLock: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]<p>Esses valores incrementarão o <code>seqNo</code> acima do máximo assumido, que foi calculado durante o flush como 7. Observe o <code>numDocsInRAM</code> adicional para os segmentos <code>_3</code> e <code>_0</code></p>1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_2, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=2, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_6, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_4, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_5, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_1, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=2, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove<p>Isso faz com que o Lucene contabilize incorretamente a sequência de ações do documento durante um flush, acionando essa falha no teste.</p><p>Como toda boa correção de bugs, a solução em si consiste em cerca de <a href="https://github.com/apache/lucene/pull/13627/files">10 linhas de código</a>. Mas dois engenheiros levaram vários dias para descobrir:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71d78f22ce6ec215/6a1706b61949f7bcd0e7a94a/7915e5ba2feba0fb8e846da0af11bfdca470c467-1600x622.jpg" alt="" /><h2>Nem todos os heróis usam capas.</h2><p>Sim, é clichê, mas é verdade.</p><p></p><p>A depuração de programas concorrentes é extremamente importante. Esses bugs de concorrência complexos levam uma quantidade excessiva de tempo para serem depurados e resolvidos. Embora novas linguagens como Rust possuam mecanismos integrados para ajudar a prevenir condições de corrida como essa, a maior parte do software no mundo já está escrita, e escrita em alguma linguagem diferente de <a href="https://www.rust-lang.org/">Rust</a>. Java, mesmo depois de todos esses anos, ainda é uma das linguagens mais utilizadas. A melhoria da depuração em linguagens baseadas na JVM torna o mundo da engenharia de software melhor. E considerando que algumas pessoas acreditam que o código será escrito por Grandes Modelos de Linguagem, talvez nosso trabalho como engenheiros acabe sendo apenas depurar código LLM ruim, em vez de apenas nosso próprio código ruim. Mas, independentemente do futuro da engenharia de software, a depuração de programas concorrentes continuará sendo fundamental para a manutenção e o desenvolvimento de software.</p><p></p><p>Obrigado, Ao Li, e seus colegas do PASTA Lab, por tornarem tudo ainda melhor.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/concurrency-bugs-lucene-debugging</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/concurrency-bugs-lucene-debugging</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Benjamin Trent,Ao Li]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf9fcbf843affc566/6a1706b8964ceaea3208bab9/7884e35f40c15fe349379ef7ae4648b21708962f-1070x712.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Aventuras com bugs do Lucene: Corrigindo uma exceção de índice corrompido]]></title>
    <description><![CDATA[Às vezes, uma única linha de código leva dias para ser escrita. Aqui, temos um vislumbre da dor e da depuração de um engenheiro ao longo de vários dias para corrigir uma possível corrupção do índice do Apache Lucene.]]></description>
    <content:encoded><![CDATA[<h2>Esteja preparado: </h2><p>Este blog em particular é diferente do habitual. Não se trata de uma explicação de uma nova funcionalidade ou de um tutorial. Trata-se de uma única linha de código que levou três dias para ser escrita. Vamos corrigir uma possível corrupção no índice do Apache Lucene. Algumas informações importantes que espero que você tenha absorvido:</p><ul><li><p>Todos os testes instáveis são repetíveis, desde que haja tempo suficiente e as ferramentas certas.</p></li><li><p>Várias camadas de testes são essenciais para sistemas robustos. No entanto, níveis mais altos de testes tornam-se cada vez mais difíceis de depurar e reproduzir.</p></li><li><p>Sleep é um excelente depurador.</p></li></ul><h2>Como o Elasticsearch realiza testes</h2><p>Na Elastic, temos uma infinidade de testes que são executados no código-fonte do Elasticsearch. Alguns são testes funcionais simples e focados, outros são testes de integração de "caminho feliz" de nó único, e outros ainda tentam quebrar o cluster para garantir que tudo se comporte corretamente em um cenário de falha. Quando um teste falha continuamente, um engenheiro ou uma ferramenta de automação cria um problema no GitHub e o sinaliza para que uma equipe específica o investigue. Este <a href="https://github.com/elastic/elasticsearch/issues/105122">erro específico</a> foi descoberto durante um teste do último tipo. Esses testes são complicados, às vezes só podendo ser repetidos após muitas tentativas.</p><h2>O que esse teste realmente avalia?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt58aab468587a7d35/6a17dd8e3e03d74b8d4f2b5f/63268f3b5714ebf9df8070ec2e3c4f0486ed822e-1600x1088.jpg" alt="Problema no GitHub: https://github.com/elastic/elasticsearch/issues/105122" /><p>Este teste em particular é interessante. Isso criará um mapeamento específico e o aplicará a um fragmento primário. Então, ao tentar criar uma réplica. A principal diferença é que, quando a réplica tenta analisar o documento, o teste injeta uma exceção, fazendo com que a recuperação falhe de uma forma surpreendente (mas esperada).</p><p></p><p>Tudo estava funcionando como esperado, com uma exceção importante. Durante a limpeza dos testes, validamos a consistência e, nesse ponto, o teste encontrou um problema.</p><p>
Este teste não apresentou o resultado esperado. Durante a verificação de consistência, verificaríamos se todos os arquivos de segmento Lucene replicados e primários estavam consistentes. Significa não corrompido e totalmente replicado. Ter dados parciais ou corrompidos é muito pior do que ter uma falha completa. Segue abaixo o rastreamento de pilha assustador e resumido da falha.</p><p></p>Caused by: org.apache.lucene.index.CorruptIndexException: Problem reading index from store(ByteSizeCachingDirectory(ElasticsearchMockDirectoryWrapper(HybridDirectory@/opt/buildkite-agent/builds/bk-agent-prod-gcp-1707109485745743789/elastic/elasticsearch-periodic/server/build/testrun/internalClusterTest/temp/org.elasticsearch.indices.recovery.IndexRecoveryIT_40853F21F419B395-001/tempDir-005/node_t0/indices/ZNwxG7VvShuwYV78RTjknA/0/index lockFactory=org.apache.lucene.store.NativeFSLockFactory@2c169f59))) (resource=store(ByteSizeCachingDirectory(ElasticsearchMockDirectoryWrapper(HybridDirectory@/opt/buildkite-agent/builds/bk-agent-prod-gcp-1707109485745743789/elastic/elasticsearch-periodic/server/build/testrun/internalClusterTest/temp/org.elasticsearch.indices.recovery.IndexRecoveryIT_40853F21F419B395-001/tempDir-005/node_t0/indices/ZNwxG7VvShuwYV78RTjknA/0/index lockFactory=org.apache.lucene.store.NativeFSLockFactory@2c169f59))))

    at org.apache.lucene.index.SegmentCoreReaders.&lt;init&gt;(SegmentCoreReaders.java:165)
    at org.apache.lucene.index.SegmentReader.&lt;init&gt;(SegmentReader.java:96)
    at org.apache.lucene.index.ReadersAndUpdates.getReader(ReadersAndUpdates.java:178)
    at org.apache.lucene.index.ReadersAndUpdates.getLatestReader(ReadersAndUpdates.java:243)
    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)
    at org.apache.lucene.index.FilterMergePolicy.keepFullyDeletedSegment(FilterMergePolicy.java:118)
    at org.apache.lucene.index.FilterMergePolicy.keepFullyDeletedSegment(FilterMergePolicy.java:118)
    at org.apache.lucene.index.ReadersAndUpdates.keepFullyDeletedSegment(ReadersAndUpdates.java:822)
    at org.apache.lucene.index.IndexWriter.isFullyDeleted(IndexWriter.java:6078)
    &lt;snip&gt;

    Caused by: java.io.FileNotFoundException: No sub-file with id .kdi found in compound file "_0.cfs" (fileName=_0.kdi files: [_0.pos, .nvm, .fnm, _0.tip, _Lucene90_0.dvd, _0.doc, _0.tim, _Lucene90_0.dvm, _ES87BloomFilter_0.bfm, .fdm, .nvd, _ES87BloomFilter_0.bfi, _0.tmd, .fdx, .fdt])

      at org.apache.lucene.codecs.lucene90.Lucene90CompoundReader.openInput(Lucene90CompoundReader.java:170)
      at org.apache.lucene.codecs.lucene90.Lucene90PointsReader.&lt;init&gt;(Lucene90PointsReader.java:63)
      at org.apache.lucene.codecs.lucene90.Lucene90PointsFormat.fieldsReader(Lucene90PointsFormat.java:74)
      at org.apache.lucene.index.SegmentCoreReaders.&lt;init&gt;(SegmentCoreReaders.java:152)
      &lt;snip&gt;
<p>De alguma forma, durante a falha de replicação forçada, o fragmento replicado acabou sendo corrompido! Deixe-me explicar a parte principal do erro em linguagem simples.</p><p></p><p>O Lucene é uma arquitetura baseada em segmentos, o que significa que cada segmento conhece e gerencia seus próprios arquivos somente leitura. Este segmento específico estava sendo validado por meio de seus <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/index/SegmentCoreReaders.java">SegmentCoreReaders</a> para garantir que tudo estivesse em ordem. Cada leitor principal possui metadados armazenados que indicam quais tipos de campos e arquivos existem para um determinado segmento. No entanto, ao validar o formato <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90PointsFormat.java">Lucene90PointsFormat</a>, alguns arquivos esperados estavam faltando. Com o arquivo de segmentos <code>_0.cfs</code> esperávamos um arquivo de formato de ponto chamado <code>kdi</code>. <code>cfs</code> significa "sistema de arquivos composto", no qual o Lucene às vezes combina todos os tipos de campo e todos os arquivos pequenos em um único arquivo maior para replicação mais eficiente e utilização de recursos. Na verdade, todas as três extensões de arquivo de ponto: <code>kdd</code>, <code>kdi</code> e <code>kdm</code> estavam faltando. Como é possível acessar um segmento do Lucene que espera encontrar um arquivo de ponto, mas ele está faltando?! Parece um bug de corrupção assustador!</p><p></p><h2>O primeiro passo para qualquer correção de bug é replicá-lo.</h2><p></p><p>Reproduzir a falha para esse bug específico foi extremamente difícil. Embora aproveitemos os <a href="https://en.wikipedia.org/wiki/Random_testing">testes de valores aleatórios</a> no Elasticsearch, garantimos que cada falha seja acompanhada de uma semente aleatória (esperamos) reproduzível para que todas as falhas possam ser investigadas. Bem, isso funciona muito bem para todas as falhas, exceto aquelas causadas por uma <a href="https://en.wikipedia.org/wiki/Race_condition">condição de corrida</a>.</p>./gradlew ':server:internalClusterTest' --tests "org.elasticsearch.indices.recovery.IndexRecoveryIT.testDoNotInfinitelyWaitForMapping" -Dtests.seed=40853F21F419B395 -Dtests.jvm.argline="-Des.concurrent_search=true" -Dtests.locale=id-ID -Dtests.timezone=Asia/Jerusalem -Druntime.java=21<p>Não importa quantas vezes eu tentasse, aquela semente específica nunca repetia a falha localmente. Mas existem maneiras de praticar os testes e buscar uma falha mais repetível.</p><p></p><p>Nosso conjunto de testes específico permite que um determinado teste seja executado mais de uma vez no mesmo comando por meio do parâmetro <code>-Dtests.iters</code> . Mas isso não era suficiente, eu precisava garantir que os threads de execução estivessem alternando e, assim, aumentando a probabilidade de ocorrência dessa condição de corrida. Outro problema no sistema era que o teste acabava demorando tanto para ser executado que o programa de execução do teste excedia o tempo limite. No fim, usei o seguinte comando bash extremamente complexo para executar o teste repetidamente:</p>for run in {1..10}; do ./gradlew ':server:internalClusterTest' --tests "org.elasticsearch.indices.recovery.IndexRecoveryIT.testDoNotInfinitelyWaitForMapping" -Dtests.jvm.argline="-Des.concurrent_search=true" -Dtests.iters=10 ; done || exit 1<p>E aí entra <a href="https://github.com/ColinIanKing/stress-ng">o estresse</a>. Isso permite iniciar rapidamente um processo que consumirá todos os núcleos da CPU num instante. Executar o stress-ng aleatoriamente em várias iterações do teste com falha finalmente me permitiu replicar a falha. Mais um passo. Para sobrecarregar o sistema, basta abrir outra janela do terminal e executar o seguinte comando:</p>stress-ng --cpu 16<h2>
Revelando o bug</h2><p>

Agora que a falha no teste que revelou o bug é praticamente repetível, é hora de tentar encontrar a causa. O que torna este teste em particular estranho é que o Lucene está gerando um erro porque espera valores pontuais, mas nenhum valor pontual é adicionado diretamente pelo teste. Somente valores de texto. Isso me levou a considerar analisar as mudanças recentes em nossos campos <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html">de controle de concorrência otimista</a> : <code>_seq_no</code> e <code>_primary_term</code>. Ambos são indexados como pontos e existem em todos os documentos do Elasticsearch.</p><p></p><p>De fato, um <a href="https://github.com/elastic/elasticsearch/pull/105036">commit</a> alterou nosso mapeador <code>_seq_no</code> ! SIM! Essa deve ser a causa! Mas minha empolgação durou pouco. Isso apenas alterou a ordem em que os campos foram adicionados ao documento. Antes dessa alteração, os campos <code>_seq_no</code> eram adicionados por último ao documento. Depois, eles foram adicionados primeiro. De jeito nenhum a ordem de adição de campos a um documento Lucene causaria essa falha...</p><p></p><p>Sim, a alteração na ordem em que os campos foram adicionados causou a falha. Isso foi surpreendente e acabou sendo um bug no próprio Lucene! Alterar a ordem em que os campos são analisados não deve alterar o comportamento da análise de um documento.</p><p></p><h2>O bug no Lucene</h2><p>De fato, o bug no Lucene se concentrava nas seguintes condições:</p><ul><li><p>Indexando um campo de valor de pontos (por exemplo) <code>_seq_no</code>)</p></li><li><p>Ao tentar indexar um campo de texto, ocorre um erro durante a análise.</p></li><li><p>Nesse estado atípico, abrimos um <a href="https://blog.mikemccandless.com/2011/06/lucenes-near-real-time-search-is-fast.html">leitor quase em tempo real</a> do escritor que apresentou a exceção de análise do índice de texto.</p></li></ul><p>Mas, por mais que eu tentasse, não conseguia replicar completamente. Adicionei pontos de pausa para depuração diretamente em toda a base de código do Lucene. Tentei abrir leitores aleatoriamente durante o caminho da exceção. Cheguei a imprimir megabytes e megabytes de registros tentando encontrar o caminho exato onde ocorreu essa falha. Eu simplesmente não consegui. Passei um dia inteiro lutando e perdendo.</p><p></p><p>Então eu dormi.</p><p></p><p>No dia seguinte, reli o rastreamento de pilha original e descobri a seguinte linha:</p><p>
</p>    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)<p>Em todas as minhas tentativas de recriação, nunca configurei especificamente a política de mesclagem de retenção. A <a href="https://github.com/apache/lucene/blob/5f0fa2b291ff9e7d878642f025a70c15b788a470/lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java">política SoftDeletesRetentionMergePolicy</a> é usada pelo Elasticsearch para que possamos replicar com precisão as exclusões nas réplicas e garantir que todos os nossos controles de concorrência sejam responsáveis por quando os documentos são realmente removidos. Caso contrário, o Lucene terá controle total e os removerá em qualquer mesclagem.</p><p></p><p>Assim que adicionei essa política e reproduzi os passos mais básicos mencionados acima, a falha se repetiu imediatamente.</p><p>
Nunca fiquei tão feliz em abrir um <a href="https://github.com/apache/lucene/issues/13353">bug no Lucene</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89d37c3454ec298b/6a17dd90ec0f8993c75a6511/c7d9ce3de5278923a8454097e2bdf487c861168a-1600x920.jpg" alt="Problema no Github: https://github.com/apache/lucene/issues/13353" /><p>
Embora se apresentasse como uma condição de corrida no Elasticsearch, era simples escrever um teste que falhasse repetidamente no Lucene, uma vez que todas as condições fossem atendidas.</p><p></p><p>No fim, como todos os bons bugs, ele foi corrigido com apenas uma linha de código. Vários dias de trabalho para apenas uma linha de código.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdab62db3402b27f7/6a17dd92dbb4ff23e7fb55ad/2c10a02eb41da181b1dceee47186c076c9b14a90-1600x539.jpg" alt="correção de uma linha de código" /><p>Mas valeu a pena.</p><h2>
Não é o fim</h2><p>Espero que tenham gostado dessa jornada incrível comigo! Escrever software, especialmente software tão amplamente utilizado e complexo quanto o Elasticsearch e o Apache Lucene, é gratificante. No entanto, às vezes, é extremamente frustrante. Eu amo e odeio software. A correção de erros nunca termina!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/lucene-corrupted-index-exception</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/lucene-corrupted-index-exception</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc25119c4add97ba/6a17dd94420229633929f4f7/2c918bad62530ff6fbe419092b2ca44bfe9408c4-944x612.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Entendendo a quantização escalar no Lucene]]></title>
    <description><![CDATA[Descubra como a Elastic introduziu a quantização escalar no Lucene, incluindo a quantização automática de bytes, a quantização por segmento e insights de desempenho.]]></description>
    <content:encoded><![CDATA[<h2>Quantização automática de bytes no Lucene</h2><p>Embora o HNSW seja uma maneira poderosa e flexível de armazenar e pesquisar vetores, ele exige uma quantidade significativa de memória para funcionar rapidamente. Por exemplo, consultar vetores float32 de 1 milhão de dimensões e 768 dimensões requer aproximadamente  de RAM. Quando você começa a pesquisar um número significativo de vetores, isso se torna caro. Uma forma de usar cerca de  menos memória é através da quantização de bytes. O Lucene, e consequentemente o Elasticsearch, já suportam a indexação de vetores  há algum tempo, mas a construção desses vetores era responsabilidade do usuário. Isso está prestes a mudar, pois introduzimos a quantização escalar  no Lucene.</p><h2>Quantização escalar 101</h2><p>Todas as técnicas de quantização são consideradas transformações com perda dos dados brutos. Significa que algumas informações são perdidas em função do espaço disponível. Para uma explicação detalhada da quantização escalar, veja: <a href="https://www.elastic.co/search-labs/scalar-quantization-101">Quantização Escalar 101</a>. Em termos gerais, a quantização escalar é uma técnica de compressão com perdas. Uma matemática simples permite economizar espaço significativamente com pouco impacto na capacidade de memorização.</p><h2>Explorando a arquitetura</h2><p>Quem já está acostumado a trabalhar com o Elasticsearch provavelmente já conhece esses conceitos, mas aqui vai uma breve visão geral da distribuição de documentos para pesquisa.</p><p>Cada índice do Elasticsearch é composto por <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#size-your-shards">múltiplos shards</a>. Embora cada fragmento só possa ser atribuído a um único nó, vários fragmentos por índice proporcionam paralelismo computacional entre os nós.</p><p>Cada fragmento é composto por um único <a href="https://lucene.apache.org/core/9_8_0/core/org/apache/lucene/index/package-summary.html">Índice Lucene</a>. Um índice Lucene consiste em múltiplos segmentos somente leitura. Durante a indexação, os documentos são armazenados em buffer e periodicamente transferidos para um segmento somente leitura. Quando determinadas condições são atendidas, esses segmentos podem ser mesclados em segundo plano, formando um segmento maior. Tudo isso é configurável e possui seu próprio conjunto de complexidades. Mas, quando falamos de segmentos e fusão, estamos falando de segmentos Lucene somente leitura e da fusão periódica automática desses segmentos. <a href="https://www.elastic.co/search-labs/vector-search-elasticsearch-rationale">Aqui está uma análise mais aprofundada</a> sobre a fusão de segmentos e as decisões de design.</p><h2>Quantização por segmento no Lucene</h2><p>Cada segmento no Lucene armazena o seguinte: os vetores individuais, os índices do grafo HNSW, os vetores quantizados e os quantis calculados. Por uma questão de brevidade, vamos nos concentrar em como o Lucene armazena vetores quantizados e brutos. Para cada segmento, mantemos o controle dos vetores brutos no arquivo  , dos vetores quantizados e de um único multiplicador corretivo de ponto flutuante em , além dos metadados relativos à quantização no arquivo  .</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1861d0af970fd096/6a17d9887f6f15a45dc099c0/507a4d578f8a5d2225b11d16ac1fc0b7dd39eaa1-1207x228.png" alt="O arquivo .vec Arquivo" /><p>Figura 1: Layout simplificado de um arquivo de armazenamento vetorial bruto. Ocupa  de espaço em disco, já que os valores  têm 4 bytes. Como estamos utilizando quantização, esses dados não serão carregados durante a busca por HNSW. Eles só são usados se forem especificamente solicitados (por exemplo, força bruta secundária via <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#query-rescorer">reavaliação</a>), ou para requantização durante a fusão de segmentos.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8c52b51939357280/6a17d9897b54f9130f8b3761/ffc6e8bffefc5cd36f14aae315184c89e04a6587-1440x207.png" alt="O arquivo .veq" /><p>Figura 2: Layout simplificado do arquivo  arquivo. Ocupa  de espaço e será carregado na memória durante a busca. Os  bytes servem para contabilizar o multiplicador corretivo de ponto flutuante, usado para ajustar a pontuação visando maior precisão e melhor recuperação da informação.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt28e007988f81e5c0/6a17d98b25daabe23208a0d9/555cc92d7d179d8c9711cefbaaac3e0b815d2e42-1440x182.png" alt="O arquivo .vemq" /><p>Figura 3: Layout simplificado do arquivo de metadados. Aqui, monitoramos a quantização e a configuração vetorial, juntamente com os quantis calculados para este segmento.</p><p>Assim, para cada segmento, armazenamos não apenas os vetores quantizados, mas também os quantis usados na criação desses vetores quantizados e os vetores brutos originais. Mas, afinal, por que ainda guardamos os vetores brutos?</p><h2>Quantização que cresce com você</h2><p>Como o Lucene periodicamente limpa os segmentos para que fiquem somente leitura, cada segmento possui apenas uma visão parcial de todos os seus dados. Isso significa que os quantis calculados se aplicam diretamente apenas a esse conjunto de amostras do total de seus dados. Isso não é um grande problema se sua amostra representar adequadamente todo o seu conjunto de dados. Mas o Lucene permite que você classifique seu índice de várias maneiras. Portanto, você pode estar indexando dados classificados de uma forma que introduza viés nos cálculos de quantis por segmento. Além disso, você pode apagar os dados quando quiser! Seu conjunto de amostras pode ser minúsculo, até mesmo contendo apenas um vetor. Outro fator complicador é que você tem controle sobre quando as fusões ocorrem. Embora o Elasticsearch tenha configurações padrão e mesclagem periódica definidas, você pode solicitar uma mesclagem sempre que desejar por meio da API <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.10/indices-forcemerge.html">_force_merge</a> . Como podemos, então, manter toda essa flexibilidade e, ao mesmo tempo, fornecer uma boa quantização que proporcione uma boa recuperação?</p><p>A quantização vetorial do Lucene se ajustará automaticamente ao longo do tempo. Como o Lucene foi projetado com uma arquitetura de segmentos somente leitura, temos a garantia de que os dados em cada segmento não foram alterados e demarcações claras no código para quando as coisas podem ser atualizadas. Isso significa que, durante a fusão de segmentos, podemos ajustar os quantis conforme necessário e, possivelmente, requantizar os vetores.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d9a2390958c3e82/6a17d98cbe6086e4fd0045d5/646baa8279719d06c05c1a4fdb80a0c060e0dd94-1440x446.png" alt="quantis de múltiplos segmentos" /><p>Figura 4: Três exemplos de segmentos com diferentes quantis.</p><p>Mas a requantização não é cara? Possui alguma sobrecarga, mas o Lucene lida com quantis de forma inteligente e só requantiza completamente quando necessário. Vamos usar os segmentos da Figura 4 como exemplo. Vamos atribuir   e  , e apenas  documentos ao segmento  O Lucene calculará a média ponderada dos quantis e, se o quantil resultante da fusão for suficientemente próximo dos quantis originais do segmento, não será necessário requantizar esse segmento, utilizando-se os quantis recém-combinados.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte2596c10ee616cf3/6a17d98eec0f8903b85a6497/58fa20d60dc337ca7108eb01e84a07c330e87ee6-1440x447.png" alt="quantis combinados" /><p>Figura 5: Exemplo de quantis combinados onde os segmentos  e  têm  documentos e  tem apenas .</p><p>Na situação visualizada na figura 5, podemos ver que os quantis resultantes da fusão são muito semelhantes aos quantis originais em  e  Portanto, eles não justificam a quantização dos vetores. O segmento  parece desviar-se demasiado. Consequentemente, os vetores em  seriam requantizados com os valores de quantil recém-combinados.</p><p>Existem, de fato, casos extremos em que os quantis combinados diferem drasticamente de quaisquer quantis originais. Neste caso, iremos extrair uma amostra de cada segmento e recalcular completamente os quantis.</p><h2>Desempenho e números da quantização</h2><p>Então, é rápido e ainda oferece boa capacidade de memorização? Os números a seguir foram coletados executando o experimento em uma instância <code>c3-standard-8</code> do GCP. Para garantir uma comparação justa com  usamos uma instância grande o suficiente para armazenar vetores brutos na memória. Indexamos  vetores <a href="https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings">do Cohere Wiki</a> usando o método do produto interno máximo.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfa48e3724ad97fed/6a17d98f445de96aca4cff88/81958f99b8a1397f669db48de00f264cacc6b49f-576x455.png" alt="Recuperação de Quantização" /><p>Figura 6: Recall@10 para vetores quantizados versus vetores brutos. O desempenho de busca de vetores quantizados é significativamente mais rápido do que o de vetores brutos, e a recuperação é rapidamente possível reunindo apenas mais 5 vetores; visível por .</p><p>A Figura 6 ilustra a história. Embora haja uma diferença na recordação, como era de se esperar, ela não é significativa. Além disso, a diferença na taxa de recall desaparece ao coletar apenas mais 5 vetores. Tudo isso com fusões de segmentos  mais rápidas e 1/4 da memória dos vetores  .</p><h2>Conclusão</h2><p>O Lucene oferece uma solução única para um problema difícil. Não há necessidade de uma etapa de "treinamento" ou "otimização" para a quantização. No Lucene, simplesmente funcionará. Não há necessidade de se preocupar com a possibilidade de ter que "re-treinar" seu índice vetorial caso seus dados mudem. O Lucene detectará mudanças significativas e cuidará disso automaticamente ao longo da vida útil dos seus dados. Aguardem ansiosamente o momento em que implementaremos essa funcionalidade no Elasticsearch!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene</guid>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[Pesquisa de aprendizado de máquina]]></category>
    <dc:creator><![CDATA[Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb382d99ffac9992c/6a17d991b1e113640179f12c/dc07f16d347a68476bded5df9adb831452f61278-1024x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Sat, 11 Nov 2023 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>