<?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/es/search-labs/author/benjamin-trent</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/author/benjamin-trent</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/author/benjamin-trent.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 08:09:49 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Escalar modelos de interacción tardía en Elasticsearch: parte 2]]></title>
    <description><![CDATA[Este artículo analiza técnicas para preparar los vectores de interacción tardía para las cargas de trabajo de producción a gran escala, como reducir el uso de espacio en disco y mejorar la eficiencia de cómputo.]]></description>
    <content:encoded><![CDATA[<p>En nuestro <a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">blog anterior sobre ColPali</a>, analizamos cómo crear aplicaciones de búsqueda visual con Elasticsearch. Nos centramos en el valor que modelos como ColPali aportan a nuestras aplicaciones, pero presentan inconvenientes de rendimiento en comparación con la búsqueda vectorial con bicodificadores como E5.</p><p>Partiendo de los ejemplos de <a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">la parte 1</a>, este blog analiza cómo usar diferentes técnicas y el poderoso kit de herramientas de búsqueda vectorial de Elasticsearch para preparar vectores de interacción tardía para cargas de trabajo de producción a gran escala.</p><p>Los ejemplos de código completos se pueden encontrar en <a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/colpali">GitHub.</a></p><h2>Desafíos de los modelos de interacción tardía</h2><p>ColPali crea más de 1000 vectores por página para los documentos de nuestro índice.</p><p>Esto plantea dos desafíos al trabajar con vectores de interacción tardía:</p><ol><li><p>Espacio en disco: almacenar todos estos vectores en los discos implicará un uso considerable de almacenamiento, lo que será costoso a escala.</p></li><li><p>Cómputo: al clasificar los documentos mediante la comparación <code>maxSimDotProduct()</code>, necesitamos comparar todos estos vectores de cada documento con los N vectores de nuestra búsqueda.</p></li></ol><p>Veamos algunas técnicas para abordar estos problemas.</p><h2>Técnicas para optimizar modelos de interacción tardía</h2><h3>Vectores de bits</h3><p>Para reducir el espacio en disco, podemos comprimir las imágenes en vectores de bits. Podemos usar una función sencilla en Python para transformar nuestros multivectores en vectores 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>El principio básico de la función es simple: los valores mayores que 0 se convierten en 1 y los valores menores que 0 se convierten en 0. Esto da como resultado un arreglo de 0 y 1, que luego se convierte en una cadena hexadecimal que representa el vector de bits.</p><p>Para nuestro mapping de índice, establecemos el parámetro <code>element_type</code> en <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>Después de haber escrito todos nuestros nuevos vectores de bits en nuestro índice, podemos clasificar nuestros vectores de bits usando el siguiente código:</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>A cambio de una pequeña pérdida de precisión, esto nos permite utilizar la distancia de Hamming (<code>maxSimInvHamming(...)</code>), la cual aprovecha optimizaciones como máscaras de bits, SIMD, etc. Obtén más información sobre <a href="https://www.elastic.co/search-labs/blog/bit-vectors-in-elasticsearch">los vectores de bits y la distancia de Hamming en nuestro blog</a>.</p><p>De forma alternativa, podemos no convertir nuestro vector de búsqueda en vectores de bits y realizar una búsqueda con el vector de interacción tardía de fidelidad completa:</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 del uso de vectores de bits para optimizar modelos de interacción tardía" /><p>Esto comparará nuestros vectores usando una función de similitud asimétrica.</p><p></p><p>Pensemos en una distancia de Hamming regular entre dos vectores de bits. Supongamos que tenemos un vector de documento <em>D:</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4b6ece1ec4dedac/6a17f4694b055d248143234e/366a70a23e37d403788327b7aefc873fd4482f5e-1235x86.png" alt="" /><p>Y un vector de búsqueda  <em>Q:</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7721df9c8f0d84f1/6a17f46b3e03d77cf54f2dcb/978ec31e0afbc0eaebf548a015b628c0cad84625-1247x84.png" alt="" /><p></p><p>La cuantificación binaria simple transformará los vectores <em>D</em> en <code>10101101</code> y <em>Q</em> en <code>11111011</code>. Para encontrar la distancia de Hamming, necesitamos matemáticas de bits directas; es extremadamente rápido. En este caso, la distancia de Hamming es <code>01010110</code>, que tiene un recuento de bits de 4. Así que, el puntaje se convierte en el inverso de esa distancia de Hamming. Hay que recordar que cuanto más vectores similares haya, menor será la distancia de Hamming, por lo que invertirla permite que los vectores más similares tengan una puntuación más alta. Concretamente aquí, el puntaje sería 1/4 = <code>0.25</code>.</p><p>Sin embargo, observa cómo perdemos la magnitud de cada dimensión. Un <code>1</code> es un <code>1</code>. Así que, para <em>Q</em>, la diferencia entre <code>0.01</code> y <code>0.79</code> desaparece. Como simplemente estamos cuantizando según <code>&gt;0</code>, podemos hacer un pequeño truco donde el vector Q no está cuantizado. Esto no permite realizar operaciones matemáticas bit a bit extremadamente rápidas, pero mantiene bajo el costo de almacenamiento, ya que D sigue cuantificado.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blted50315518e2d599/6a17f46c414c6463709452d3/508e8498ba969534d2a0131d431c75735fc27cb9-1399x611.png" alt="" /><p>En resumen, esto conserva la información proporcionada en <em>Q</em>, lo que aumenta la calidad de estimación de distancia y mantiene el almacenamiento bajo.</p><p>El uso de vectores de bits nos permite ahorrar significativamente en espacio en disco y carga computacional en el momento de la búsqueda. Pero hay más que podemos hacer.</p><h3>Vectores promedio</h3><p>Para escalar nuestra búsqueda a cientos de miles de documentos, ni siquiera las ventajas de rendimiento que nos ofrecen los vectores de bits serán suficientes. Para escalar a este tipo de cargas de trabajo, querremos aprovechar la estructura de índice HNSW de Elasticsearch para la búsqueda vectorial.</p><p>ColPali genera alrededor de mil vectores por documento, lo que es demasiado para agregar a nuestro grafo HNSW. Por lo tanto, necesitamos reducir el número de vectores. Para esto, se puede crear una única representación del significado del documento promediando todos los vectores del documento que genera ColPali al incrustar nuestra imagen.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc053425fbfcf8de8/6a17f46f148009faf1b488aa/7c3b4dffb70bd95f67deb35f8c01e73c5286c2ab-1476x1102.png" alt="Vector promedio sobre todos los vectores de interacción tardía" /><p>Por ahora, esto no es posible dentro de Elastic, por lo que tendremos que procesar previamente los vectores antes de ingestarlos en Elasticsearch. </p><p>Podemos hacer esto con Logstash o pipelines de ingesta, pero aquí usaremos una función sencilla en 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>Además, normalizamos el vector; de este modo, podremos usar la similitud por producto punto.</p><p>Luego de transformar todos nuestros vectores de ColPali en vectores promedio, podemos indexarlos en nuestro 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>Debemos tener en cuenta que esto aumentará el uso total del disco, ya que estamos almacenando más información junto con nuestros vectores de interacción tardíos. Además, usaremos RAM adicional para almacenar el grafo HNSW, lo que nos permitirá escalar la búsqueda a miles de millones de vectores. Para reducir el uso de RAM, podemos utilizar nuestra popular <a href="https://www.elastic.co/search-labs/blog/optimized-scalar-quantization-elasticsearch">característica BBQ</a>. A cambio, obtenemos resultados de búsqueda rápidos en conjuntos de datos masivos que de otra manera no serían posibles.</p><p>Ahora, simplemente hacemos una búsqueda con la consulta knn para encontrar nuestros documentos más 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>La mejor coincidencia anterior, lamentablemente, cayó al tercer rango.</p><p>Para solucionar este problema, podemos realizar una recuperación en varias etapas. En nuestra primera etapa, utilizamos la búsqueda knn para buscar los mejores candidatos para nuestra búsqueda en millones de documentos. En la segunda etapa, solo reclasificamos los principales k (aquí: 10) con la mayor fidelidad de los vectores de interacción tardía de 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 del uso de vectores promedio para optimizar modelos de interacción tardía" /><p>Aquí, usamos el <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.18/retriever.html#rescorer-retriever">recalificador</a> introducido en 8.18 para reclasificar nuestros resultados. Después de la recalificación, vemos que nuestra mejor coincidencia está nuevamente en la primera posición. </p><p>Nota: En una aplicación de producción, podemos usar un valor de k mucho mayor que 10, ya que la función de similitud máxima sigue siendo comparativamente eficiente.</p><h3>Agrupación de tokens</h3><p>La agrupación de tokens reduce la longitud de la secuencia de incrustaciones de vectores múltiples al agrupar información redundante, como parches de fondo blanco. Esta técnica reduce el número de incrustaciones, al tiempo que conserva la mayor parte de la señal de la página.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3d643e6ac908f640/6a17f4754b055d3783432352/09eae0b768f4b450e555d7e53e57561bd52de2c0-1412x1056.png" alt="Agrupación de tokens para optimizar modelos de interacción tardía" /><p>El agrupamiento de tokens funciona mediante la agrupación de incrustaciones similares de tokens dentro de un documento en clústeres con un algoritmo de agrupamiento en clusters. A continuación, se calcula la media de los vectores de cada grupo para crear una representación única y agregada. Este vector agregado reemplaza los tokens originales en el grupo, lo que reduce el número total de vectores sin una pérdida significativa de la señal del documento.</p><p>El documento de ColPali propone un valor inicial de factor de agrupación de 3 para la mayoría de los sets de datos, lo que mantiene el 97,8 % del rendimiento original y al mismo tiempo reduce el número total de vectores en un 66,7 %. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d78a15c6944a1ea/6a17f477414c64a2929452d9/343cc9eaf54af8c7a125d4115838f3c7d5659de0-1600x1007.png" alt="Factor de agrupación para optimizar modelos de interacción tardía" /><p>Pero debemos tener cuidado: el set de datos “Shift”, que contiene documentos muy densos y con mucho texto con poco espacio en blanco, disminuye rápidamente en rendimiento a medida que aumentan los factores de grupo.</p><p>Para crear los vectores agrupados, podemos usar la 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>Ahora tenemos un vector que se redujo en aproximadamente un 66,7 % en sus dimensiones. Lo indexamos como siempre y podemos hacer búsquedas en él con nuestra función <code>maxSimDotProduct()</code>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc460c14031814ede/6a17f4794202292bf629f72d/2412c5db7d79a590b96d42fe01f140c42f010612-1600x481.png" alt="Resultados de modelos de interacción tardía" /><p>Podemos obtener buenos resultados de búsqueda a costa de cierta precisión en los resultados.</p><p>Sugerencia: con un pool_factor más alto (100-200), también puedes tener un término medio entre la solución vectorial promedio y la que analizamos aquí. Con unos 5 a 10 vectores por documento, es viable indexarlos en un campo anidado para aprovechar el índice HNSW.</p><h2>Codificador cruzado vs. interacción tardía vs. bicodificador</h2><p>Con lo que hemos aprendido hasta ahora, ¿dónde se sitúan los modelos de interacción tardía, como ColPali o ColBERT, cuando los comparamos con otras técnicas de recuperación mediante AI?</p><p>Aunque la función max sim es más económica en comparación con los codificadores cruzados, todavía requiere muchas más comparaciones y cálculos que la búsqueda vectorial con codificadores binarios, en la que solo se comparan dos vectores para cada par de búsqueda-documento. </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbfd97f3bba3e5b17/6a17f47be31791b0052d5943/75e1fc9e601aa7a6e88f137565c1919166c7a71c-1480x458.png" alt="Codificador cruzado vs. modelos de interacción tardía vs. bicodificador" /><p>Por eso, nuestra recomendación para modelos de interacción tardía es usarlos generalmente solo para reclasificar los k mejores resultados de búsqueda. También reflejamos esto en el nombre del tipo de campo: rank_vectors.</p><p>¿Pero qué pasa con el codificador cruzado? ¿Son mejores los modelos de interacción tardía porque son más baratos de ejecutar en el momento de la búsqueda? Como suele suceder, la respuesta es: depende. Los codificadores cruzados generalmente producen resultados de mayor calidad, pero requieren una gran cantidad de recursos de cómputo porque los pares de búsqueda-documento necesitan hacer un pase completo a través del modelo transformador. También se benefician del hecho de que no requieren indexar vectores y pueden operar de manera sin estado. Esto da como resultado lo siguiente:</p><ul><li><p>Menor uso de espacio en disco.</p></li><li><p>Un sistema más sencillo.</p></li><li><p>Mayor calidad de los resultados de búsqueda</p></li><li><p>Una latencia más alta y, por lo tanto, la imposibilidad de realizar una reclasificación tan profunda.</p></li></ul><p>Por otro lado, los modelos de interacción tardía pueden descargar parte de este cálculo en el índice, lo que hace que la búsqueda sea más económica. El precio que pagar es la necesidad de indexar los vectores, lo que aumenta la complejidad de nuestros pipelines de indexación y requiere más espacio en disco para almacenarlos.</p><p>En el caso concreto de ColPali, el análisis de la información de las imágenes resulta muy costoso, porque contienen una gran cantidad de datos. En este caso, el equilibrio se desplaza a favor de utilizar un modelo de interacción tardía como ColPali porque evaluar esta información en el momento de la búsqueda consumiría demasiados recursos o sería demasiado lento. </p><p>Para un modelo de interacción tardía como ColBERT, que funciona con datos de texto como la mayoría de los codificadores cruzados (por ejemplo, elastic-rerank-v1), la decisión podría inclinarse más hacia usar el codificador cruzado para beneficiarse del ahorro y la simplicidad en disco.</p><p>Te recomendamos que evalúes las ventajas y desventajas para tu caso de uso y experimentes con las diferentes herramientas que Elasticsearch te proporciona para crear las mejores aplicaciones de búsqueda.</p><h2>Conclusión</h2><p>En este blog, analizamos diversas técnicas para optimizar modelos de interacción tardía como ColPali para búsquedas vectoriales a gran escala en Elasticsearch. Si bien los modelos de interacción tardía proporcionan un sólido equilibrio entre la eficiencia de recuperación y la calidad de clasificación, también presentan desafíos relacionados con el almacenamiento de información y el cómputo.</p><p>Para abordar estos desafíos, analizamos:</p><ul><li><p><strong>Vectores de bits</strong> para reducir significativamente el espacio en disco, al tiempo que se aprovechan los cálculos de similitud eficientes como la distancia de Hamming o la similitud máxima asimétrica.</p></li><li><p><strong>Promedio de vectores</strong> para comprimir múltiples inserciones en una sola representación densa, lo que permite una recuperación eficiente con indexación HNSW.</p></li><li><p><strong>Agrupación de tokens</strong> para fusionar de forma inteligente las incorporaciones redundantes mientras se mantiene la integridad semántica, lo que reduce la sobrecarga computacional en el momento de la búsqueda.</p></li></ul><p>Elasticsearch ofrece un conjunto potente de herramientas para personalizar y optimizar las aplicaciones de búsqueda en función de tus necesidades. Ya sea que priorices la velocidad de recuperación, la calidad de clasificación o la eficiencia de almacenamiento, estas herramientas y técnicas te permiten equilibrar el rendimiento y la calidad según lo que necesites para tus aplicaciones reales.</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[Relevancia]]></category>
    <category><![CDATA[Base de datos vectorial]]></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[Errores de concurrencia en Lucene: Cómo corregir fallos de concurrencia optimistas]]></title>
    <description><![CDATA[Gracias a Fray, un marco determinista de pruebas de concurrencia del laboratorio PASTA de CMU, localizamos un bug complicado de Lucene y lo eliminamos]]></description>
    <content:encoded><![CDATA[<p>Sí, otro blog para corregir errores. Pero este tiene un giro: un héroe de código abierto aparece y salva el día. </p><p>Depurar bugs de concurrencia no es nada fácil, pero vamos a entrar en ello. Entra en escena Fray, un marco determinista de pruebas de concurrencia del laboratorio PASTA de CMU, que convierte fallos irregulares en fallos fiables y reproducibles. Gracias al ingenioso diseño de shadow lock de Fray y su control preciso del hilo, localizamos un bug complicado de Lucene y finalmente lo superamos. Esta entrada explora cómo los héroes y herramientas del código abierto están haciendo que la depuración concurrente sea menos dolorosa—y que el mundo del software sea mucho mejor.</p><h2>Errores de concurrencia: la maldición de los ingenieros de software</h2><p>Los errores de concurrencia son los peores. No solo son difíciles de arreglar, sino que simplemente conseguir que fallen de forma fiable es la parte más complicada. Tomemos este fallo de prueba, <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat#testGlobalVersions</code></a>, como ejemplo. Genera múltiples hilos de redacción y actualización de documentos, desafiando el modelo optimista de concurrencia de Lucene. Esta prueba expuso una condición de carrera en el control optimista de concurrencia. Es decir, una operación de documento puede afirmar erróneamente ser la última de una secuencia de operaciones 😱. Es decir, en ciertas condiciones, una operación de actualización o eliminación podría tener éxito cuando debería fallar dadas las limitaciones optimistas de concurrencia.</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>Disculpas a quienes odian los rastreos de Java Stack. Ten en cuenta que eliminar no significa necesariamente "eliminar". También puede indicar una "actualización" de documento, ya que los segmentos de Lucene son de solo lectura.
</p><p>Apache Lucene gestiona cada hilo que consiste en escribir documentos a través de la clase <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> . Esta clase creará o reutilizará hilos para la redacción de documentos y cada acción de escritura controla su información dentro de la clase <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java"><code>DocumentsWriterPerThread</code></a> (DWPT). Además, el autor lleva un registro de qué documentos se eliminan en el <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> (DWDQ). Estas estructuras mantienen todas las acciones de mutación de los documentos en memoria y periódicamente se vacian, liberando recursos en memoria y estructuras persistentes en disco.</p><p></p><p>Para evitar <a href="https://en.wikipedia.org/wiki/Blocking_(computing)">bloquear hilos</a> y cerciorar un alto rendimiento en sistemas concurrentes, Apache Lucene intenta <a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html">sincronizar</a> solo en secciones muy críticas. Aunque esto puede ser bueno en la práctica, como en cualquier sistema concurrente, hay dragones.</p><h2>
Una falsa esperanza</h2><p>Mi investigación inicial me llevó a un par de secciones críticas que no estaban sincronizadas adecuadamente. Todas las interacciones con un <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> dado están controladas por su <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a>que encierra . Así que, aunque los métodos individuales pueden no estar adecuadamente sincronizados en el <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a>, su acceso al mundo lo es (o debería estar). (No profundicemos en cómo esto confunde la propiedad y el acceso: es un proyecto de larga duración escrito por muchos colaboradores. Ten un poco de margen.)</p><p></p><p>Sin embargo, encontré <a href="https://github.com/apache/lucene/blob/40060f8b7080d06a218518445a0a1dfc520c812a/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java#L572-L576">un sitio durante un flush</a> que no estaba 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>Estas acciones no están sincronizadas en una sola operación atómica. Es decir, entre <code>newQueue</code> de creación y llamada a <code>getMaxSeqNo</code>, otro código podría haber ejecutado incrementando el número de secuencia en la clase <code>documentsWriter</code> . ¡Encontré el bicho!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc55033280b45913/6a1706b4dc55dec27fe00d30/3f2617a102d28735e755a7539b047c28beda41be-1600x453.jpg" alt="" /><p>
Pero, como ocurre con la mayoría de los bugs complejos, encontrar la causa raíz no fue sencillo. Fue entonces cuando intervino un héroe.</p><h2>Un héroe en la refriega</h2><p>Entran en escena nuestro héroe: <a href="https://aoli.al/">Ao Li</a> y sus colegas del Laboratorio PATA. Le dejaré explicar cómo salvaron el día con Fray.</p><p><a href="https://github.com/cmu-pasta/fray">Fray</a> es un marco determinista de pruebas de concurrencia desarrollado por investigadores del <a href="https://pastalab.org/">PASTA Lab</a>, Universidad Carnegie Mellon. La motivación detrás de la construcción de Fray proviene de una brecha notable entre la academia y la industria: aunque las pruebas de concurrencia deterministas se estudiaron extensamente en la investigación académica durante más de 20 años, los profesionales siguen confiando en las pruebas de estrés —un método ampliamente reconocido como poco fiable e inestable— para poner a prueba sus programas concurrentes. Por ello, queríamos diseñar e implementar un marco determinista de pruebas de concurrencia con generalidad y aplicabilidad práctica como objetivo principal.</p><p></p><h2>La idea central</h2><p>En esencia, Fray aprovecha un principio sencillo pero poderoso: la ejecución secuencial. El modelo de concurrencia de Java proporciona una <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.3">propiedad</a>clave: si un programa está libre de carreras de datos, todas las ejecuciones aparecerán secuencialmente consistentes. Esto significa que el comportamiento del programa puede representar como una secuencia de sentencias del programa.</p><p>Fray funciona correr el programa destino de forma secuencial: en cada paso, pausa todos los hilos excepto uno, permitiendo a Fray controlar con precisión la planeación de hilos. Los hilos se seleccionan aleatoriamente para simular la concurrencia, pero las elecciones se registran para una posterior repetición determinista. Para optimizar la ejecución, Fray solo realiza cambios de contexto cuando un hilo está a punto de ejecutar una instrucción de sincronización, como el bloqueo o acceso atómico/volátil. Una buena característica de la libertad entre datos y razas es que este cambio limitado de contexto es suficiente para explorar todos los comportamientos observables debidos a cualquier entrelazado de hilos (<a href="https://arxiv.org/abs/2501.12618">nuestro artículo</a> tiene un esbozo de demostración).</p><p></p><h2>El reto: controlar la planeación de hilos</h2><p>Aunque la idea central parece sencilla, implementar Fray presentó desafíos significativos. Para controlar la planeación de hilos, Fray debe gestionar la ejecución de cada hilo de aplicación. A primera vista, esto podría parecer sencillo: reemplazar primitivas de concurrencia por implementaciones personalizadas. Sin embargo, el control de concurrencia en la JVM es complejo, implicando una mezcla de <a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html">instrucciones 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 nivel</a> y <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>Esto resultó ser un agujero de conejo:</p><p></p><ul><li><p>Por ejemplo, cada <code>MONITORENTER</code> instrucción debe tener un <code>MONITOREXIT</code> correspondiente en el mismo método. Si Fray reemplaza <code>MONITORENTER</code> por una llamada a método a un stub/mock, también debe reemplazar <code>MONITOREXIT</code>.</p></li><li><p>En el código que emplea <code>object.wait/notify</code>, si <code>MONITORENTER</code> se reemplaza, también debe ser reemplazada la <code>object.wait</code> correspondiente. Esta cadena de reemplazo se extiende hasta <code>object.notify</code> y más allá.</p></li><li><p>La JVM invoca ciertos métodos relacionados con la concurrencia (por ejemplo, <code>object.notify</code> cuando termina un hilo) dentro del código nativo. Reemplazar estas operaciones requeriría modificar la propia JVM.</p></li><li><p>Las funciones de la JVM, como cargadores de clases e hilos de recogida de basura (GC), también emplean primitivas de concurrencia. Modificar estas primitivas puede crear desajustes con esas funciones de la JVM.</p></li><li><p>Reemplazar primitivas de concurrencia en el JDK suele provocar fallos de la JVM durante su fase de inicialización.</p></li></ul><p></p><p>Estos desafíos dejaron claro que una sustitución integral de los primitivos de concurrencia no era factible.</p><h2>
Nuestra solución: diseño de cerraduras de sombra</h2><p>Para abordar estos desafíos, Fray emplea un novedoso mecanismo de bloqueo de sombra para orquestar la ejecución de hilos sin reemplazar primitivas de concurrencia. Los bloqueos sombra actúan como intermediarios que guían la ejecución del hilo. Por ejemplo, antes de adquirir un bloqueo, un hilo de aplicación debe interactuar con su correspondiente bloqueo de sombra. El bloqueo sombra determina si el hilo puede adquirir el bloqueo. Si el hilo no puede continuar, el bloqueo sombra lo bloquea y permite que otros hilos se ejecuten, evitando bloqueos y permitiendo una concurrencia controlada. Este diseño permite a Fray controlar el entrelazado de hilos de forma transparente mientras preserva la corrección de la semántica de concurrencia. Cada primitiva de concurrencia está cuidadosamente modelada dentro del marco de bloqueo de sombra para garantizar la solidez y la completitud. Más detalles técnicos pueden encontrar en nuestro artículo.</p><p></p><p>Además, este diseño pretende ser a prueba de futuro. Al requerir únicamente la instrumentación de bloqueos de sombra alrededor de primitivas de concurrencia, se garantiza compatibilidad con versiones más recientes de JVM. Esto es factible porque las interfaces de las primitivas de concurrencia en la JVM son relativamente estables y permanecieron sin cambios durante años.</p><h2>
Probando la batalla</h2><p>Luego de construir Fray, el siguiente paso fue la evaluación. Afortunadamente, muchas aplicaciones, como Apache Lucene, ya incluyen pruebas de concurrencia. Estas pruebas de concurrencia son pruebas JUnit regulares que generan múltiples hilos, realizan algo de trabajo, luego (normalmente) esperan a que terminen esos hilos y luego afirman alguna propiedad. La mayoría de las veces, estas pruebas se aprueban porque solo hacen un entrecalado. Peor aún, algunas pruebas solo fallan ocasionalmente en el entorno CI/CD, como se describió antes, lo que hace que estos fallos sean extremadamente difíciles de depurar. Cuando ejecutamos las mismas pruebas con Fray, descubrimos numerosos errores. Cabe destacar que Fray redescubrió errores previamente reportados que no fueron corregidos debido a la falta de una reproducción fiable, incluyendo el enfoque de este blog: <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat.testGlobalVersions</code></a>. Por suerte, con Fray, podemos reproducirlos de forma determinista y proporcionar a los desarrolladores información detallada, permitiéndoles reproducir y solucionar el problema de forma fiable.</p><p></p><h2>Próximos pasos para Fray</h2><p>Nos entusiasma escuchar de los desarrolladores de Elastic que Fray fue útil para depurar errores de concurrencia. Seguiremos trabajando en Fray para que esté disponible para más desarrolladores.</p><p>Nuestros objetivos a corto plazo incluyen mejorar la capacidad de Fray para reproducir determinísticamente el calendario, incluso en presencia de otras operaciones no deterministas como un generador de valores aleatorios o el uso de <code>object.hashcode</code>. También pretendemos mejorar la usabilidad de Fray, permitiendo a los desarrolladores analizar y depurar pruebas de concurrencia existentes sin intervención manual. Lo más importante es que, si tienes dificultades para depurar o probar problemas de concurrencia en tu programa, nos encantaría saber de ti. Por favor, no dudes en crear un problema en el <a href="https://github.com/cmu-pasta/fray">repositorio Fray Github</a>.</p><p></p><h2>Hora de arreglar el bug de la concurrencia</h2><p>¡Gracias a Ao Li y al laboratorio de PATA, ahora tenemos un caso fiable que suspende esta prueba! Por fin podemos arreglar esto. El problema clave residía en cómo <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThreadPool.java"><code>DocumentsWriterPerThreadPool</code></a> permitía la reutilización de hilos y 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>Aquí podemos ver cada hilo creado, haciendo referencia a la cola inicial de eliminación en la generación 0.</p><p>Después, el avance de cola ocurrirá al encajar la cola y se verán correctamente las 7 acciones anteriores en la cola.</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>Pero, antes de que todos los hilos terminen de enjuagar, dos se reutilizan para un 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>Estos incrementarán el <code>seqNo</code> por encima del máximo asumido, que se calculó durante el flush como 7. Notar el <code>numDocsInRAM</code> adicional para los segmentos <code>_3</code> y <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>Lo que provoca que Lucene tenga en cuenta incorrectamente la secuencia de acciones del documento durante un vaciado y activa este fallo de prueba.</p><p>Como todas las buenas correcciones de errores, la corrección real tiene unas <a href="https://github.com/apache/lucene/pull/13627/files">10 líneas de código</a>. Pero dos ingenieros tardaron varios días en entenderlo realmente:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71d78f22ce6ec215/6a1706b61949f7bcd0e7a94a/7915e5ba2feba0fb8e846da0af11bfdca470c467-1600x622.jpg" alt="" /><h2>No todos los héroes llevan capa</h2><p>Sí, es un cliché, pero es verdad.</p><p></p><p>La depuración concurrente de programas es increíblemente importante. Estos complicados errores de concurrencia requieren una cantidad desproporcionada de tiempo para depurar y resolver. Aunque nuevos lenguajes como Rust tienen mecanismos incorporados para ayudar a prevenir condiciones raciales como esta, la mayoría del software del mundo ya está escrito, y escrito en algo <a href="https://www.rust-lang.org/">distinto a Rust</a>. Java, incluso luego de todos estos años, sigue siendo uno de los lenguajes más empleados. Mejorar la depuración en lenguajes basados en JVM mejora el mundo de la ingeniería de software. Y dado que algunos piensan que el código será escrito por grandes modelos de lenguaje, quizá nuestro trabajo como ingenieros acabe siendo simplemente depurar código malo de LLM en lugar de solo nuestro propio código malo. Pero, sea cual sea el futuro de la ingeniería de software, la depuración concurrente de programas seguirá siendo fundamental para el mantenimiento y desarrollo de software.</p><p></p><p>Gracias a Ao Li y a sus colegas del PASTA Lab por hacerlo aún mejor.</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 de bugs con Lucene: Arreglando una excepción de índice corrupto]]></title>
    <description><![CDATA[A veces, una sola línea de código tarda días en escribir. Aquí vemos un vistazo al dolor y la depuración de un ingeniero durante varios días para corregir una posible corrupción del índice Lucene de Apache.]]></description>
    <content:encoded><![CDATA[<h2>Prepárate: </h2><p>Este blog en individuo es diferente de lo habitual. No es una explicación de una nueva función ni un tutorial. Esto trata sobre una sola línea de código que tardó tres días en escribir. Estaremos corrigiendo una posible corrupción en el índice Apache Lucene. Algunas conclusiones que espero que tengáis:</p><ul><li><p>Todas las pruebas poco estables son repetibles, si se tiene tiempo suficiente y las herramientas adecuadas</p></li><li><p>Muchas capas de pruebas son clave para sistemas robustos. Sin embargo, niveles más altos de pruebas se vuelven cada vez más difíciles de depurar y reproducir.</p></li><li><p>Sleep es un excelente depurador</p></li></ul><h2>Cómo prueba Elasticsearch</h2><p>En Elastic, tenemos una gran cantidad de pruebas que se ejecutan contra la base de código de Elasticsearch. Algunas son pruebas funcionales simples y enfocadas, otras son pruebas de integración de "happy path" de un solo nodo, y otras intentan romper el clúster para cerciorar de que todo funcione correctamente en un caso de fallo. Cuando una prueba falla continuamente, un ingeniero o automatización de herramientas crea un problema en github y lo señala para que un equipo en individuo lo investigue. Este <a href="https://github.com/elastic/elasticsearch/issues/105122">error en individuo</a> fue descubierto mediante una prueba del último tipo. Estas pruebas son complicadas, a veces solo repetibles tras muchas pruebas.</p><h2>¿Qué es realmente esta prueba?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt58aab468587a7d35/6a17dd8e3e03d74b8d4f2b5f/63268f3b5714ebf9df8070ec2e3c4f0486ed822e-1600x1088.jpg" alt="Problema en GitHub: https://github.com/elastic/elasticsearch/issues/105122" /><p>Esta prueba en individua es interesante. Creará un mapeo particular y lo aplicará a un fragmento primario. Luego, al intentar crear una réplica. La diferencia clave es que cuando la réplica intenta analizar el documento, la prueba inyecta una excepción, lo que provoca que la recuperación falle de una manera sorprendente (pero esperada).</p><p></p><p>Sin embargo, todo funcionaba como se esperaba, con un inconveniente importante. Durante la limpieza de la prueba, validamos la consistencia, y ahí esta prueba se topó con un problema.</p><p>
Esta prueba estaba fallando de la manera esperada. Durante la comprobación de consistencia verificábamos que todos los archivos replicados y los principales de segmentos de Lucene fueran consistentes. Es decir, no corrompido y completamente replicado. Tener datos parciales o corruptos es mucho peor que que algo falle por completo. Aquí está la pista aterradora y abreviada de la pila del fallo.</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 alguna manera, durante el fallo de replicación forzada, ¡el fragmento replicado acabó corrompido! Permítanme explicar la parte clave del error en un lenguaje sencillo.</p><p></p><p>Lucene es una arquitectura basada en segmentos, lo que significa que cada segmento conoce y gestiona sus propios archivos de solo lectura. Este segmento en individuo estaba siendo validado a través de <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/index/SegmentCoreReaders.java">sus SegmentCoreReaders</a> para cerciorar que todo estuviera en orden. Cada lector central almacena metadatos que indican qué tipos de campos y archivos existen para un segmento determinado. Sin embargo, al validar el <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90PointsFormat.java">Lucene90PointsFormat</a>, faltaban ciertos archivos esperados. Con los segmentos <code>_0.cfs</code> archivo esperábamos un archivo de formato puntual llamado <code>kdi</code>. <code>cfs</code> significa "sistema de archivos compuesto" en el que Lucene a veces combina todos los tipos de campos y todos los archivos diminutos en un único archivo más grande para una replicación y uso de recursos más eficiente. De hecho, faltaban las tres extensiones de archivo de puntos: <code>kdd</code>, <code>kdi</code>y <code>kdm</code> . ¿Cómo podríamos llegar al punto en que un segmento de Lucene espera encontrar un archivo puntual pero falta?! ¡Parece un error de corrupción aterrador!</p><p></p><h2>El primer paso para cada corrección de error es replicarlo</h2><p></p><p>Replicar el fallo de este error en individuo fue extremadamente doloroso. Aunque aprovechamos las <a href="https://en.wikipedia.org/wiki/Random_testing">pruebas de valor aleatorizadas</a> en Elasticsearch, nos cercioramos de proporcionar a cada fallo una semilla aleatoria (esperemos) reproducible para que todos puedan ser investigados. Bueno, esto funciona muy bien para todos los fallos excepto los causados por <a href="https://en.wikipedia.org/wiki/Race_condition">una condición de carrera</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>Por mucho que lo intentara, la semilla en individua nunca repetía el fallo localmente. Pero hay formas de poner a prueba las pruebas y avanzar hacia un fracaso más repetible.</p><p></p><p>Nuestro conjunto de pruebas en individua permite que una prueba se ejecute más de una vez en el mismo comando mediante el parámetro <code>-Dtests.iters</code> . Pero esto no era suficiente, necesitaba cerciorarme de que los hilos de ejecución estuvieran cambiando y así aumentaran la probabilidad de que ocurriera esta condición de carrera. Otro problema era que la prueba tardaba tanto en ejecutar que el corredor de pruebas se apagaba. Al final, usé el siguiente pesadilla para ejecutar la prueba de forma repetida:</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>Entra <a href="https://github.com/ColinIanKing/stress-ng">el estrés</a>. Esto te permite iniciar rápidamente un proceso que solo consumirá núcleos de CPU durante la comida. Spamear stress-ng aleatoriamente mientras ejecutaba varias iteraciones de la prueba fallida finalmente me permitió replicar el fallo. Un paso más. Para estresar el sistema, simplemente abre otra ventana de terminal y ejecuta:</p>stress-ng --cpu 16<h2>
Revelando el error</h2><p>

Ahora que el fallo de la prueba que revela el error es mayormente repetible, es hora de intentar encontrar la causa. Lo que hace extraño este test en individuo es que Lucene lanza porque espera valores de puntos, pero no se agregan directamente por la prueba. Solo valores de texto. Esto me llevó a considerar los cambios recientes en nuestros <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html">optimistas campos de control de concurrencia</a> : <code>_seq_no</code> y <code>_primary_term</code>. Ambos están indexados como puntos y existen en todos los documentos de Elasticsearch.</p><p></p><p>¡De hecho, un <a href="https://github.com/elastic/elasticsearch/pull/105036">commit</a> cambió nuestro <code>_seq_no</code> mapeador! ¡SÍ! ¡Esta tiene que ser la causa! Pero mi entusiasmo duró poco. Esto solo cambió el orden en que se agregaron los campos al documento. Antes de este cambio, <code>_seq_no</code> campos se agregaron al último en el documento. Después, ellos fueron agregados primero. No hay manera de que el orden de agregar campos a un documento Lucene causara este fallo...</p><p></p><p>Sí, cambiar el orden en que se agregaron los campos causó el fallo. ¡Esto fue sorprendente y resultó ser un error en Lucene mismo! Cambiar el orden de los campos analizados no debería modificar el comportamiento de analizar un documento.</p><p></p><h2>El bicho en Lucene</h2><p>De hecho, el insecto en Lucene se centró en las siguientes condiciones:</p><ul><li><p>Indexación de un campo de valor de puntos (por ejemplo, <code>_seq_no</code>)</p></li><li><p>Intentando indexar un lanzamiento de campo de texto durante el análisis</p></li><li><p>En este estado extraño, abrimos un <a href="https://blog.mikemccandless.com/2011/06/lucenes-near-real-time-search-is-fast.html">Lector en Tiempo Casi Real</a> del autor que experimenta la excepción de análisis de índice de texto</p></li></ul><p>Pero por muchas formas que lo intentara, no pude replicarlo completamente. Agregué directamente puntos de pausa para depurar en toda la base de código de Lucene. Intenté abrir lectores al azar durante el camino de excepción. Incluso imprimí megabytes y megabytes de registros intentando encontrar la ruta exacta por donde ocurrió este fallo. Simplemente no pude hacerlo. Pasé todo un día luchando y perdiendo.</p><p></p><p>Luego me dormí.</p><p></p><p>Al día siguiente volví a leer el rastro original de la pila y descubrí la siguiente línea:</p><p>
</p>    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)<p>En todos mis intentos de recreación, nunca establecí específicamente la política de fusión de retención. La <a href="https://github.com/apache/lucene/blob/5f0fa2b291ff9e7d878642f025a70c15b788a470/lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java">Política de Fusión SoftDeletesRetentionMergePolicy</a> la emplea Elasticsearch para que podamos replicar con precisión las eliminaciones en réplicas y cerciorarnos de que todos nuestros controles de concurrencia se encarguen de cuándo se eliminan realmente los documentos. Por lo demás, Lucene tiene el control total y los eliminará en cualquier fusión.</p><p></p><p>Una vez que agregué esta política y replicé los pasos más básicos mencionados anteriormente, el fallo se replicó inmediatamente.</p><p>
Nunca estuve más feliz de abrir un <a href="https://github.com/apache/lucene/issues/13353">micrófono en Lucene</a>.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89d37c3454ec298b/6a17dd90ec0f8993c75a6511/c7d9ce3de5278923a8454097e2bdf487c861168a-1600x920.jpg" alt="Problema con Github https://github.com/apache/lucene/issues/13353" /><p>
Aunque se presentaba como una condición de raza en Elasticsearch, era sencillo escribir una prueba repetidamente suspendida en Lucene una vez cumplidas todas las condiciones.</p><p></p><p>Al final, como todos los buenos errores, se solucionó con solo una línea de código. Varios días de trabajo, solo por una línea de código.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdab62db3402b27f7/6a17dd92dbb4ff23e7fb55ad/2c10a02eb41da181b1dceee47186c076c9b14a90-1600x539.jpg" alt="Corrección de una línea de código" /><p>Pero valió la pena.</p><h2>
No es el final</h2><p>¡Espero que disfrutaste de esta aventura salvaje conmigo! Escribir software, especialmente software tan empleado y complejo como Elasticsearch y Apache Lucene, es gratificante. Sin embargo, a veces resulta excepcionalmente frustrante. Me encanta y odio el software a la vez. ¡La corrección de errores 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[Comprendiendo la cuantización escalar en Lucene]]></title>
    <description><![CDATA[Explora cómo Elastic introdujo la cuantización escalar en Lucene, incluyendo cuantización automática de bytes, cuantización por segmento y análisis de rendimiento.]]></description>
    <content:encoded><![CDATA[<h2>Cuantización automática de bytes en Lucene</h2><p>Aunque HNSW es una forma poderosa y flexible de almacenar y buscar vectores, requiere una cantidad significativa de memoria para funcionar rápidamente. Por ejemplo, consultar vectores float32 de 1 MM de 768 dimensiones requiere aproximadamente  de RAM. Una vez que empiezas a buscar un número significativo de vectores, esto se vuelve caro. Una forma de usar alrededor  menos de memoria es mediante cuantización por bytes. Lucene y, en consecuencia, Elasticsearch soportaron la indexación de  durante algún tiempo, pero la construcción de estos vectores fue responsabilidad del usuario. Esto está a punto de cambiar, ya que introdujimos la cuantización  en Lucene.</p><h2>Cuantización escalar 101</h2><p>Todas las técnicas de cuantización se consideran transformaciones con pérdida de los datos en bruto. Es decir, se pierde algo de información por el bien del espacio. Para una explicación detallada de la cuantización escalar, ver: <a href="https://www.elastic.co/search-labs/scalar-quantization-101">Cuantización Escalar 101</a>. A un nivel general, la cuantización escalar es una técnica de compresión con pérdida. Unas matemáticas sencillas ofrecen un ahorro significativo de espacio con muy poco impacto en la memoria.</p><h2>Explorando la arquitectura</h2><p>Quienes están acostumbrados a trabajar con Elasticsearch quizá ya estén familiarizados con estos conceptos, pero aquí tienes un resumen rápido de la distribución de los documentos para la búsqueda.</p><p>Cada índice de Elasticsearch está compuesto por <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#size-your-shards">múltiples fragmentos</a>. Aunque cada fragmento solo puede asignar a un solo nodo, múltiples fragmentos por índice te dan paralelismo de cálculo entre nodos.</p><p>Cada fragmento está compuesto como un <a href="https://lucene.apache.org/core/9_8_0/core/org/apache/lucene/index/package-summary.html">único Índice Luceno</a>. Un índice Lucene consta de múltiples segmentos de solo lectura. Durante la indexación, los documentos se almacenan en búfer y periódicamente se vacian en un segmento de solo lectura. Cuando se cumplen ciertas condiciones, estos segmentos pueden fusionar en el fondo en un segmento más grande. Todo esto es configurable y tiene su propio conjunto de complejidades. Pero, cuando hablamos de segmentos y fusiones, nos referimos a segmentos Lucene de solo lectura y a la fusión periódica automática de estos segmentos. <a href="https://www.elastic.co/search-labs/vector-search-elasticsearch-rationale">Aquí tienes una profundización</a> en la fusión de segmentos y las decisiones de diseño.</p><h2>Cuantización por segmento en Luceno</h2><p>Cada segmento en Lucene almacena lo siguiente: los vectores individuales, los índices de grafos HNSW, los vectores cuantizados y los cuantiles calculados. Por brevedad, nos centraremos en cómo Lucene almacena vectores cuantizados y en bruto. Para cada segmento, llevamos un seguimiento de los vectores en bruto en el archivo  , los vectores cuantizados y un único flotador de multiplicador correctivo en , así como los metadatos alrededor de la cuantización dentro del archivo  .</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1861d0af970fd096/6a17d9887f6f15a45dc099c0/507a4d578f8a5d2225b11d16ac1fc0b7dd39eaa1-1207x228.png" alt="El .vec Archivo" /><p>Figura 1: Diseño simplificado de un archivo de almacenamiento vectorial en bruto. Ocupa  de espacio en disco ya que los  son 4 bytes. Como estamos cuantizando, estos no se cargarán durante la búsqueda HNSW. Solo se emplean si se aplicar específicamente (por ejemplo, secundario de fuerza bruta mediante <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#query-rescorer">repuntuación</a>), o para la recuantización durante la fusión de segmentos.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8c52b51939357280/6a17d9897b54f9130f8b3761/ffc6e8bffefc5cd36f14aae315184c89e04a6587-1440x207.png" alt="El archivo .veq" /><p>Figura 2: Diseño simplificado del  archivo. Ocupa  del espacio y se cargará en memoria durante la búsqueda. El  bytes es para tener en cuenta el multiplicador correctivo flotante, empleado para ajustar el puntaje y mejorar la precisión y la recuperación.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt28e007988f81e5c0/6a17d98b25daabe23208a0d9/555cc92d7d179d8c9711cefbaaac3e0b815d2e42-1440x182.png" alt="El archivo .vemq" /><p>Figura 3: El diseño simplificado del archivo de metadatos. Aquí es donde hacemos seguimiento de la cuantización y la configuración vectorial junto con los cuantiles calculados para este segmento.</p><p>Así que, para cada segmento, almacenamos no solo los vectores cuantizados, sino también los cuantiles usados para fabricar estos vectores cuantizados y los vectores originales en bruto. Pero, ¿por qué mantenemos los vectores en bruto?</p><h2>Cuantización que crece contigo</h2><p>Como Lucene vacía periódicamente para leer solo segmentos, cada segmento solo tiene una vista parcial de todos tus datos. Esto significa que los cuantiles calculados solo se aplican directamente a ese conjunto muestral de todos tus datos. Ahora bien, esto no es un gran problema si tu muestra representa adecuadamente todo tu corpus. Pero Lucene te permite ordenar tu índice de varias maneras. Así que podrías indexar datos ordenados de una manera que agregue sesgo para cálculos de cuantil por segmento. ¡Además, puedes vaciar los datos cuando quieras! Tu conjunto de muestras podría ser muy pequeño, incluso solo un vector. Otro inconveniente es que tienes control sobre cuándo ocurren las fusiones. Aunque Elasticsearch tiene configuraciones por defecto y fusiones periódicas, puedes pedir una fusión cuando quieras a través de <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.10/indices-forcemerge.html">_force_merge</a> API. Entonces, ¿cómo permitimos toda esta flexibilidad, proporcionando una buena cuantización que proporcione una buena recordación?</p><p>La cuantización vectorial de Lucene se ajustará automáticamente con el tiempo. Como Lucene está diseñado con una arquitectura de segmentos de solo lectura, tenemos garantías de que los datos de cada segmento no cambiaron y demarcaciones claras en el código para cuándo se pueden actualizar cosas. Esto significa que durante la fusión de segmentos podemos ajustar los cuantiles según sea necesario y posiblemente volver a cuantizar los vectores.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d9a2390958c3e82/6a17d98cbe6086e4fd0045d5/646baa8279719d06c05c1a4fdb80a0c060e0dd94-1440x446.png" alt="Cuantillos de múltiples segmentos" /><p>Figura 4: Tres segmentos de ejemplo con diferentes cuantiles.</p><p>¿Pero no es caro la recuantización? Tiene cierta sobrecarga, pero Lucene maneja los cuantiles con inteligencia y solo recuantiza completamente cuando es necesario. Usemos los segmentos de la Figura 4 como ejemplo. Demos a los  y   documentos cada uno y al  solo  documentos. Lucene tomará un promedio ponderado de los cuantiles y si ese cuantil combinado resultante está lo suficientemente cerca de los cuantiles originales de los segmentos, no tenemos que volver a cuantificar ese segmento y emplearemos los cuantiles recién fusionados.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte2596c10ee616cf3/6a17d98eec0f8903b85a6497/58fa20d60dc337ca7108eb01e84a07c330e87ee6-1440x447.png" alt="Cuantiles fusionados" /><p>Figura 5: Ejemplo de cuantiles fusionados donde los segmentos  y  tienen  documentos y  solo .</p><p>En la situación visualizada en la figura 5, podemos ver que los cuantiles fusionados resultantes son muy similares a los cuantiles originales en  y . Por tanto, no justifican cuantizar los vectores. El segmento  parece desviar demasiado. En consecuencia, los vectores en  se recuantizarían con los nuevos valores cuantiles fusionados.</p><p>De hecho, existen casos extremos en los que los cuantiles fusionados difieren significativamente de cualquiera de los cuantiles originales. En este caso, tomaremos una muestra de cada segmento y recalcularemos completamente los cuantiles.</p><h2>Rendimiento y números de cuantización</h2><p>Entonces, ¿es rápido y sigue proporcionando buena recuperación? Los siguientes números se recopilaron ejecutando el experimento en una instancia <code>c3-standard-8</code> GCP. Para cerciorar una comparación justa con  , usamos una instancia lo suficientemente grande como para almacenar vectores en bruto en memoria. Indexamos  vectores <a href="https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings">de Cohere Wiki</a> usando el producto interno máximo.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfa48e3724ad97fed/6a17d98f445de96aca4cff88/81958f99b8a1397f669db48de00f264cacc6b49f-576x455.png" alt="Recuperación de cuantización" /><p>Figura 6: Recall@10 para vectores cuantizados frente a vectores en bruto. El rendimiento de búsqueda de vectores cuantizados es significativamente más rápido que el en bruto, y la recuperación se recupera rápidamente reuniendo solo 5 vectores más; visible .</p><p>La Figura 6 muestra la historia. Aunque hay una diferencia en la retirada, como era de esperar, no es significativa. Y la diferencia de recordación desaparece reuniendo solo 5 vectores más. Todo esto con fusiones de segmentos  más rápidas y 1/4 de la memoria de vectores  .</p><h2>Conclusión</h2><p>Lucene ofrece una solución única a un problema difícil. No se requiere ningún paso de "entrenamiento" ni de "optimización" para la cuantización. En Lucene, simplemente funcionará. No hay que preocupar por tener que "reentrenar" tu índice vectorial si tus datos se desplazan. Lucene detectará cambios significativos y se encargará de esto automáticamente durante la vida útil de tus datos. ¡Espero con ganas que lleguemos a incorporar esta capacidad a 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[Investigación en ML]]></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>