<?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[Lucene - 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[Lucene - 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/blog/category/lucene</link>
    </image>
    <link>https://www.elastic.co/es/search-labs/blog/category/lucene</link>
    <atom:link href="https://www.elastic.co/es/search-labs/rss/category/lucene.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[es]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 15:08:06 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Filtrado de búsqueda vectorial: Mantenerlo relevante]]></title>
    <description><![CDATA[Realizar una búsqueda vectorial para encontrar los resultados más similares a una consulta no es suficiente. A menudo se necesita filtrar para reducir los resultados de búsqueda. Este artículo explica cómo funciona el filtrado para la búsqueda vectorial en Elasticsearch y Apache Lucene.]]></description>
    <content:encoded><![CDATA[<p>La búsqueda vectorial no es suficiente para encontrar resultados relevantes. Es muy común usar criterios de filtrado que ayudan a reducir los resultados de búsqueda y a filtrar los resultados irrelevantes.</p><p>Entender cómo funciona el filtrado en la búsqueda vectorial te ayudará a equilibrar los compromisos entre rendimiento y recordación, así como descubrir algunas de las optimizaciones que se usan para que la búsqueda vectorial sea eficiente al usar filtrado.</p><h2>¿Por qué filtrar?</h2><p>La búsqueda vectorial revolucionó la forma en que encontramos información relevante en grandes conjuntos de datos, permitiéndonos descubrir elementos que son semánticamente similares a una consulta.</p><p>Sin embargo, simplemente encontrar objetos similares no es suficiente. A menudo necesitamos reducir los resultados de búsqueda en función de criterios o atributos específicos.</p><p>Imagina que buscas un producto en una tienda online. Una búsqueda vectorial pura puede mostrarte artículos visualmente similares, pero también podrías filtrar por rango de precio, marca, disponibilidad o valoraciones de clientes. Sin filtrar, te presentarías con una gran variedad de productos similares, lo que dificultaría encontrar exactamente lo que buscas.</p><p>El filtrado permite un control preciso sobre los resultados de búsqueda, cerciorando que los elementos recuperados no solo se alineen semánticamente, sino que también cumplan todos los requisitos necesarios. Esto conduce a una experiencia de búsqueda mucho más precisa, eficiente y fácil de usar.</p><p>Aquí es donde Elasticsearch y Apache Lucene excelen: usar filtrado efectivo entre varios tipos de datos es una de las diferencias clave con otras bases de datos vectoriales.</p><h2>Filtrado para búsqueda vectorial exacta</h2><p>Existen dos formas principales de realizar búsquedas vectoriales exactas:</p><ul><li><p>Usar un tipo de índice <code>flat</code> para tu campo de dense_vector. Esto hace que <code>knn</code> búsquedas empleen la búsqueda exacta en lugar de aproximada.</p></li><li><p>Emplear una <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-script-score-query#vector-functions">consulta script_score</a> que emplea funciones vectoriales para calcular el puntaje. Esto puede usar con cualquier tipo de índice.</p></li></ul><p>Al ejecutar una búsqueda vectorial exacta, todos los vectores se comparan con la consulta. En este escenario, el filtrado ayudará al rendimiento, ya que solo se necesitan comparar los vectores que pasan el filtro.</p><p>Esto no afecta a la calidad del resultado, ya que todos los vectores se consideran de todos modos. Simplemente filtramos de antemano los resultados que no son interesantes, para poder reducir el número de operaciones.</p><p>Esto es muy importante, ya que puede ser más eficiente ejecutar una búsqueda exacta en lugar de una búsqueda aproximada cuando los filtros aplicados resultan en un pequeño número de documentos.</p><p>La regla general es usar la búsqueda exacta cuando menos de 10.000 documentos pasan el filtro. Los índices <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a> son mucho más rápidos para comparar, así que tiene sentido usar la búsqueda exacta cuando hay menos de 100k para los índices basados. Consulta <a href="https://www.elastic.co/search-labs/blog/knn-exact-vs-approximate-search">esta entrada del blog</a> para más detalles.</p><p>Si tus filtros siempre son muy restrictivos, puedes considerar indexar centrado en la búsqueda exacta en lugar de en la búsqueda aproximada, usando un tipo de índice <code>flat</code> en lugar de uno basado en HNSW. Para más detalles, <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-params">ver las propiedades de index_options</a>.</p><h2>Filtrado para búsqueda vectorial aproximada</h2><p>Al ejecutar búsqueda vectorial aproximada, cambiamos la precisión de los resultados por el rendimiento. Las estructuras de datos de búsqueda vectorial como HNSW buscan eficientemente vecinos aproximados en millones de vectores. Se centran en recuperar los vectores más similares haciendo la menor cantidad posible de comparaciones vectoriales, que son costosas de calcular.</p><p>Esto significa que otros atributos de filtrado no forman parte de los datos vectoriales. Diferentes tipos de datos tienen sus propias estructuras de indexación que son eficientes para encontrarlos y filtrarlos, como diccionarios de términos, listas de publicación y valores de documentos.</p><p>Dado que estas estructuras de datos son independientes del mecanismo de búsqueda vectorial, ¿cómo aplicamos el filtrado a la búsqueda vectorial? Hay dos opciones: aplicar filtros luego de la búsqueda vectorial (postfiltrado) o antes de la búsqueda vectorial (prefiltrado).</p><p>Cada una de esas opciones tiene sus pros y sus contras. ¡Vamos a profundizar en ellos!</p><h3>Postfiltrado</h3><p>El postfiltrado aplica filtros después de que se realizó la búsqueda vectorial. Esto significa que los filtros se aplican después de que se encontraron los k primeros resultados vectoriales más similares.</p><p>Obviamente, podemos obtener menos de k resultados aplicando los filtros a los resultados. Por supuesto, podríamos obtener más resultados de la búsqueda vectorial (valores k más altos), pero no estaremos seguros de obtener k o más tras aplicar los filtros.</p><p>El beneficio del postfiltrado es que no cambia el comportamiento en tiempo de ejecución de la búsqueda vectorial: la búsqueda vectorial no es consciente del filtrado. Pero sí cambia el número final de resultados obtenidos.</p><p>A continuación se muestra un ejemplo de postfiltrado usando la <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query">consulta knn</a>. Comprueba que la cláusula de filtrado esté separada de la consulta knn:</p>{
  "query": {
    "bool": {
      "must": {
        "knn": {
          "field": "image-vector",
          "query_vector": [54, 10, -2],
          "k": 5,
          "num_candidates": 50
        }
      },
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}<p>El filtrado de postfiltrado también está disponible para la búsqueda de knn usando <a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/filter-search-results#post-filter">el filtro de postfiltro</a>:</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, 2],
    "k": 5,
    "num_candidates": 50
  },
  "post_filter": {
    "term": {
      "file-type": "png"
    }
  }
}<p>Ten en cuenta que necesitas usar una sección explícita de filtro posterior con la búsqueda de knn. Si no usas un filtro de post, la búsqueda de knn <a href="https://www.elastic.co/docs/solutions/search/vector/knn#_combine_approximate_knn_with_other_features">combinará los resultados de vecinos más cercanos</a> con otras consultas o filtros en lugar de hacer un filtro de post.</p><h3>Prefiltrado</h3><p>Aplicar filtros antes de la búsqueda vectorial primero recuperará los documentos que cumplan con los filtros y luego transmitirá esa información a la búsqueda vectorial.</p><p>Lucene emplea <a href="https://github.com/apache/lucene/blob/7a60d7ce92392181e137361336e5196bd486cdd9/lucene/core/src/java/org/apache/lucene/util/BitSet.java">BitSets</a> para almacenar eficientemente los documentos que cumplen la condición de filtro. La búsqueda vectorial recorre entonces el grafo HNSW, teniendo en cuenta los documentos que cumplen la condición. Antes de agregar un candidato a los resultados, comprueba que esté contenido en el BitSet de documentos válidos.</p><p>Sin embargo, el candidato debe ser explorado y comparado con la consulta, aunque no sea un documento válido. La efectividad de HNSW depende de la conexión entre los vectores del grafo: si dejáramos de explorar un candidato, significaría que podríamos estar saltándonos también sus vecinos.</p><p>Piénsalo como manejar para llegar a una gasolinera. Si descartas cualquier carretera que no tenga gasolinera, es poco probable que llegues a tu destino. Puede que otras carreteras no sean lo que necesitas, pero te <em>conectan</em> con tu destino. ¡Lo mismo ocurre con los vectores en un grafo HNSW!</p><p>Por tanto, aplicar prefiltrado es menos eficiente que no aplicar filtros. Tenemos que trabajar en <em>todos</em> los vectores que visitamos en nuestra búsqueda, y desechar aquellos que no coinciden con el filtro. Estamos trabajando más y tardando más en conseguir los mejores resultados de la k.</p><p>A continuación se muestra un ejemplo de pretfiltering en la DSL de Elasticsearch Consult. Comprueba que la cláusula de filtrado ahora forma parte de la sección knn:</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  }
}<p>El prefiltrado está disponible tanto para <a href="https://www.elastic.co/docs/solutions/search/vector/knn#knn-search-filter-example">la búsqueda como</a> para <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-filtering">la consulta knn</a>:</p>{
  "query": {
    "knn": {
      "field": "image-vector",
      "query_vector": [-5, 9, -12],
      "k": 5,
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}<h4>Optimizaciones de prefiltrado</h4><p>Hay un par de optimizaciones que podemos aplicar para cerciorar que el prefiltrado sea eficiente.</p><p>Podemos cambiar a búsqueda exacta si el filtro es muy restrictivo. Cuando hay pocos vectores para comparar, es más rápido realizar una búsqueda exacta en los pocos documentos que cumplen con el filtro.</p><p>Esta es una optimización que se aplica automáticamente en <a href="https://github.com/apache/lucene/blob/eb876b618da5d04c1ad14b04a48321638318493a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L218">Lucene</a> y Elasticsearch.</p><p>Otro método de optimización implica ignorar los vectores que no satisfacen el filtro. En su lugar, este método comprueba los vecinos de los vectores filtrados que sí pasan el filtro. Este enfoque reduce efectivamente el número de comparaciones ya que no se consideran los vectores filtrados, y continúa explorando vectores conectados al camino actual.</p><p>Este algoritmo es ACORN-1, y el proceso se describe en detalle en <a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">esta entrada del blog</a>.</p><h2>Filtrado usando la seguridad a nivel de documento</h2><p><a href="https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/controlling-access-at-document-field-level#document-level-security">La Seguridad a Nivel de Documento (DLS)</a> es una función de Elasticsearch que especifica los documentos que los roles de usuario pueden recuperar.</p><p>DLS se realiza mediante consultas. Un rol puede tener una consulta asociada a índices, lo que limita efectivamente los documentos que un usuario que pertenece a ese rol puede recuperar de los índices.</p><p>La consulta de rol se emplea como filtro para <a href="https://github.com/elastic/elasticsearch/blob/c3a1cb34294e902a9f46d7e840ea09965019f456/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapper.java#L92">recuperar los documentos que coinciden con ella</a>, y se almacenan en caché como un BitSet. Este BitSet se emplea entonces para envolver el lector Lucene subyacente, de modo que solo los documentos que se devolvieron de la consulta se consideran <em>activos,</em>es decir, existen en el índice y no fueron eliminados.</p><p>A medida que los documentos en tiempo real se <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L196">recuperan del lector</a> para realizar la consulta knn, solo se considerarán los documentos disponibles para el usuario. Si hay un prefiltro, se <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L204">agregarán los documentos DLS a él</a>.</p><p>Esto significa que el filtrado DLS funciona como prefiltro para la búsqueda vectorial aproximada, con las mismas participaciones de rendimiento y optimizaciones.</p><p>DLS con búsqueda exacta tendrá los mismos beneficios que aplicar cualquier filtro: cuantos menos documentos se recuperen de DLS, más eficiente será una búsqueda exacta. Considera también el número de documentos devueltos por DLS; si los roles DLS son muy restrictivos, puedes considerar usar búsqueda exacta en lugar de búsqueda aproximada.</p><h2>Evaluación comparativa</h2><p>En Elasticsearch, queremos cerciorarnos de que el filtrado de búsqueda vectorial sea eficiente. Disponemos <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/so_vector/nightly/default/90d">de un benchmark específico para el filtrado vectorial</a> que realiza búsquedas vectoriales aproximadas con diferentes filtros para cerciorar que la búsqueda vectorial siga recuperando resultados relevantes lo más rápido posible.</p><p>Consulta las <a href="https://elasticsearch-benchmark-analytics.elastic.co/app/dashboards#/view/43b63e80-5ba2-11ed-aede-a742809feed4?_g=(refreshInterval:(pause:!t,value:60000),time:(from:'2025-05-28T01:27:58.456Z',to:'2025-06-30T13:53:26.430Z'))&amp;_a=()">mejoras</a> cuando se introdujo ACORN-1. Para pruebas en las que solo el 2% de los vectores pasan el filtro, la latencia de consulta se reduce al 55% de la duración original:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt820cb0b715cf291c/6a17e227dbb4ff8d49fb5615/3eac3748a33376fc97d957364a5c1f5108d5c58b-1023x896.png" alt="" /><h2>Conclusión</h2><p>El filtrado es una parte integral de la búsqueda. Cerciorar que el filtrado sea eficiente en la búsqueda vectorial y comprender los compromisos y optimizaciones es lo que hace que una búsqueda sea eficiente y precisa o fracase.</p><p>El filtrado afecta al rendimiento de la búsqueda vectorial:</p><ul><li><p>La búsqueda exacta es más rápida cuando se usa filtrado. Deberías considerar usar la búsqueda exacta en lugar de la aproximada si tu filtrado es lo suficientemente restrictivo. Esta es una optimización automática en Elasticsearch.</p></li><li><p>La búsqueda aproximada es más lenta cuando se emplea prefiltrado. El prefiltrado nos permite obtener los k primeros resultados que coinciden con el filtro, a costa de una búsqueda más lenta.</p></li><li><p>El postfiltrado no necesariamente recupera los k primeros resultados, ya que pueden filtrar mediante el filtro cuando se aplica.</p></li></ul><p>¡Feliz filtrado!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-search-filtering</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-search-filtering</guid>
    <category><![CDATA[Base de datos vectorial]]></category>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[Dentro de Elastic]]></category>
    <dc:creator><![CDATA[Carlos Delgado]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt39ede1736e0f1456/6a17e2282f4a5c5031fa8843/03b1dd4c7bda4fbabd8e374bc2e4f12d5be6ef5f-1600x1150.png" length="0" type="image/png"/>
    <pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Acelerar la fusión de gráficos HNSW]]></title>
    <description><![CDATA[Explore el trabajo que estuvimos haciendo para reducir la sobrecarga de crear varios gráficos HNSW, en individuo reducir el costo de fusionar gráficos.]]></description>
    <content:encoded><![CDATA[<p>En el pasado, <a href="https://www.elastic.co/es/search-labs/blog/multi-graph-vector-search">hablamos</a> de algunos de los retos de tener que buscar <a href="https://www.elastic.co/es/search-labs/blog/hnsw-graph">en varios gráficos HNSW</a> y cómo pudimos mitigarlos. En ese momento insinuamos algunas mejoras adicionales que planeamos. Esta publicación es la culminación de ese trabajo.</p><p>Podrías preguntarte, ¿por qué usar varios gráficos? Este es un efecto secundario de una elección arquitectónica en Lucene: segmentos inmutables. Como ocurre con la mayoría de las opciones arquitectónicas, hay pros y contras. Por ejemplo, recientemente lanzamos Elasticsearch sin servidor con GA. En este contexto, obtuvimos beneficios muy significativos de los segmentos inmutables, incluida la replicación eficiente de índices y la capacidad de desacoplar el índice y el proceso de consulta y escalarlos automáticamente de forma independiente. Para la cuantificación vectorial, las fusiones de segmentos nos dan la oportunidad de actualizar los parámetros para adaptarlos a las características de los datos. En este sentido, creemos que hay otros beneficios que ofrece tener oportunidades para medir las características de los datos y revisar las opciones de indexación.</p><p>En esta publicación, discutiremos el trabajo que estuvimos haciendo para reducir significativamente la sobrecarga de crear múltiples gráficos HNSW y, en individuo, para reducir el costo de fusionar gráficos.</p><h3>Fondo</h3><p>Para mantener un número manejable de segmentos, Lucene verifica periódicamente si debe fusionar segmentos. Esto equivale a comprobar si el recuento de segmentos actual supera un recuento de segmentos de destino, que está determinado por el tamaño del segmento base y la política de combinación. Si se supera el recuento, Lucene combina grupos de segmentos mientras se infringe la restricción. Este proceso se describió en detalle <a href="https://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html">en otra parte</a>.</p><p>Lucene elige fusionar segmentos de tamaño similar porque esto logra un crecimiento logarítmico en la amplificación de escritura. En el caso de un índice vectorial, la amplificación de escritura es el número de veces que se insertará un vector en un gráfico. Lucene intentará fusionar segmentos en grupos de aproximadamente 10. En consecuencia, los vectores se insertan en un gráfico aproximadamente veces, donde  es el recuento de vectores índice y  es el recuento de vectores de segmento base esperado. Debido al crecimiento logarítmico, la amplificación de escritura es de un solo dígito incluso para índices enormes. Sin embargo, el tiempo total dedicado a fusionar gráficos es linealmente proporcional a la amplificación de escritura.</p><p>Al fusionar gráficos HNSW ya hacemos una pequeña optimización: retener el gráfico para el segmento más grande e insertar vectores de los otros segmentos en él. Esta es la razón del factor 9/10 anterior. A continuación, mostramos cómo podemos hacerlo significativamente mejor empleando información de todos los gráficos que estamos fusionando.</p><h3>Fusión de grafos HNSW</h3><p>Antes conservábamos el grafo más grande e insertábamos vectores de los otros ignorando los gráficos que los contenían. La idea clave que aprovechamos a continuación es que cada grafo HNSW que descartamos contiene información importante de proximidad sobre los vectores que contiene. Nos gustaría usar esta información para acelerar la inserción, al menos algunos, de los vectores.</p><p>Nos enfocamos en el problema de insertar un gráfico más pequeño  en un gráfico más grande , ya que esta es una operación atómica que podemos usar para construir cualquier política de combinación.</p><p>La estrategia consiste en encontrar un subconjunto de vértices de  insertarlos en el gráfico grande. Luego usamos la conectividad de estos vértices en el gráfico pequeño para acelerar la inserción de los vértices restantes . A continuación, usamos  y  para denotar los vecinos de un vértice  en el grafo pequeño y grande, respectivamente. Esquemáticamente, el proceso es el siguiente.</p><p><code>MERGE-HNSW</code></p><p><code>Inputs </code><code> and </code></p><p><code>1</code><code>Find </code><code> to insert into </code><code> using COMPUTE-JOIN-SET</code>
<code>2</code><code>Insert each vertex </code><code> into </code>
<code>3</code><code>for </code><code> do</code>
<code>4</code>
<code>5</code>
<code>6</code><code>FAST-SEARCH-LAYER</code>
<code>7</code><code>SELECT-NEIGHBORS-HEURISTIC</code>
<code>8</code></p><p>Calculamos el conjunto  empleando un procedimiento que discutimos a continuación (línea 1). Luego insertamos cada vértice en  en el gráfico grande usando el procedimiento de inserción HNSW estándar (línea 2). Para cada vértice que no insertamos encontramos sus vecinos que insertamos y sus vecinos en el gráfico grande (líneas 4 y 5). Usamos un procedimiento <code>FAST-SEARCH-LAYER</code> sembrado con este conjunto (línea 6) para encontrar los candidatos para el <code>SELECT-NEIGHBORS-HEURISTIC</code> del <a href="https://arxiv.org/pdf/1603.09320">artículo</a> HNSW (línea 7). En efecto, estamos reemplazando <code>SEARCH-LAYER</code> para encontrar el conjunto candidato en el método <code>INSERT</code> (Algoritmo 1 del artículo), que de lo contrario no cambia. Finalmente, agregamos el vértice que acabamos de insertar en  (línea 8).</p><p>Está claro que para que esto funcione, cada vértice en  debe tener al menos un vecino en . De hecho, requerimos que para cada vértice en  que  para algunos , la máxima conectividad de capa. Observamos que en los gráficos reales de HNSW vemos una gran dispersión de grados de vértice. La siguiente figura muestra una función de densidad acumulativa típica del grado de vértice para la capa inferior de un gráfico HNSW de Lucene.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc815e1a9c3bb0d06/6a17e178ec0f89308a5a6564/44f001b3a1bc6627e172fed5c52a02cfdcb4cd66-1324x898.png" alt="Grafo HNSW: Ejemplo de distribución de grados de vértices" /><p>Exploramos el uso de un valor fijo para  así como convertirlo en una función del grado del vértice. Esta segunda opción conduce a mayores aceleramientos con un impacto mínimo en la calidad del gráfico, por lo que optó por lo siguiente</p><p>Tenga en cuenta que | es igual al grado del vértice  en el gráfico pequeño por definición. Tener un límite inferior de dos significa que insertaremos cada vértice cuyo grado sea menor que dos.</p><p>Un argumento de conteo simple sugiere que si elegimos  con cuidado, solo necesitamos insertar alrededor  en  directamente. Específicamente, coloreamos una arista del grafo si insertamos exactamente uno de sus vértices finales en . Entonces sabemos que para que cada vértice en  tenga al menos  vecinos en  , necesitamos colorear al menos  aristas. Además, esperamos que</p><p>Aquí,  es el grado medio del vértice en la gráfica pequeña. Para cada vértice  coloreamos como máximo  Bordes. Por lo tanto, el número total de bordes que esperamos colorear es como máximo . Esperamos que al elegir  con cuidado coloreemos cerca de este número de aristas y así cubrir todos los vértices  necesidades para satisfacer</p><p>Esto implica que .</p><p>Siempre que <code>SEARCH-LAYER</code> domine el tiempo de ejecución, esto sugiere que podríamos lograr un aceleramiento de hasta  en el tiempo de fusión. Dado el crecimiento logarítmico de la amplificación de escritura, esto significa que incluso para índices muy grandes, normalmente solo duplicaríamos el tiempo de construcción en comparación con la construcción de un gráfico.</p><p>El riesgo en esta estrategia es que dañamos la calidad del gráfico. Inicialmente lo intentamos con un <code>FAST-SEARCH-LAYER</code>sin operación. Descubrimos que esto degrada la calidad del gráfico en la medida en que la recuperación en función de la latencia se vio afectada, particularmente cuando se fusiona en un solo segmento. Luego exploramos varias alternativas empleando una búsqueda limitada del gráfico. Al final, la elección más efectiva fue la más simple. Usar <code>SEARCH-LAYER</code> pero con un <code>ef_construction</code>bajo. Con esta parametrización pudimos lograr gráficos de excelente calidad y aún así disminuir el tiempo de fusión en un poco más del 30% en promedio.</p><h3>Cálculo del conjunto de unión</h3><p>Encontrar un buen conjunto de unión puede formular como un problema de cobertura de grafos HNSW. Una heurística codiciosa es una heurística simple y efectiva para aproximar cubiertas óptimas de grafos. El enfoque que tomamos elige vértices uno a uno para sumar a  en orden decreciente de ganancia. La ganancia se define de la siguiente manera:</p><p>Aquí,  denota el recuento de vecinos de un vector  en  y  es la función indicadora. La ganancia incluye el cambio en el recuento del vértice que agregamos a , es decir,  ya que nos acercamos a nuestro objetivo al agregar un vértice menos cubierto. El cálculo de la ganancia se ilustra en la siguiente figura para el vértice naranja central.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7a353d56fa09af5f/6a17e17a2f4a5c33f7fa8825/fe11cd55a7d94d9e0b0f4d47ea309a99075c355d-540x474.png" alt="Ganancia de vértices para agregar al conjunto de unión J en el grafo HNSW" /><p>Mantenemos el siguiente estado para cada vértice :</p><ol><li><p>Ya sea rancio,</p></li><li><p>Su ganancia </p></li><li><p>El recuento de vértices adyacentes en  denotado </p></li><li><p>Un número aleatorio en el rango [0,1] que se usa para el desempate.</p></li></ol><p>El pseudocódigo para calcular el conjunto de combinaciones es el siguiente.</p><p><code>COMPUTE-JOIN-SET</code></p><p><code>Inputs </code></p><p><code>1</code>
<code>2</code>
<code>3</code><code>for </code><code> do</code>
<code>4</code>
<code>5</code>
<code>6</code>
<code>7</code><code>while </code><code> do
8</code><code> maximum gain vertex in </code>
<code>9</code><code>Remove the state for </code><code> from </code>
<code>10</code><code>if </code><code> is not stale then</code>
<code>11</code>
<code>12</code>
<code>13</code><code>for </code><code> do</code>
<code>14</code><code>mark </code><code> as stale if </code>
<code>15</code><code>mark neighbors of </code><code> stale if </code><code>
16</code><code>
17</code><code>else
18</code><code>
19</code><code>if </code><code> then
20</code><code>
21</code><code>return </code></p><p>Primero inicializamos el estado en las líneas 1-5.</p><p>En cada iteración del bucle principal extraemos inicialmente el vértice de ganancia máxima (línea 8), rompiendo los empates al azar. Antes de realizar cualquier cambio, debemos verificar si la ganancia del vértice está obsoleta. En individuo, cada vez que agregamos un vértice a  afectamos la ganancia de otros vértices:</p><ol><li><p>Dado que todos sus vecinos tienen un vecino adicional en  , sus ganancias pueden cambiar (línea 14)</p></li><li><p>Si alguno de sus vecinos ahora está completamente cubierto, todas las ganancias de sus vecinos pueden cambiar (líneas 14-16)</p></li></ol><p>Recalculamos las ganancias de manera perezosa, por lo que solo recalculamos la ganancia de un vértice si queremos insertarlo en  (líneas 18-20). Dado que las ganancias solo disminuyen, nunca podemos perder un vértice que debamos insertar.</p><p>Tenga en cuenta que simplemente necesitamos realizar un seguimiento de la ganancia total de vértices que agregamos a  para determinar cuándo salir. Además,  al menos un vértice tendrá una ganancia distinta de cero, por lo que siempre progresamos.</p><h3>Resultados</h3><p>Realizamos experimentos en cuatro conjuntos de datos que juntos cubren nuestras tres métricas de distancia admitidas (euclidiana, coseno y producto interno):</p><ol><li><p>quora-E5-small: 522931 documentos, 384 dimensiones y emplea la similitud del coseno,</p></li><li><p>cohe-wikipedia-v2: 1M de documentos, 768 dimensiones y emplea similitud de coseno,</p></li><li><p>gist: 1M documentos, 960 dimensiones y emplea la distancia euclidiana, y</p></li><li><p>cohe-wikipedia-v3: 1M de documentos, 1024 dimensiones y emplea el máximo producto interno.</p></li></ol><p>Para cada conjunto de datos evaluamos dos niveles de cuantificación:</p><ol><li><p>int8: que emplea un entero de 1 byte por dimensión y</p></li><li><p>BBQ: que emplea un solo bit por dimensión.</p></li></ol><p>Finalmente, para cada experimento evaluamos la calidad de la búsqueda a dos profundidades de recuperación y examinamos luego de construir el índice y luego luego de forzar la fusión en un solo segmento.</p><p>En resumen, logramos aceleramientos sustanciales consistentes en la indexación y la fusión mientras mantenemos la calidad del gráfico y, por lo tanto, el rendimiento de búsqueda en todos los casos.</p><h4>Experimento 1: cuantización int8</h4><p>Los aceleramientos promedio desde la línea de base hasta el candidato, los cambios propuestos, son:</p><p>Aceleramiento del tiempo del índice: <strong>1.28</strong></p><p>Forzar la velocidad de combinación: <strong>1.72</strong></p><p>Esto corresponde al siguiente desglose en tiempos de ejecución</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8422be122ef1a9c9/6a17e17bbe6086522e00467d/5f9f6d487b4c74c4998aa47465912c1a9743e928-734x479.png" alt="Indexar y fusionar tiempos para las estrategias de fusión de línea base y candidatas" /><p>Para completar, los tiempos exactos son</p><p></p><p>Índice</p><p></p><p>Fusionar</p><p></p><p>Conjunto de datos</p><p>referencia</p><p>candidato</p><p>Desarrolla</p><p>candidato</p><p>quora-E5-pequeño</p><p>112.41s</p><p>81.55s</p><p>113.81s</p><p>70.87s</p><p>wiki-cohesión-v2</p><p>158.1s</p><p>122,95 segundos</p><p>425.20s</p><p>239.28s</p><p>quid</p><p>141.82s</p><p>119.26s</p><p>536.07s</p><p>279.05s</p><p>wiki-cohesión-v3</p><p>211.86s</p><p>168.22s</p><p>654,97 segundos</p><p>414.12s</p><p>A continuación, mostramos los gráficos de recuperación frente a latencia que comparan el candidato (líneas discontinuas) con la línea de base en dos profundidades de recuperación: recall@10 y recall@100 para índices con múltiples segmentos (el resultado final de nuestra estrategia de fusión predeterminada luego de indexar todos los vectores) y luego de forzar la fusión en un solo segmento. Una curva más alta y más a la izquierda es mejor, lo que significa una mayor recuperación con una latencia más baja.</p><p>Como puede ver, para índices de múltiples segmentos, el candidato es mejor para el conjunto de datos Cohere v3 y un poco peor, pero casi comparable, para todos los demás conjuntos de datos. Luego de fusionar en un solo segmento, las curvas de recuperación son casi idénticas para todos los casos.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf674622469bf6a90/6a17e17dfbc5f874a84919c9/9195297f19f90b172d832ba56bdada7b0d8a768b-985x392.png" alt="Recuperar @10 y @100 frente a la latencia luego de crear el índice" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltde89f7e94ac823f4/6a17e17fe9ea876429a9c507/c866f32d579ae2b841e92ca733dd29c0e214ac6b-986x386.png" alt="Recuperar @10 y @100 frente a la latencia luego de fusionar en un solo segmento" /><h4>Experimento 2: Cuantización de asado</h4><p>Los aceleramientos promedio desde la línea de base hasta el candidato son:</p><p>Aceleramiento del tiempo de índice: <strong>1.33</strong></p><p>Forzar la velocidad de combinación: <strong>1.34</strong></p><p>Esto corresponde al siguiente desglose en tiempos de ejecución</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5b0eb305222fa5c2/6a17e18125daab514f08a18c/11be0cf6b63480dc703409329357ea4847b55051-740x415.png" alt="Indexar y fusionar el tiempo para las estrategias de fusión de línea base y candidatas" /><p>Para completar, los tiempos exactos son</p><p></p><p>Índice</p><p></p><p>Fusionar</p><p></p><p>Conjunto de datos</p><p>referencia</p><p>candidato</p><p>Desarrolla</p><p>candidato</p><p>quora-E5-pequeño</p><p>70.71s</p><p>58.25s</p><p>59.38s</p><p>40.15s</p><p>wiki-cohesión-v2</p><p>203.08s</p><p>142.27s</p><p>107.27s</p><p>85.68s</p><p>quid</p><p>110.35s</p><p>105,52 segundos</p><p>323,66 segundos</p><p>202.2s</p><p>wiki-cohesión-v3</p><p>313.43s</p><p>190.63s</p><p>165,98 segundos</p><p>159,95 segundos</p><p>Para índices de segmentos múltiples, el candidato es mejor para casi todos los conjuntos de datos, excepto cohere v2, donde la línea de base es ligeramente mejor. Para los índices de un solo segmento, las curvas de recuperación son casi idénticas para todos los casos.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb12174abeadbcdd1/6a17e1822f4a5c4cc2fa8829/7da1be27bab5a7f5f9c9a1882ea35761a4a0ba5c-973x383.png" alt="Recuperar @10 y @100 frente a la latencia luego de crear el índice" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf1b88d4f7fec9905/6a17e184414c64a8de9450b4/a26af30a56c55a12addee7e4fb7e8f606f366668-979x386.png" alt="Recordar @10 y @100 frente a la latencia que se fusionó en un solo segmento" /><h3>Conclusión</h3><p>El algoritmo discutido en este blog estará disponible en el próximo Lucene 10.2 y en la versión de Elasticsearch que se basa en él. Los usuarios podrán aprovechar el rendimiento mejorado de la combinación y el tiempo de compilación de índices reducido en estas nuevas versiones. Este cambio es parte de nuestro esfuerzo continuo para hacer que Lucene y Elasticsearch sean rápidos y eficientes para la búsqueda vectorial e híbrida.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hnsw-graphs-speed-up-merging</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hnsw-graphs-speed-up-merging</guid>
    <category><![CDATA[Base de datos vectorial]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Thomas Veasey,Mayya Sharipova]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9fca6a01d0541cfb/6a17e187033c8d46486bb0e1/49a6c880f5dedd0fa502ece5be124824ee218cc0-1792x1024.png" length="0" type="image/png"/>
    <pubDate>Mon, 07 Apr 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[Lucene Wrapped 2024]]></title>
    <description><![CDATA[2024 fue otro año importante para Apache Lucene. En este blog, exploraremos los puntos clave más destacados.]]></description>
    <content:encoded><![CDATA[<p>Apache Lucene tuvo una actividad significativa en 2024, con numerosos lanzamientos, incluida la primera gran actualización en tres años, repleta de mejoras emocionantes y nuevas funciones. Vamos a explorar algunos de los puntos clave.</p><h2>Lucene y la comunidad</h2><p>Un proyecto solo es tan fuerte como la comunidad que lo apoya. A pesar de más de 20 años de desarrollo, el proyecto Lucene sigue siendo vibrante y prospera gracias a sus colaboradores apasionados y activos.</p><p>En 2024, el proyecto Lucene recibió más de 2.000 commits de 98 colaboradores únicos y casi 800 pull requests. El número de colaboradores sigue creciendo, con nuevos comprometedores y afiliados a PMC que se unen al proyecto y contribuyen a su éxito.</p><h2>Lucene 10</h2><p>En 2024 se produjo el primer gran lanzamiento en casi 3 años: Lucene 10, con más de 2.000 commits de 185 colaboradores únicos. Aunque el modelo de desarrollo que sigue Lucene permite ofrecer muchas mejoras y características en lanzamientos menores, un lanzamiento importante ofrece la oportunidad de aportar funciones y modernizaciones más grandes. Por ejemplo, Lucene 10 requiere un mínimo de Java 21. Aumentar la versión mínima de Java garantiza que Lucene pueda seguir aprovechando las mejoras que ofrece Java moderno.</p><p>El objetivo principal de Lucene 10 es aprovechar mejor el hardware sobre el que se ejecuta. Echemos un vistazo rápido a algunos de los principales puntos destacados:</p><ul><li><p><strong>Más paralelismo en la búsqueda</strong> : aunque la ejecución de búsqueda ya está paralelizada entre segmentos, ahora vamos más allá, paralelizando dentro de los segmentos. Esto desacopla la representación en disco del rendimiento de ejecución, permitiendo que incluso segmentos individuales se beneficien del número de núcleos en sistemas modernos.</p></li><li><p><strong>Mejor paralelismo de E/S</strong> : el modelo sincrónico de E/S sencillo que emplea Lucene fue mejorado con una etapa de prelectura. Esto informa al sistema operativo de que se necesitará una región de un archivo índice en un futuro muy próximo, sin bloquear el hilo que llama.</p></li><li><p><strong>Mejor eficiencia de CPU y almacenamiento con indexación dispersa</strong> - Lucene 10 introduce soporte para indexación dispersa, a veces llamada indexación de clave primaria o indexación por zonas en otros almacenes de datos.</p></li></ul><p>Para más información sobre Lucene 10, consulta el <a href="https://www.elastic.co/search-labs/blog/apache-lucene-10-release-highlights">artículo</a> dedicado a Lucene 10.</p><h2>Investigación e innovación de Lucene</h2><p>En 2024, Lucene experimentó un auge de investigación e innovación, especialmente en las áreas de integración de aprendizaje automático, búsqueda vectorial y optimización para conjuntos de datos a gran escala, con 10 artículos y publicaciones <a href="https://scholar.google.com/scholar?as_ylo=2024&amp;q=lucene&amp;hl=en&amp;as_sdt=0,5">de</a> investigación de referencia separados. Algunas de las áreas y desarrollos clave de investigación incluyen:</p><ul><li><p><strong>Soporte para búsqueda vectorial e incrustación</strong> - Lucene ofrece una solución poderosa y escalable para la búsqueda basada en vectores, permitiendo la recuperación semántica a gran escala. Aprovechando la robusta infraestructura de indexación y búsqueda de Lucene, los usuarios pueden combinar lo mejor de la búsqueda tradicional de texto con las avanzadas capacidades de la búsqueda vectorial moderna, convirtiendo Lucene en una solución integral para una amplia gama de tareas de búsqueda y recuperación de información.</p></li><li><p><strong>Modelos de búsqueda híbrida</strong> - La investigación también profundizó en técnicas de búsqueda híbrida, donde Lucene combina la búsqueda tradicional basada en palabras clave con la recuperación moderna basada en vectores. Al combinar índices basados en términos con representaciones vectoriales densas, Lucene puede ofrecer resultados de búsqueda más precisos y contextualmente relevantes, cerrando la brecha entre la precisión de los motores de búsqueda tradicionales y la flexibilidad de la búsqueda semántica.</p></li></ul><p>Los esfuerzos de investigación en curso en 2024 demuestran la adaptabilidad de Lucene a las necesidades cambiantes de las tecnologías de búsqueda modernas, especialmente en el contexto de la IA, la búsqueda semántica y las aplicaciones de big data. El proyecto sigue creciendo como una plataforma poderosa, flexible y eficiente tanto para casos de búsqueda tradicionales como de vanguardia.</p><h2>Lanzamientos de Lucene en 2024</h2><p>Aunque no es un reflejo exacto, el gran volumen de lanzamientos pone de manifiesto la dedicación y energía continua de la comunidad. Estas actualizaciones incluyen mejoras importantes en el rendimiento y eficiencia de la búsqueda vectorial, soporte para madvise, optimizaciones para la decodificación de listas de anuncios, mejoras de velocidad adicionales mediante SIMD y mucho más.</p><p>Aquí tienes la lista completa de lanzamientos:</p><ul><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-1010-available">10.1.0</a> (2024-12-20)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9121-available">9.12.1</a> (2024-12-13)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-1000-available">10.0.0</a> (2024-10-14)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9120-available">9.12.0</a> (28-09-2024)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-8114-available">8.11.4</a> (2024-09-24)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9111-available">9.11.1</a> (27-06-2024)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9110-available">9.11.0</a> (2024-06-06)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9100-available">9.10.0</a> (2024-02-20)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-8113-available">8.11.3</a> (2024-02-08)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-992-available">9.9.2</a> (29-01-2024)</p></li></ul><p>Puedes encontrar más información y notas de lanzamiento en la página <a href="https://projects.apache.org/project.html?lucene-core">de Lucene Core</a> . Además, existen ediciones <a href="https://projects.apache.org/project.html?lucene-pylucene">equivalentes de PyLucene</a> .</p><h2>Concluyendo</h2><p>A medida que Lucene madura, sigue prosperando gracias a su comunidad dedicada y vibrante. Como vimos, 2024 fue un año increíblemente productivo, y ahora miramos hacia adelante los emocionantes desarrollos que traerá 2025.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/apache-lucene-wrapped-2024</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/apache-lucene-wrapped-2024</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Chris Hegarty]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt901211870015335c/6a17ddf6a292998b08d02b93/29a02c89b3c5adb37a5f900de634ff09cb63fdd9-1792x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 03 Jan 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[Elasticsearch vs. OpenSearch: Comparación del rendimiento de búsqueda vectorial]]></title>
    <description><![CDATA[Elasticsearch es de 2 a 12 veces más rápido que OpenSearch para la búsqueda vectorial]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison#up-to-12x-faster-out-of-the-box">TLDR: Elasticsearch es hasta 12 veces más rápido</a> - En Elastic hemos recibido numerosas solicitudes de nuestra comunidad para aclarar las diferencias de rendimiento entre Elasticsearch y OpenSearch, particularmente en el realm de la búsqueda semántica/búsqueda vectorial, por lo que hemos efectuado esta prueba de rendimiento para proporcionar una comparación clara y basada en datos: sin ambigüedades, solo datos directos para informar a nuestros usuarios. Los resultados muestran que <strong>Elasticsearch es hasta 12 veces más rápido</strong> que OpenSearch para la búsqueda de vectores y, por lo tanto, requiere menos recursos computacionales. Esto refleja el enfoque de Elastic en consolidar Lucene como la mejor base de datos vectorial para casos de uso de búsqueda y recuperación.</p><p>La búsqueda vectorial está revolucionando la forma en que hacemos búsquedas de similitud, particularmente en campos como la IA y el machine learning. Con la creciente adopción de modelos de incrustación de vectores, la capacidad de búsqueda eficiente a través de millones de vectores de alta dimensionalidad se vuelve crítica.</p><p>Cuando se trata de habilitar bases de datos vectoriales, Elastic y OpenSearch han adoptado enfoques notablemente diferentes. Elastic ha invertido mucho en la optimización de Apache Lucene junto con Elasticsearch para elevarlos como la opción de primer nivel para las aplicaciones de búsqueda vectorial. Por el contrario, OpenSearch ha ampliado su enfoque, integrando otras implementaciones de búsqueda vectorial y explorando más allá del alcance de Lucene. Nuestro enfoque en Lucene es estratégico, lo que nos permite brindar soporte sumamente integrado en nuestra versión de Elasticsearch, resultando en un conjunto de características mejorado en el que cada componente complementa y amplifica las capacidades del otro.</p><p>Este blog presenta una comparación detallada entre Elasticsearch 8.14 y OpenSearch 2.14, considerando diferentes configuraciones y motores vectoriales. En este análisis de rendimiento, Elasticsearch demostró ser la plataforma superior para las operaciones de búsqueda de vectores. Incluso las próximas <a href="https://www.elastic.co/search-labs/blog/vector-similarity-computations-ludicrous-speed">características</a> ampliarán las diferencias de forma más <a href="https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">significativa</a>. Cuando se enfrentó a OpenSearch, sobresalió en todas las pistas de referencia, <strong>con un rendimiento de 2 a 12 veces más rápido en promedio</strong>. Esto sucedió en todos los casos que utilizaban cantidades y dimensiones vectoriales variables, como <code>so_vector</code> (2M vectores, 768D), <code>openai_vector</code> (2.5M vectores, 1536D) y <code>dense_vector</code> (10M vectores, 96D), todos disponibles en <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance">este repositorio</a> junto con los scripts de Terraform para provisionar toda la infraestructura requerida en Google Cloud y los manifiestos de Kubernetes para ejecutar las pruebas.</p><p>Los resultados detallados en este blog complementan los resultados de un <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap">estudio previamente publicado y validado por terceros</a> que muestra que Elasticsearch es 40%–140% más rápido que OpenSearch para las operaciones de análisis de búsqueda más comunes: consulta de texto, clasificación, rango, histograma de fechas y filtrado de términos. Ahora podemos agregar otro diferenciador: la búsqueda vectorial.</p><h2>Hasta 12 veces más rápido desde el primer momento</h2><p>Nuestros puntos de referencia enfocados en los cuatro conjuntos de datos vectoriales involucraron tanto búsquedas de KNN aproximados como de KNN exactos, considerando diferentes tamaños, dimensiones y configuraciones, totalizando <code>40.189.820</code> solicitudes de búsqueda no almacenadas en caché. Los resultados: <strong>Elasticsearch es hasta 12 veces más rápido</strong> que OpenSearch para la búsqueda vectorial y, por lo tanto, requiere menos recursos computacionales.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt34b83c6eba3bcb6e/6a17d727dbb4ff18d6fb54fb/cdb26e91f085b90e9b12aeb8fee53b04d365ecae-1440x1156.webp" alt="promedio de P90" /><p>Figura 1: Tareas agrupadas para ANN y KNN exacto en diferentes combinaciones en Elasticsearch y OpenSearch.</p><p>Los grupos como <code>knn-10-100</code> implican una búsqueda KNN con  y . En la búsqueda vectorial HNSW,  determina el número de vecinos más cercanos a recuperar para un vector de consulta. Especifica cuántos vectores similares se deben encontrar como resultado.  establece el número de vectores candidatos a recuperar en cada segmento. Más candidatos pueden mejorar la precisión, pero requieren mayores recursos computacionales.</p><p>También probamos con diferentes técnicas de cuantización y aprovechamos las optimizaciones específicas del motor; los resultados detallados para cada pista, tarea y motor vectorial están disponibles a continuación.</p><h2>KNN exacto y KNN aproximado</h2><p>Al tratar con conjuntos de datos y casos de uso variados, el enfoque correcto para la búsqueda vectorial será diferente. En este blog, todas las tareas indicadas como <code>knn-*</code> como <code>knn-10-100</code> utilizan <strong>KNN aproximado</strong> y <code>script-score-*</code> se refieren a <strong>KNN exacto</strong>, pero ¿en qué se diferencian y por qué son importantes?</p><p>En definitiva, si estás manejando conjuntos de datos más sustanciales, el método preferido es Approximate K-Nearest Neighbor (ANN) debido a su escalabilidad superior. Para conjuntos de datos más modestos que pueden requerir un proceso de filtración, el método Exact KNN es ideal.</p><p>El KNN exacto utiliza un método de fuerza bruta, calculando la distancia entre un vector y todos los demás vectores en el conjunto de datos. Luego clasifica estas distancias para encontrar los  vecinos más cercanos. Si bien este método garantiza una coincidencia exacta, enfrenta desafíos de escalabilidad para conjuntos de datos grandes y de alta dimensionalidad. Sin embargo, hay muchos casos en los que se necesita un KNN exacto:</p><ul><li><p><strong>Recalificación</strong>: En escenarios que involucran búsquedas léxicas o semánticas seguidas de recalificación basada en vectores, el KNN exacto es esencial. Por ejemplo, en un motor de búsqueda de productos, los resultados de búsqueda iniciales se pueden filtrar en función de consultas textuales (por ejemplo, palabras clave, categorías) y luego se emplean vectores asociados con los elementos filtrados para una evaluación de similitud más precisa.</p></li><li><p><strong>Personalización</strong>: Al tratar con un gran número de usuarios, cada uno representado por un número relativamente pequeño (como 1 millón) de vectores distintos, la clasificación del índice por metadatos específicos del usuario (por ejemplo, user_id) y el cálculo de puntajes mediante fuerza bruta con vectores se vuelve eficiente. Este enfoque permite recomendaciones personalizadas o la entrega de contenido basadas en comparaciones vectoriales precisas adaptadas a las preferencias individuales del usuario.</p></li></ul><p>Por lo tanto, Exact KNN garantiza que la clasificación final y las recomendaciones basadas en la similitud de vectores sean precisas y estén adaptadas a las preferencias del usuario.</p><p>Por otro lado, el KNN aproximado (o ANN) emplea métodos para que la búsqueda de datos sea más rápida y eficaz que el KNN exacto, especialmente en conjuntos de datos grandes y de alta dimensionalidad. En lugar de un enfoque de fuerza bruta, que mide la distancia más cercana exacta entre una consulta y todos los puntos, lo que plantea problemas de cálculo y escalado, el ANN emplea ciertas técnicas para reestructurar de forma eficiente los índices y las dimensiones de los vectores buscables en el conjunto de datos. Aunque esto puede provocar una ligera imprecisión, aumenta considerablemente la velocidad del proceso de búsqueda, lo que lo convierte en una alternativa eficaz para tratar con grandes conjuntos de datos.</p><p>En este blog, todas las tareas indicadas como <code>knn-*</code> como <code>knn-10-100</code> usan <strong>KNN aproximado</strong> y <code>script-score-*</code> se refieren a <strong>KNN exacto</strong>.</p><h2>Metodología de prueba</h2><p>Si bien Elasticsearch y OpenSearch son similares en términos de API para las operaciones de búsqueda BM25, ya que este último es una bifurcación del primero, no ocurre lo mismo con la búsqueda vectorial, que se introdujo después de la bifurcación. OpenSearch adoptó un enfoque diferente al de Elasticsearch en lo que respecta a los algoritmos, al introducir otros dos motores — <code>nmslib</code> y <code>faiss</code> — además de <code>lucene</code>, cada uno con sus configuraciones y limitaciones específicas (por ejemplo, <code>nmslib</code> en OpenSearch no permite filtros, una característica esencial para muchos casos de uso).</p><p>Los tres motores utilizan el algoritmo Hierarchical Navigable Small World (HNSW), que es eficiente para la búsqueda aproximada de vecinos más cercanos y especialmente potente al trabajar con datos de alta dimensionalidad. Es importante señalar que <code>faiss</code> también admite un segundo algoritmo, <code>ivf</code>, pero dado que requiere entrenamiento previo en el conjunto de datos, nos centraremos únicamente en HNSW. La idea núcleo de HNSW es organizar los datos en varias capas de grafos conectados, donde cada capa representa una granularidad diferente del conjunto de datos. La búsqueda comienza en la capa superior con la vista más burda y progresa hacia capas cada vez más finas hasta llegar al nivel base.</p><p>Ambos motores de búsqueda se probaron en condiciones idénticas en un entorno controlado para asegurar la imparcialidad. El método aplicado es similar a <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap#testing-methodology">esta comparación de rendimiento publicada anteriormente</a>, con grupos de nodo dedicados para Elasticsearch, OpenSearch y Rally. El <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/main/terraform/main.tf">script de terraform</a> está disponible (junto con todas las fuentes) para provisionar un clúster de Kubernetes con:</p><ul><li><p>1 grupo de nodo para Elasticsearch con 3 <code>e2-standard-32</code> máquinas (128 GB de RAM y 32 CPU)</p></li><li><p>Grupo de 1 Node para OpenSearch con 3 máquinas <code>e2-standard-32</code> (128 GB de RAM y 32 CPU)</p></li><li><p>Grupo de 1 Node para Rally con 2 máquinas <code>t2a-standard-16</code> (64 GB de RAM y 16 CPU)</p></li></ul><p>Cada "pista" (o prueba) se ejecutó 10 veces para cada configuración, que incluyó diferentes motores, diferentes configuraciones y diferentes tipos de vectores. Las pistas tienen tareas que se repiten entre 1000 y 10 000 veces, dependiendo de la pista. Si una de las tareas de una pista fallaba, por ejemplo, debido a un tiempo de espera de red, todas las tareas se descartaban, por lo que todos los resultados representan pistas que comenzaron y terminaron sin problemas. Todos los resultados de las pruebas se validan estadísticamente, lo que garantiza que las mejoras no sean una coincidencia.</p><h2>Resultados detallados</h2><p>¿Por qué comparar usando el percentil 99 y no el promedio de latencia? Consideremos un ejemplo hipotético de los precios promedio de las viviendas en un barrio determinado. El precio promedio puede indicar una zona cara, pero en una inspección más cercana, puede resultar que la mayoría de las viviendas estén valoradas mucho más bajo, con solo unas pocas propiedades de lujo inflando la cifra promedio. Esto ilustra cómo el precio promedio puede no representar con precisión el espectro completo de valores de las viviendas en esa zona. Esto es similar a examinar los tiempos de respuesta, en los que el promedio puede ocultar problemas críticos.</p><h4>Tareas</h4><ul><li><p>KNN aproximado con k:10 n:50</p></li><li><p>KNN aproximado con k:10 n:100</p></li><li><p>KNN aproximado con k:100 n:1000</p></li><li><p>KNN aproximado con k:10 n:50 y filtros de palabras clave</p></li><li><p>KNN aproximado con k:10 n:100 y filtros de palabras clave</p></li><li><p>KNN aproximado con k:100 n:1000 y filtros de palabras clave</p></li><li><p>KNN aproximado con k:10 n:100 en combinación con indexación</p></li><li><p>KNN exacto (puntaje del script)</p></li></ul><h4>Motores vectoriales</h4><ul><li><p><code>lucene</code> en Elasticsearch y OpenSearch, ambos en la versión 9.10</p></li><li><p><code>faiss</code> en OpenSearch</p></li><li><p><code>nmslib</code> en OpenSearch</p></li></ul><h4>Tipos de vectores</h4><ul><li><p><code>hnsw</code> en Elasticsearch y OpenSearch</p></li><li><p><code>int8_hnsw</code> en Elasticsearch (HNSW con cuantificación automática de 8 bits: <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">enlace</a>)</p></li><li><p><code>sq_fp16 hnsw </code>en OpenSearch (HNSW con cuantificación automática de 16 bits: <a href="https://opensearch.org/docs/2.14/search-plugins/knn/knn-vector-quantization#faiss-16-bit-scalar-quantization">enlace</a>)</p></li></ul><h4>Búsqueda de segmentos concurrentes y lista para usar</h4><p>Como probablemente sabes, Lucene es una biblioteca de motor de búsqueda de texto de alto rendimiento escrita en Java que sirve como estructura para muchas plataformas de búsqueda como Elasticsearch, OpenSearch y Solr. Básicamente, Lucene organiza los datos en segmentos, que son esencialmente índices autónomos que permiten a Lucene ejecutar búsquedas de manera más eficiente. Entonces, cuando emites una búsqueda a cualquier motor de búsqueda basado en Lucene, tu búsqueda terminará siendo ejecutada en esos segmentos, ya sea secuencialmente o en paralelo.</p><p>OpenSearch introdujo la búsqueda de segmentos concurrentes como una opción adicional y no la utiliza por defecto; debes habilitarla mediante una configuración especial del índice <code>index.search.concurrent_segment_search.enabled</code> como se detalla <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/">aquí</a>, con algunas <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/#other-considerations">limitaciones</a>.</p><p>Elasticsearch, por otro lado, hace búsquedas en segmentos de forma concurrente <a href="https://github.com/elastic/elasticsearch/pull/101230">listas para usar</a>, por lo que las comparaciones que hacemos en este blog tendrán en cuenta, además de los diferentes motores de vectores y tipos de vectores, también las diferentes configuraciones:</p><ul><li><p>Elasticsearch ootb: Elasticsearch listo para usar, con búsqueda concurrente por segmentos;</p></li><li><p>OpenSearch ootb: sin búsqueda concurrente de segmentos habilitada;</p></li><li><p>OpenSearch css: con búsqueda concurrente de segmentos habilitada</p></li></ul><p>Comencemos con algunos resultados detallados para cada conjunto de datos vectoriales probado:</p><h2>2,5 millones de vectores, 1536 dimensiones (openai_vector)</h2><p>Comenzando con la ruta más simple, pero también la más grande en términos de dimensiones, <a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a>, que utiliza el <a href="https://huggingface.co/datasets/BeIR/nq">conjunto de datos NQ</a> enriquecido con incrustaciones generadas usando el <a href="https://openai.com/blog/new-and-improved-embedding-model">modelo text-embedding-ada-002</a> de OpenAI. Es el más simple ya que solo prueba KNN aproximado y tiene solo 5 tareas. Se prueba de forma independiente (sin indexar) así como junto con la indexación, y utilizando un solo cliente y 8 clientes simultáneos.</p><h3>Tareas</h3><ul><li><p><strong>standalone-search-knn-10-100-multiple-clients</strong>: búsqueda en 2,5 millones de vectores con 8 clientes simultáneamente, k: 10 y n:100</p></li><li><p><strong>standalone-search-knn-100-1000-multiple-clients</strong>: búsqueda en 2,5 millones de vectores con 8 clientes simultáneamente, k: 100 y n:1000</p></li><li><p><strong>standalone-search-knn-10-100-single-client</strong>: búsqueda en 2,5 millones de vectores con un solo cliente, k: 10 y n:100</p></li><li><p><strong>standalone-search-knn-100-1000-single-client</strong>: búsqueda en 2,5 millones de vectores con un solo cliente, k: 100 y n: 1000</p></li><li><p><strong>parallel-documents-indexing-search-knn-10-100</strong>: búsqueda en 2.5 millones de vectores mientras se indexan 100 000 documentos adicionales, k:10 y n:100</p></li></ul><p>El rendimiento promedio del p99 se describe a continuación:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf5791848eb0c12fb/6a17d72925daab9cae08a09e/eea0b2b49c690baada3e09d6968e513bfffe51a9-1440x318.webp" alt="tabla openai_vector" /><p>Aquí observamos que Elasticsearch es de <strong>3x a 8x más rápido</strong> que OpenSearch al realizar la búsqueda vectorial junto con la indexación (por ej. lectura+escritura) con :10 y :100 y de <strong>2x a 3x más rápido</strong> sin indexar para los mismos k y n. Para :100 y :1000 (<em>standalone-search-knn-100-1000-single-client</em> y <em>standalone-search-knn-100-1000-multiple-clients</em> Elasticsearch es de <strong>2x a 7x</strong> más rápido que OpenSearch, en promedio.</p><p>Los resultados detallados muestran los casos exactos y los motores vectoriales comparados:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdbe41d3187ced7ec/6a17d72a445de951c44cff4c/a7a761ed631d3e6211beb83d9d93d752d10123c9-1440x1728.webp" alt="openai_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0.969485</p><p>0.995138</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0.781445</p><p>0.784817</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0.96519</p><p>0.995422</p><p>OpenSearch-2.14.0@faiss</p><p>0.984154</p><p>0.98049</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0.980012</p><p>0.97721</p><p>OpenSearch-2.14.0@nmslib</p><p>0.982532</p><p>0.99832</p><h2>10 millones de vectores, 96 dimensiones (dense_vector)</h2><p>En <a href="https://github.com/elastic/rally-tracks/tree/master/dense_vector">dense_vector</a> con 10M vectores y 96 dimensiones. Se basa en el conjunto de datos de imágenes <a href="https://big-ann-benchmarks.com/">Yandex DEEP1B</a>. El conjunto de datos se crea a partir de los primeros 10 millones de vectores del archivo "sample data" llamado <code>learn.350M.fbin</code>. Las operaciones de búsqueda utilizan vectores de la búsqueda de archivos "query data".<code>public.10K.fbin</code>.</p><p>Tanto Elasticsearch como OpenSearch funcionan muy bien en este conjunto de datos, especialmente después de un <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html">force merge</a>, que generalmente se realiza en índices de solo lectura y es similar a desfragmentar el índice para tener una sola "tabla" en la que realizar la búsqueda.</p><h3>Tareas</h3><p>Cada tarea se prepara para 100 solicitudes y luego se miden 1000 solicitudes</p><ul><li><p><strong>knn-search-10-100</strong>: búsqueda en 10 millones de vectores, k: 10 y n:100</p></li><li><p><strong>knn-search-100-1000</strong>: búsqueda en 10 millones de vectores, k: 100 y n: 1000</p></li><li><p><strong>knn-search-10-100-force-merge</strong>: búsqueda en 10 millones de vectores después de una fusión forzada, k: 10 y n:100</p></li><li><p><strong>knn-search-100-1000-force-merge</strong>: búsqueda en 10 millones de vectores después de una fusión forzada, k: 100 y n:1000</p></li><li><p><strong>knn-search-100-1000-concurrent-with-indexing</strong>: búsqueda en 10 millones de vectores mientras también se actualiza <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/challenges/default.json#L76C36-L76C37">el 5 % del conjunto de datos</a>, k: 100 y n: 1000</p></li><li><p><strong>script-score-query</strong>: búsqueda KNN exacta de <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/queries.json">2000 vectores específicos</a>.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4629d06af85eb96c/6a17d72c6864a423e7b685dc/174995e0a2156d86359cdb7aa446dfaae6312ea4-1440x316.webp" alt="dense_vector" /><p>Tanto Elasticsearch como OpenSearch tuvieron un buen desempeño para el KNN aproximado. Cuando el índice se fusiona (es decir, tiene un solo segmento) en <em>knn-search-100-1000-force-merge</em> y <em>knn-search-10-100-force-merge</em>, OpenSearch funciona mejor que los demás cuando se usan <code>nmslib</code> y <code>faiss</code>, aunque todos estén alrededor de 15 ms y todos muy cerca.</p><p>Sin embargo, cuando el índice tiene varios segmentos (una situación típica en la que un índice recibe actualizaciones de sus documentos) en <em>knn-search-10-100</em> y <em>knn-search-100-1000</em>, Elasticsearch mantiene la latencia en aproximadamente ~7 ms y ~16 ms, mientras que todos los demás motores de OpenSearch son más lentos.</p><p>También cuando se busca en el índice y se indexa en él al mismo tiempo (<em>knn-search-100-1000-concurrent-with-indexing</em>), Elasticsearch mantiene la latencia por debajo de 15 ms (a 13.8 ms), siendo casi <strong>4x más rápido</strong> que OpenSearch out-of-the-box (49.3 ms) y aún más rápido cuando se habilita la búsqueda concurrente por segmentos (17.9 ms), pero demasiado cerca para ser significativo.</p><p>En cuanto al KNN exacto, la diferencia es mucho mayor: Elasticsearch <strong>es 6 veces más rápido</strong> que OpenSearch (~260 ms vs ~1600 ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt254f43bcaa3dbfc2/6a17d72ddbb4ffc780fb54ff/17aec6be31117440bc4d1f99984aed95df1c4f6b-1440x1728.webp" alt="dense_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0.969843</p><p>0.996577</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0.775458</p><p>0.840254</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0.971333</p><p>0.996747</p><p>OpenSearch-2.14.0@faiss</p><p>0.9704</p><p>0.914755</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0.968025</p><p>0.913862</p><p>OpenSearch-2.14.0@nmslib</p><p>0.9674</p><p>0.910303</p><h2>2 millones de vectores, 768 dimensiones (so_vector)</h2><p>Esta <a href="https://github.com/elastic/rally-tracks/tree/master/so_vector">pista</a>, <code>so_vector</code>, se deriva de un <a href="https://archive.org/download/stackexchange/stackoverflow.com-Posts.7z">volcado de publicaciones de StackOverflow descargadas</a> el 21 de abril de 2022. Solo contiene documentos de preguntas; se eliminaron todos los documentos que representan respuestas. Cada título de pregunta se codificó en un vector usando el modelo de transformador de oraciones <a href="https://huggingface.co/sentence-transformers/multi-qa-mpnet-base-cos-v1">multi-qa-mpnet-base-cos-v1</a>. Este conjunto de datos contiene los primeros 2 millones de preguntas.</p><p>A diferencia de la pista anterior, cada documento aquí contiene otros campos además de vectores para soportar características de prueba como KNN aproximado con filtrado y búsqueda híbrida. <code>nmslib</code> para OpenSearch está notablemente ausente en esta prueba ya que <a href="https://opensearch.org/docs/latest/search-plugins/knn/filter-search-knn/#k-nn-search-with-filters">no admite filtros</a>.</p><h3>Tareas</h3><p>Cada tarea se calienta con 100 solicitudes y luego se miden 100 solicitudes. Tenga en cuenta que las tareas se agruparon por simplicidad, ya que la prueba contiene 16 tipos de búsqueda * 2 valores k diferentes * 3 valores n diferentes.</p><ul><li><p><strong>knn-10-50</strong>: búsqueda en 2 millones de vectores sin filtros, k:10 y n:50</p></li><li><p><strong>knn-10-50-filtered</strong>: búsqueda en 2 millones de vectores <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">con filtros</a>, k:10 y n:50</p></li><li><p><strong>knn-10-50-after-force-merge</strong>: búsqueda en 2 millones de vectores con filtros y después de una fusión forzosa, k:10 y n:50</p></li><li><p><strong>knn-10-100</strong>: búsqueda en 2 millones de vectores sin filtros, k:10 y n:100</p></li><li><p><strong>knn-10-100-filtered</strong>: búsqueda en 2 millones de vectores <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">con filtros</a>, k:10 y n:100</p></li><li><p><strong>knn-10-100-after-force-merge</strong>: búsqueda en 2 millones de vectores con filtros y luego de una fusión forzada, k: 10 y n: 100</p></li><li><p><strong>knn-100-1000</strong>: búsqueda en 2 millones de vectores sin filtros, k:100 y n:1000</p></li><li><p><strong>knn-100-1000-filtered</strong>: búsqueda en 2 millones de vectores <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">con filtros</a>, k:100 y n:1000</p></li><li><p><strong>knn-100-1000-after-force-merge</strong>: búsqueda en 2 millones de vectores con filtros y luego de una fusión forzada, k:100 y n:1000</p></li><li><p><strong>exact-knn</strong>: búsqueda de KNN exacto <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json#L56">con y sin filtros</a>.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8b44e1306af35877/6a17d72f577262aca11bca3e/d4ed2982d55370cad4b2048b23ce97caa55017c0-1440x316.webp" alt="tabla so_vector" /><p>Elasticsearch es <strong>consistentemente más rápido</strong> que OpenSearch de manera inmediata en esta prueba, solo en dos casos OpenSearch es más rápido, y no por mucho (<em>knn-10-100</em> y <em>knn-100-1000</em>). Las tareas que involucran <em>knn-10-50</em>, <em>knn-10-100</em> y <em>knn-100-1000</em> en combinación con filtros muestran una diferencia de hasta <strong>7 veces</strong> (112 ms versus 803 ms).</p><p>El rendimiento de ambas soluciones parece igualarse después de un "force merge" (fusión forzada), lógicamente, como lo demuestran <em>knn-10-50-after-force-merge</em>, <em>knn-10-100-after-force-merge</em> y <em>knn-100-1000-after-force-merge.</em> En esas tareas, <code>faiss</code> es más rápido.</p><p>Como mencionamos, el rendimiento para Exact KNN es muy diferente, ya que Elasticsearch fue <strong>13 veces más rápido</strong> que OpenSearch esta vez (~385 ms vs ~5262 ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf80ccc7c2df84559/6a17d7314b055de00d43203f/615cb9228eb05ddd2e9512b3a6a5bc88d4088a1a-1440x1440.webp" alt="so_vector" /><h4>Recall</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>knn-recall-10-50</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>1</p><p>0.986667</p><p>1</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@nmslib</p><p>0.9674</p><p>0.910303</p><p>0.976394</p><h2>Elasticsearch y Lucene como claros vencedores</h2><p>En Elastic, estamos innovando incansablemente Apache Lucene y Elasticsearch para garantizar que podamos proporcionar la base de datos vectorial de primer nivel para casos de uso de búsqueda y recuperación, incluido RAG (Retrieval Augmented Generation). Nuestros últimos avances han aumentado significativamente el rendimiento, haciendo que la búsqueda vectorial <a href="https://search-labs.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">sea más rápida y eficiente en cuanto a espacio</a> que antes, basándose en las mejoras de Lucene 9.10. En este blog, se presentó un estudio que muestra que al comparar versiones actualizadas, Elasticsearch es hasta 12 veces más rápido que OpenSearch.</p><p>Vale la pena señalar que ambos productos usan la misma versión de Lucene (<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/release-notes-8.14.0.html">Notas de lanzamiento de Elasticsearch 8.14</a> y <a href="https://github.com/opensearch-project/OpenSearch/blob/2.14/release-notes/opensearch.release-notes-2.14.0.md">Notas de lanzamiento de OpenSearch 2.14</a>).</p><p>El ritmo de innovación en Elastic ofrecerá aún más, no solo para nuestros clientes locales y de Elastic Cloud, sino también para aquellos que utilizan nuestra <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">plataforma sin estado</a>. Las características como el soporte para la <a href="https://www.elastic.co/search-labs/blog/int4-scalar-quantization-in-lucene">cuantificación escalar a int4</a> se ofrecerán con pruebas rigurosas para garantizar que los clientes puedan utilizar estas técnicas sin una caída significativa en la recuperación, similar a <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">nuestras pruebas para int8</a>.</p><p>La eficiencia de la búsqueda vectorial se está convirtiendo en una característica no negociable en los motores de búsqueda modernos debido a la proliferación de aplicaciones de inteligencia artificial y machine learning. Para las organizaciones que buscan un motor de búsqueda poderoso capaz de mantenerse al día con las demandas de datos vectoriales de alto volumen y alta complejidad, Elasticsearch es la respuesta definitiva.</p><p>Ya sea que quieres expandir una plataforma establecida o iniciar nuevos proyectos, integrar Elasticsearch para las necesidades de búsqueda vectorial es un movimiento estratégico que generará beneficios tangibles a largo plazo. Con su ventaja de rendimiento comprobada, Elasticsearch está a punto de apuntalar la próxima ola de innovaciones en búsqueda.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</guid>
    <category><![CDATA[Base de datos vectorial]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Ugo Sangiorgi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5d70b25967c2194e/6a17d732b1e11383f879f0ca/13c3c0053e2968fb835ba2f90f34bec3a011b5c0-880x592.webp" length="0" type="image/webp"/>
    <pubDate>Wed, 26 Jun 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>
  <item>
    <title><![CDATA[Implementación de artículos académicos: Lecciones aprendidas de Elasticsearch y Lucene]]></title>
    <description><![CDATA[Descubre estrategias para incorporar trabajos de investigación en una aplicación de software, basándonos en nuestras experiencias con Elasticsearch y Lucene.]]></description>
    <content:encoded><![CDATA[<p>Esta publicación comparte estrategias para implementar trabajos académicos en una aplicación de software. Se basa en ejemplos de Elasticsearch y Lucene con la esperanza de ayudar a otros ingenieros a aprender de nuestras experiencias. Puedes leer estas estrategias y pensar "¡pero esto es solo desarrollo de software!" Y eso sería, efectivamente: como ingenieros ya tenemos las prácticas y herramientas adecuadas, solo necesitan adaptar a un nuevo reto.</p><h2>Fondo</h2><p>Mientras desarrollamos Elasticsearch, ocasionalmente nos encontramos con un problema importante sin un enfoque sencillo o establecido para solucionarlo. Es natural preguntar: "hmm, ¿existe algún artículo académico que aborde esto?" Otras veces, el trabajo académico es una fuente de inspiración. Nos encontramos con un artículo que propone un nuevo algoritmo o estructura de datos y pensamos "¡esto sería muy útil!" Aquí tienes solo algunos ejemplos de cómo Elasticsearch y Apache Lucene incorporan el trabajo académico:</p><ul><li><p><a href="https://research.google/pubs/pub40671/">HyperLogLog++</a> para <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-aggregations-metrics-cardinality-aggregation.html">agregaciones de cardinalidad</a></p></li><li><p><a href="https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-suresh.pdf">Algoritmo C3</a> para <a href="https://www.elastic.co/blog/improving-response-latency-in-elasticsearch-with-adaptive-replica-selection">la selección adaptativa de réplicas</a></p></li><li><p><a href="https://arxiv.org/abs/1603.09320">Grafos jerárquicos navegables de mundos pequeños (HNSW)</a> para búsqueda vectorial más cercana en Lucene</p></li><li><p><a href="https://jmlr.csail.mit.edu/papers/volume17/15-308/15-308.pdf">Estadística MIC</a> para <a href="https://github.com/elastic/ml-cpp/pull/488">mejorar la clasificación del aprendizaje automático</a></p></li><li><p><a href="http://engineering.nyu.edu/~suel/papers/bmw.pdf">WAND de bloqueo máximo</a> para <a href="https://www.elastic.co/blog/faster-retrieval-of-top-hits-in-elasticsearch-with-block-max-wand">una recuperación más rápida de top-hits en Lucene</a></p></li><li><p>... y <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/query-dsl-combined-fields-query.html">muchos</a> <a href="https://github.com/elastic/elasticsearch/blob/b2a9328890b23e7ccf6c66a3b13d6d65e453a3dd/server/src/main/java/org/elasticsearch/search/sort/BucketedSort.java#L302-L323">más</a></p></li></ul><p>Los artículos académicos son un recurso invaluable para los ingenieros que desarrollan sistemas intensivos en datos. Pero implementarlas puede ser intimidante y propenso a errores: las descripciones de algoritmos suelen ser complejas, omitiendo detalles prácticos importantes. Y las pruebas son un verdadero desafío: por ejemplo, ¿cómo podemos probar a fondo un algoritmo de aprendizaje automático cuya salida depende estrechamente del conjunto de datos?</p><h2>Evalúa el trabajo como si fuera una dependencia de software</h2><p>Agregar una nueva dependencia de software requiere una evaluación cuidadosa: si el otro paquete es incorrecto, lento o inseguro, nuestro proyecto también podría serlo. Antes de incorporar una dependencia, los desarrolladores se cercioran de evaluar su calidad.</p><p>Lo mismo ocurre con los trabajos académicos que estás considerando implementar. Puede parecer que, porque un algoritmo se publicó en un artículo, debe ser correcto y funcionar bien. Pero aunque pasó un proceso de revisión, un artículo académico puede tener problemas. Quizá la demostración de corrección se basa en suposiciones que no son realistas. O quizá la sección de "experimentos" muestra un rendimiento mucho mejor que la línea base, pero esto solo se cumple en un conjunto de datos específico. Aunque el trabajo sea de gran calidad, su enfoque puede no encajar bien con tu proyecto.</p><p>Al pensar si tomar una "dependencia" de un artículo académico, es útil plantear las mismas preguntas que haríamos con un paquete de software:</p><ul><li><p>¿La biblioteca está ampliamente empleada y "probada en batalla"? → ¿Otros paquetes implementaron este artículo y les funcionó bien?</p></li><li><p>¿Están disponibles los benchmarks de rendimiento? ¿Te parecen precisos y justos? → ¿El artículo incluye experimentos realistas? ¿Están bien diseñadas?</p></li><li><p>¿Es suficiente una mejora de rendimiento como para justificar la complejidad? → ¿El artículo se compara con un enfoque de línea base fuerte? ¿Cuánto supera esta línea base?</p></li><li><p>¿Se integrará bien este enfoque con nuestro sistema? → ¿Encajan las suposiciones y compensaciones del algoritmo en nuestro caso de uso?</p></li></ul><p>De alguna manera, cuando un paquete de software publica una comparación de rendimiento con sus competidores, ¡el paquete siempre sale más rápido! Si un tercero diseñó los benchmarks, podrían estar más equilibrados. El mismo fenómeno se aplica a los artículos académicos. Si un algoritmo funciona bien no solo en el artículo original, sino que también aparece en otros como una línea base estable, entonces es muy probable que sea estable.</p><h2>Sé creativo con las pruebas</h2><p>Los algoritmos de artículos académicos suelen tener un comportamiento más sofisticado que los tipos de algoritmos que habitualmente nos encontramos. Quizá sea un algoritmo de aproximación que sacrifica la precisión por una mejor velocidad. O quizá sea un método de aprendizaje automático que absorbe un gran conjunto de datos y produce resultados (a veces inesperados). ¿Cómo podemos escribir pruebas para estos algoritmos si no podemos caracterizar su comportamiento de forma sencilla?</p><h3>Enfoque en invariantes</h3><p>Al diseñar pruebas unitarias, es común pensar en términos de ejemplos: si damos al algoritmo esta entrada de ejemplo, debería tener esa salida. Desafortunadamente, para la mayoría de los algoritmos matemáticos, las pruebas basadas en ejemplos no cubren suficientemente su comportamiento.</p><p>Consideremos el algoritmo C3, que Elasticsearch emplea para determinar qué nodo debe gestionar una solicitud de búsqueda. Clasifica cada nodo usando una fórmula matizada que incorpora el servicio y los tiempos de respuesta previos del nodo, así como el tamaño de su cola. Probar un par de ejemplos realmente no verifica que entendiéramos bien la fórmula. Ayuda dar un paso atrás y pensar en probar invariantes: si aumenta el tiempo de servicio, ¿disminuye el rango del nodo? Si el tamaño de la cola es 0, ¿el rango se determina por el tiempo de respuesta, como afirma el artículo?</p><p>Centrar en los invariantes puede ayudar en varios casos comunes:</p><ul><li><p>¿Se supone que el método es agnóstico respecto al orden? Si es así, pasar los datos de entrada en un orden diferente debería dar el mismo resultado.</p></li><li><p>¿Algún paso del algoritmo produce probabilidades de clase? Si es así, estas probabilidades deberían sumar 1.</p></li><li><p>¿Es la función simétrica alrededor del origen? Si es así, invertir el signo de la entrada debería simplemente invertir el signo de la salida.</p></li></ul><p>Cuando implementamos C3 por primera vez, tuvimos un error en la fórmula donde accidentalmente usamos el inverso del tiempo de respuesta en lugar del tiempo de respuesta. ¡Esto significaba que los nodos más lentos podían estar más bien clasificados! Al solucionar el problema, nos <a href="https://github.com/elastic/elasticsearch/pull/70283">cercioramos de agregar comprobaciones invariantes</a> para evitar futuros errores.</p><h3>Comparar con una implementación de referencia</h3><p>Junto al artículo, los autores esperan publicar una implementación del algoritmo. (Esto es especialmente probable si el artículo contiene experimentos, ya que muchas revistas requieren que los autores envíen el código para reproducir los resultados.) Puedes probar tu enfoque con esta implementación de referencia para cerciorarte de que no te perdiste detalles importantes del algoritmo.</p><p>Mientras desarrollábamos la implementación HNSW de Lucene para la búsqueda de vecinos más cercanos, <a href="https://issues.apache.org/jira/browse/LUCENE-9937">probamos contra una biblioteca de referencia</a> de los autores del artículo. Ejecutamos tanto Lucene como la biblioteca con el mismo conjunto de datos, comparando la precisión de sus resultados y el número de cálculos que realizaron. Cuando estos números coinciden de cerca, sabemos que Lucene implementa fielmente el algoritmo.</p><p>Al incorporar un algoritmo en un sistema, a menudo necesitas hacer modificaciones o extensiones, como escalarlo a múltiples núcleos o agregar heurísticas para mejorar el rendimiento. Lo mejor es implementar primero una versión "vanilla", probarla con la referencia y luego hacer cambios incrementales. Así puedes estar seguro de que capturaste todas las piezas clave antes de hacer personalizaciones.</p><h3>Duelo contra un algoritmo existente</h3><p>La última sección plantea otra idea para un invariante de prueba: comparar la salida del algoritmo con la salida de un algoritmo más simple y mejor entendido. Como ejemplo, consideremos el algoritmo WAND de bloques máximas en Lucene, que acelera la recuperación de documentos saltar documentos que no pueden aparecer en los resultados principales. Es difícil describir exactamente cómo debería comportar la WAND con bloque máximo en todos los casos, pero sí sabemos que aplicarla no debería cambiar los mejores resultados. Así que nuestras pruebas pueden generar varias consultas de búsqueda aleatoria, luego <a href="https://github.com/apache/lucene/blob/main/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java#L669">ejecutarlas tanto con como sin la optimización WAND</a> y comprobar que sus resultados siempre coinciden.</p><p>Un aspecto importante de estas pruebas es que <a href="https://www.elastic.co/blog/elasticsearch-testing-qa-increasing-coverage-randomizing-test-runs">generan entradas aleatorias</a> sobre las que realizar la comparación. Esto puede ayudar a resolver casos que no consideraste y a sacar a la luz problemas inesperados. Por ejemplo, la prueba de comparación aleatoria de Lucene para el puntaje BM25F ayudó <a href="https://issues.apache.org/jira/browse/LUCENE-10039">a detectar errores en casos extremos sutiles</a>. La idea de alimentar a un algoritmo con entradas aleatorias está estrechamente relacionada con el concepto de <a href="https://en.wikipedia.org/wiki/Fuzzing">fuzzing</a>, una técnica común de pruebas en seguridad informática.</p><p>Elasticsearch y Lucene emplean frecuentemente este enfoque de prueba. Si ves una prueba que menciona un "duelo" entre dos algoritmos (TestDuelingAnalyzers, testDuelTermsQuery...), entonces sabes que esta estrategia está en acción.</p><h2>Emplea la terminología del artículo</h2><p>Cuando otro desarrollador trabaje con tu código, necesitará consultar el documento para seguir sus detalles. El <a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/search/aggregations/metrics/HyperLogLogPlusPlus.java#L24-L39">comentario sobre la implementación de HyperLogLog++ de Elasticsearch</a> lo dice bien: "Intentar entender qué hace esta clase sin leer el artículo se considera aventurero." Este comentario sobre el método también da un buen ejemplo. Incluye un enlace al artículo académico y destaca qué modificaciones se hicieron al algoritmo tal y como fue descrito originalmente.</p><p>Como los desarrolladores basan su comprensión del código en el papel, es útil usar exactamente la misma terminología. Dado que la notación matemática es concisa, esto puede dar lugar a nombres que normalmente no se considerarían de "buen estilo", pero que son muy claros en el contexto del artículo. Las fórmulas de artículos académicos son una de las pocas ocasiones en las que te encontrarás con nombres de variables crípticos en Elasticsearch, como <a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/node/ResponseCollectorService.java#L151">rS y muBarSInverse</a>.</p><p>
<em>La forma recomendada por el autor de leer un artículo: con un café grande.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12d81453c706f7cb/6a17d80e0b0bede6badd3424/d03a8e2a50e173b15a4ec3732810c63ff53321f6-1440x1081.jpg" alt="elastic-blog-academicpaper.jpg" /><h2>Puedes enviar un email al autor</h2><p>Al trabajar en un trabajo difícil, puedes pasar horas dándole vueltas a una fórmula, sin saber si estás malinterpretando o si simplemente hay un error tipográfico. Si fuera un proyecto de código abierto, podrías hacer una pregunta en GitHub o StackOverflow. Pero, ¿a dónde puedes acudir para un artículo académico? Los autores parecen ocupados y pueden molestar por tus correos.</p><p>Al contrario, a muchos académicos les encanta saber que sus ideas se están poniendo en práctica y están encantados de responder preguntas por email. Si trabajas en un producto que ellos conocen, ¡incluso pueden poner la solicitud en su sitio web!</p><p>También hay una tendencia creciente de académicos a debatir artículos en público, empleando muchas de las mismas herramientas del desarrollo de software. Si un artículo incluye un paquete de software adjunto, puede que encuentres respuestas a <a href="https://github.com/facebookresearch/faiss/issues/1928">preguntas comunes en Github</a>. Las comunidades de Stack Exchange como "Theoretical Computer Science" y "Cross Validated" también contienen <a href="https://cstheory.stackexchange.com/questions/49296/problem-in-the-paper-stable-minimum-space-partitioning-in-linear-time">discusiones detalladas sobre artículos populares</a>. Algunas conferencias empezaron a publicar todas las reseñas de artículos en línea. Estas revisiones contienen <a href="https://openreview.net/forum?id=H1eA7AEtvS">conversaciones de ida y vuelta</a> con los autores que pueden aportar ideas útiles sobre el enfoque.</p><h2>Continuará</h2><p>Esta publicación se centra en los fundamentos para elegir un trabajo académico y </p><p>Implementarlo correctamente, pero no cubre todos los aspectos del despliegue real del algoritmo. Por ejemplo, si el algoritmo es solo un componente en un sistema complejo, ¿cómo cercioramos que los cambios en el componente conduzcan a mejoras de extremo a extremo? ¿Y qué pasa si integrar el algoritmo requiere modificaciones o extensiones sustanciales que el artículo original no cubre? Son temas importantes sobre los que esperamos compartir más en futuras publicaciones.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/implementing-academic-papers-lessons-learned-from-elasticsearch-and-lucene</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/implementing-academic-papers-lessons-learned-from-elasticsearch-and-lucene</guid>
    <category><![CDATA[Investigación en ML]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Julie Tibshirani]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbd7e961f594005e7/6a17d80f033c8d981f6bb009/d240bfef29e9d432069059b312dd044eb76eec6c-1440x840.png" length="0" type="image/png"/>
    <pubDate>Wed, 29 Sep 2021 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>