<?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/cn/search-labs/blog/category/lucene</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/blog/category/lucene</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/category/lucene.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 16:29:50 GMT</lastBuildDate>
  <item>
    <title><![CDATA[矢量搜索过滤：保持相关性]]></title>
    <description><![CDATA[仅靠矢量搜索来查找与查询最相似的结果是不够的。要缩小搜索结果的范围，通常需要进行筛选。本文介绍了在 Elasticsearch 和 Apache Lucene 中如何对矢量搜索进行过滤。]]></description>
    <content:encoded><![CDATA[<p>矢量搜索不足以找到相关结果。使用过滤标准非常常见，这有助于缩小搜索结果的范围并过滤掉不相关的结果。</p><p>了解筛选在矢量搜索中是如何工作的，将有助于你平衡性能和召回率之间的权衡，并发现一些优化方法，使矢量搜索在使用筛选时性能更佳。</p><h2>为什么要过滤？</h2><p>矢量搜索彻底改变了我们在大型数据集中查找相关信息的方式，使我们能够发现与查询语义相似的项目。</p><p>然而，仅仅找到相似的物品是不够的。我们经常需要根据特定的标准或属性来缩小搜索结果的范围。</p><p>想象一下，您正在一家电子商务商店中搜索产品。纯矢量搜索可能会显示视觉上相似的商品，但您可能还想根据价格范围、品牌、可用性或客户评价进行筛选。如果不进行筛选，您就会看到大量类似的产品，很难准确找到您要找的产品。</p><p>过滤功能可对搜索结果进行精确控制，确保检索到的项目不仅在语义上一致，而且符合所有必要的要求。这将带来更加准确、高效和用户友好的搜索体验。</p><p>这正是 Elasticsearch 和 Apache Lucene 的优势所在--对各种数据类型进行有效过滤是它们与其他矢量数据库的主要区别之一。</p><h2>精确矢量搜索的筛选</h2><p>进行精确矢量搜索主要有两种方法：</p><ul><li><p>为 dense_vector 字段使用<code>flat</code> 索引类型。这使得<code>knn</code> 搜索使用精确搜索而不是近似搜索。</p></li><li><p>使用<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-script-score-query#vector-functions"> script_score 查询</a> ，该 查询 使用向量函数计算分数。这可用于任何索引类型。</p></li></ul><p>在执行精确向量搜索时，所有向量都会与查询进行比较。在这种情况下，过滤将有助于提高性能，因为只需要比较通过过滤的向量。</p><p>这不会影响结果质量，因为所有向量都会被考虑在内。我们只是提前过滤掉不感兴趣的结果，从而减少操作次数。</p><p>这一点非常重要，因为当应用筛选器得到的文档数量很少时，执行精确搜索比近似搜索更有效。</p><p>经验法则是，当通过过滤器的文件少于 10k 时，应使用精确搜索。<a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a>索引的比较速度更快，因此当基于索引的数据少于 100k 时，使用精确搜索是合理的。详情请查看<a href="https://www.elastic.co/search-labs/blog/knn-exact-vs-approximate-search">本博文</a>。</p><p>如果您的筛选器总是限制性很强，您可以考虑使用<code>flat</code> 索引类型而不是基于 HNSW 的索引类型，将索引重点放在精确搜索而不是近似搜索上。更多详情，请参阅<a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-params">index_options 的属性</a>。</p><h2>近似矢量搜索的筛选</h2><p>在执行近似向量搜索时，我们需要用结果的准确性来换取性能。像 HNSW 这样的矢量搜索数据结构可在数百万个矢量上高效搜索近似近邻。它们的重点是通过进行最少的向量比较来检索最相似的向量，而向量比较的计算成本很高。</p><p>这意味着其他过滤属性不属于矢量数据的一部分。不同的数据类型有自己的索引结构，如术语字典、发布列表和 doc 值等，可以有效地查找和过滤这些数据。</p><p>既然这些数据结构与矢量搜索机制是分开的，那么我们如何将过滤功能应用于矢量搜索呢？有两种选择：在矢量搜索后应用过滤器（后过滤）或在矢量搜索前应用过滤器（预过滤）。</p><p>每种方案都各有利弊。让我们深入了解它们！</p><h3>后过滤</h3><p>后过滤在矢量搜索完成后应用过滤器。这意味着，在找到前 k 个最相似的向量结果后，才会应用筛选器。</p><p>显然，在对结果进行筛选后，我们可能会得到少于 k 个结果。当然，我们可以从矢量搜索中获取更多的结果（k 值更高），但我们无法确定在应用过滤器后是否会得到 k 或更多的结果。</p><p>后过滤的优势在于它不会改变矢量搜索的运行时行为--矢量搜索不知道过滤的存在。但是，它确实会改变检索结果的最终数量。</p><p>下面是使用<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query">knn 查询</a>进行后过滤的示例。检查过滤子句是否与 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>使用后置<a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/filter-search-results#post-filter">过滤器</a>还可对 knn 搜索进行后置过滤：</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, 2],
    "k": 5,
    "num_candidates": 50
  },
  "post_filter": {
    "term": {
      "file-type": "png"
    }
  }
}<p>请记住，您需要在 knn 搜索中使用明确的后置过滤器部分。如果不使用后置过滤器，knn 搜索<a href="https://www.elastic.co/docs/solutions/search/vector/knn#_combine_approximate_knn_with_other_features"> 会将最近邻</a> 搜索 结果 与其他查询或过滤器 结合起来 ，而不是进行后置过滤器。</p><h3>预过滤</h3><p>在矢量搜索前应用筛选器将首先检索出满足筛选条件的文档，然后将这些信息传递给矢量搜索。</p><p>Lucene 使用<a href="https://github.com/apache/lucene/blob/7a60d7ce92392181e137361336e5196bd486cdd9/lucene/core/src/java/org/apache/lucene/util/BitSet.java">BitSets</a>高效地存储满足筛选条件的文档。然后，矢量搜索会遍历 HNSW 图，并将满足条件的文档考虑在内。在将候选文件添加到结果中之前，它会检查该候选文件是否包含在有效文件的 BitSet 中。</p><p>不过，即使候选文件不是有效文件，也必须对其进行探索并与查询进行比较。HNSW 的有效性取决于图中向量之间的联系--如果我们停止探索某个候选向量，就意味着我们可能也会跳过它的邻近向量。</p><p>就像开车去加油站一样。如果放弃任何一条没有加油站的道路，您就不可能到达目的地。其他道路可能不是你所需要的，但它们将你<em>连接</em>到目的地。HNSW 图形上的向量也是如此！</p><p>因此，应用预过滤比不应用过滤的性能要低。我们需要对搜索中访问的<em>所有</em>向量进行处理，并丢弃不符合筛选条件的向量。我们正在做更多的工作，花更多的时间来获得最高 K 值的结果。</p><p>下面是在 Elasticsearch 查询 DSL 中进行预过滤的示例。检查过滤子句是否已成为 knn 部分的一部分：</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  }
}<p><a href="https://www.elastic.co/docs/solutions/search/vector/knn#knn-search-filter-example">knn 搜索</a>和<a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-filtering">knn 查询</a>均可使用预过滤功能：</p>{
  "query": {
    "knn": {
      "field": "image-vector",
      "query_vector": [-5, 9, -12],
      "k": 5,
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}<h4>预过滤优化</h4><p>我们可以进行一些优化，以确保预过滤的性能。</p><p>如果筛选条件非常严格，我们可以切换到精确搜索。当需要比较的向量很少时，对满足筛选条件的少数文档进行精确搜索会更快。</p><p>这是<a href="https://github.com/apache/lucene/blob/eb876b618da5d04c1ad14b04a48321638318493a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L218">Lucene</a>和 Elasticsearch 自动应用的优化。</p><p>另一种优化方法是忽略不符合筛选条件的向量。相反，该方法会检查滤波向量的邻近向量是否通过滤波。这种方法不考虑过滤后的向量，而是继续探索与当前路径相连的向量，从而有效减少了比较次数。</p><p>这种算法就是 ACORN-1，<a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">本篇博文</a>将详细介绍其过程。</p><h2>使用文档级安全过滤</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">文档级别安全（DLS）</a>是 Elasticsearch 的一项功能，可指定用户角色可检索的文档。</p><p>DLS 通过查询来执行。一个角色可以有一个与索引相关联的查询，这实际上限制了属于该角色的用户可以从索引中检索的文档。</p><p>角色查询用作过滤器，用于<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">检索与之匹配的文档</a>，并作为 BitSet 缓存。然后，这个 BitSet 会被用来封装底层的 Lucene 阅读器，因此只有从查询返回的文档才会被认为是<em>实时的</em>，也就是说，它们存在于索引中，并且没有被删除。</p><p>由于要<a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L196">从阅读器获取</a>实时文档来执行 knn 查询，因此只考虑用户可用的文档。如果有预检器，DLS 文件将被<a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L204"> 添加到 预检器 中</a> 。</p><p>这意味着，DLS 过滤可以作为近似矢量搜索的预过滤，具有相同的性能影响和优化效果。</p><p>使用精确搜索的 DLS 与应用任何过滤器的好处相同--从 DLS 检索的文档越少，精确搜索的性能就越高。还要考虑 DLS 返回的文档数量--如果 DLS 的作用非常有限，可以考虑使用精确搜索而不是近似搜索。</p><h2>基准</h2><p>在 Elasticsearch，我们希望确保矢量搜索过滤的效率。我们有<a href="https://elasticsearch-benchmarks.elastic.co/#tracks/so_vector/nightly/default/90d">一个专门的向量过滤基准</a>，通过不同的过滤执行近似向量搜索，以确保向量搜索尽可能快地检索到相关结果。</p><p>查看 ACORN-1 推出时的<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=()">改进</a>情况。在只有 2% 个向量通过过滤器的测试中，查询延迟时间缩短到原来的 55% ：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt820cb0b715cf291c/6a17e227dbb4ff8d49fb5615/3eac3748a33376fc97d957364a5c1f5108d5c58b-1023x896.png" alt="" /><h2>结论</h2><p>过滤是搜索不可或缺的一部分。确保过滤在矢量搜索中的性能，并了解权衡和优化，是高效和准确搜索的关键所在。</p><p>过滤会影响向量搜索的性能：</p><ul><li><p>使用过滤功能时，精确搜索速度更快。如果过滤条件足够严格，应考虑使用精确搜索而不是近似搜索。这是 Elasticsearch 的自动优化功能。</p></li><li><p>使用预过滤时，近似搜索速度较慢。通过预过滤，我们可以得到与过滤器匹配的前 k 个结果，但搜索速度会减慢。</p></li><li><p>后过滤并不一定能检索到前 k 个结果，因为在应用过滤器时，这些结果可能已被过滤器过滤。</p></li></ul><p>快乐过滤</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[向量数据库]]></category>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[在 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[加速合并 HNSW 图表]]></title>
    <description><![CDATA[探索我们为降低构建多个 HNSW 图形的开销所做的工作，尤其是降低合并图形的成本。]]></description>
    <content:encoded><![CDATA[<p>过去，<a href="https://www.elastic.co/cn/search-labs/blog/multi-graph-vector-search">我们讨论过</a>搜索多个<a href="https://www.elastic.co/cn/search-labs/blog/hnsw-graph">HNSW 图表所</a>面临的一些挑战，以及我们是如何缓解这些挑战的。当时，我们提到了我们计划进行的一些进一步改进。这篇文章就是这项工作的结晶。</p><p>你可能会问，为什么要使用多图表呢？这是 Lucene 架构选择的副作用：不可变的段。与大多数建筑选择一样，有利也有弊。例如，我们最近对无服务器 Elasticsearch 进行了 GA。在这种情况下，我们从不可变分段中获得了非常显著的优势，包括高效的索引复制以及将索引和查询计算解耦并独立自动扩展的能力。对于矢量量化，分段合并让我们有机会更新参数，使其适应数据特征。按照这种思路，我们认为有机会测量数据特征和重新审视索引选择还有其他好处。</p><p>在这篇文章中，我们将讨论我们为大幅降低构建多个 HNSW 图形的开销，尤其是降低合并图形的成本所做的工作。</p><h3>背景</h3><p>为了保持可管理的分段数量，Lucene 会定期检查是否应该合并分段。这相当于检查当前分段数是否超过目标分段数，目标分段数由基本分段大小和合并策略决定。如果超过该计数，Lucene 会合并片段组，同时违反约束条件。这一过程在<a href="https://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html">其他地方有</a>详细描述。</p><p>Lucene 选择合并大小相似的数据段，因为这样可以实现写入放大的对数增长。就向量索引而言，写入放大是指向量插入图形的次数。Lucene 会尝试以大约 10 个为一组合并数据段。因此，向量插入图的次数大约为 {10}\left (\frac{n}{n_0} \right )次，其中是索引向量数，n 是预期的基本段向量数。由于写入量呈对数增长，即使是庞大的指数，写入放大率也只有个位数。不过，合并图形所花费的总时间与写入放大率成线性比例。</p><p>在合并 HNSW 图形时，我们已经进行了小幅优化：保留最大分段的图形，并将其他分段的向量插入其中。这就是上述 9/10 因素的原因。下面，我们将展示如何通过使用我们正在合并的所有图表中的信息来大幅提高性能。</p><h3>HNSW 图表合并</h3><p>此前，我们保留了最大的图形，并从其他图形中插入矢量，但忽略了包含这些矢量的图形。我们在下文中利用的关键见解是，我们丢弃的每个 HNSW 图形都包含了有关其所含向量的重要邻近性信息。我们希望利用这些信息来加快插入至少部分载体的速度。</p><p>我们重点讨论将较小的图插入较大的图 _l=(V _l, E _l)的问题，因为这是一个原子操作，我们可以用它来构建任何合并策略。</p><p>策略是找到的一个顶点子集，将其插入大图中。然后，我们利用这些顶点在小图中的连通性，加速插入剩余的顶点。在下文中，我们用和分别表示小图和大图中顶点的邻居。具体流程如下</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>我们使用下面讨论的程序来计算集合（第 1 行）。然后，我们使用标准的 HNSW 插入程序将中的每个顶点插入大图中（第 2 行）。对于我们尚未插入的每个顶点，我们都要找到已插入的邻接顶点及其在大图中的邻接顶点（第 4 行和第 5 行）。我们使用<code>FAST-SEARCH-LAYER</code> 程序（第 6 行）作为种子程序，从 HNSW<a href="https://arxiv.org/pdf/1603.09320">论文</a>（第 7 行）中找到<code>SELECT-NEIGHBORS-HEURISTIC</code> 的候选者。实际上，我们在<code>INSERT</code> 方法（论文中的算法 1）中替换了<code>SEARCH-LAYER</code> 来查找候选集，其他方面没有变化。最后，我们将刚刚插入的顶点添加到（第 8 行）。</p><p>很明显，要做到这一点，中的每个顶点都必须在至少有一个邻居。事实上，我们要求对于中的每个顶点，|J\capfor some M，即最大层连接性。我们观察到，在真实的 HNSW 图中，顶点度的分布相当广泛。下图显示了 Lucene HNSW 图表底层顶点度的典型累积密度函数。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc815e1a9c3bb0d06/6a17e178ec0f89308a5a6564/44f001b3a1bc6627e172fed5c52a02cfdcb4cd66-1324x898.png" alt="HNSW 图形：顶点度分布示例" /><p>我们探讨了的固定值以及使其成为顶点度函数的方法。第二种选择会带来更快的速度，而对图形质量的影响却很小，因此采用了以下方案</p><p>请注意，根据定义，|N_s| 等于小图中顶点的度数。下限为 2 意味着我们将插入每个度数小于 2 的顶点。</p><p>一个简单的计数论证表明，如果我们仔细选择 ，我们只需要在 中直接插入大约具体来说，如果我们将一条图边的一个末端顶点恰好插入到，我们就会给这条图边着色。那么我们知道，对于 中 至少有  个 邻居，我们至少需要给 条边着色。此外，我们预计</p><p>这里，_U（left[N_s(U)|\right]）是小图中的平均顶点度。对于每个顶点u\我们最多为 |N_s条边着色。因此，我们期望着色的边的总数最多为 |J|\_U\left[|N_s(U)|\right].我们希望通过仔细选择，使着色的边数接近这一数字，因此，为了覆盖所有顶点，J| 需要满足以下条件</p><p>这意味着 {1}{4}|V_s|=\frac{1}{5} |V_s|。</p><p>如果<code>SEARCH-LAYER</code> 的运行时间占主导地位，这表明我们可以将合并时间最多提高。考虑到写入放大率的对数增长，这意味着即使对于非常大的索引，我们的构建时间通常也只比构建一个图形多一倍。</p><p>这种策略的风险在于会破坏图形质量。我们最初尝试使用无操作程序<code>FAST-SEARCH-LAYER</code> 。我们发现这降低了图表质量，以至于影响了作为延迟函数的召回率，尤其是在合并到单个片段时。然后，我们通过对图形的有限搜索，探索了各种替代方案。最终，最有效的选择是最简单的。使用<code>SEARCH-LAYER</code> ，但<code>ef_construction</code> 要低。通过这种参数设置，我们能够获得质量极佳的图形，同时还能将合并时间平均缩短 30% 多一点。</p><h3>计算连接集</h3><p>寻找一个好的连接集可以表述为一个 HNSW 图覆盖问题。贪婪启发式是一种简单有效的近似最优图覆盖的启发式。我们采用的方法是按增益递减的顺序逐个选取顶点添加到。增益定义如下</p><p>这里，表示向量在中的邻域数，是指示函数。增益包括我们添加到的顶点计数的变化，即 \max，因为我们添加了一个覆盖范围较小的顶点，从而更接近我们的目标。下图展示了中心橙色顶点的增益计算。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7a353d56fa09af5f/6a17e17a2f4a5c33f7fa8825/fe11cd55a7d94d9e0b0f4d47ea309a99075c355d-540x474.png" alt="加入 HNSW 图中连接集 J 的顶点增益" /><p>我们为每个顶点 维护以下状态</p><ol><li><p>是否陈旧、</p></li><li><p>其增益</p></li><li><p>中相邻顶点的计数，用 表示、</p></li><li><p>范围为 [0,1] 的随机数，用于打破平局。</p></li></ol><p>计算连接集的伪代码如下。</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>我们首先在第 1-5 行中初始化状态。</p><p>在主循环的每次迭代中，我们首先提取最大增益顶点（第 8 行），然后随机打破平局。在进行任何更改之前，我们需要检查顶点的增益是否过时。特别是，每次我们将一个顶点添加到，都会影响其他顶点的增益：</p><ol><li><p>由于它的所有邻居在中都多了一个邻居，因此它们的收益会发生变化（第 14 行）</p></li><li><p>如果它的任何一个邻居现在被完全覆盖，其所有邻居的收益都会发生变化（第 14-16 行）</p></li></ol><p>我们以一种懒散的方式重新计算增益，因此只有当我们想要将某个顶点插入，才会重新计算该顶点的增益（第 18-20 行）。由于增益只会减少，我们永远不会错过应该插入的顶点。</p><p>请注意，我们只需跟踪我们添加到的顶点的总增益，就能确定何时退出。此外，当 {exit} 时，至少有一个顶点的增益不为零，因此我们总能取得进展。</p><h3>实施结果</h3><p>我们在四个数据集上进行了实验，这四个数据集涵盖了我们支持的三种距离度量（欧氏、余弦和内积）：</p><ol><li><p>quora-E5-small：522931 个文档，384 个维度，使用余弦相似性、</p></li><li><p>cohere-wikipedia-v2：1M 文档，768 维度，使用余弦相似性、</p></li><li><p>gist：100 万个文档、960 个维度并使用欧氏距离，以及</p></li><li><p>cohere-wikipedia-v3：100 万文档，1024 维度，使用最大内积。</p></li></ol><p>对于每个数据集，我们都会评估两种量化水平：</p><ol><li><p>int8 - 每个维度使用一个 1 字节的整数，而</p></li><li><p>BBQ - 每个维度使用一个比特。</p></li></ol><p>最后，在每个实验中，我们在两个检索深度对搜索质量进行了评估，并在建立索引后和强制合并为单一片段后对搜索质量进行了检查。</p><p>总之，我们在索引和合并方面实现了持续的大幅提速，同时保持了图的质量，因此在所有情况下都能保持搜索性能。</p><h4>实验 1：int8 量化</h4><p>从基线到候选方案（建议的修改）的平均提速为</p><p>索引时间加速：<strong>1.</strong> 次</p><p>强制合并加速：<strong>1.</strong> 次</p><p>运行时间细分如下</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8422be122ef1a9c9/6a17e17bbe6086522e00467d/5f9f6d487b4c74c4998aa47465912c1a9743e928-734x479.png" alt="基线和候选合并策略的索引和合并时间" /><p>为完整起见，确切时间为</p><p></p><p>索引</p><p></p><p>合并</p><p></p><p>数据集</p><p>底线</p><p>候选人</p><p>构建</p><p>候选人</p><p>quora-E5-small</p><p>112.41s</p><p>81.55s</p><p>113.81s</p><p>70.87s</p><p>wiki-cohere-v2</p><p>158.1s</p><p>122.95s</p><p>425.20s</p><p>239.28s</p><p>要领</p><p>141.82s</p><p>119.26s</p><p>536.07s</p><p>279.05s</p><p>wiki-cohere-v3</p><p>211.86s</p><p>168.22s</p><p>654.97s</p><p>414.12s</p><p>下面我们展示了候选方案（虚线）与基线在两种检索深度下的召回率与延迟对比图：多段索引的召回率@10 和召回率@100（我们的默认合并策略在索引所有矢量后的最终结果），以及强制合并为单段后的召回率与延迟对比图。曲线越高、越靠左越好，这意味着在较低的延迟条件下有更高的记忆率。</p><p>正如您所看到的，对于 Cohere v3 数据集来说，候选者的多分段指数更好，而对于所有其他数据集来说，候选者的多分段指数稍差，但几乎不相上下。合并为单一网段后，所有情况下的召回曲线几乎相同。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf674622469bf6a90/6a17e17dfbc5f874a84919c9/9195297f19f90b172d832ba56bdada7b0d8a768b-985x392.png" alt="建立索引后的 10 倍和 100 倍召回率与延迟时间对比" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltde89f7e94ac823f4/6a17e17fe9ea876429a9c507/c866f32d579ae2b841e92ca733dd29c0e214ac6b-986x386.png" alt="合并为单一网段后，10 和 100 的召回率与延迟对比" /><h4>实验 2：烧烤量化</h4><p>从基准线到候选方案的平均加速度为</p><p>索引时间加速：<strong>1.</strong> 次</p><p>强制合并加速：<strong>1.</strong> 次</p><p>运行时间细分如下</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5b0eb305222fa5c2/6a17e18125daab514f08a18c/11be0cf6b63480dc703409329357ea4847b55051-740x415.png" alt="基线和候选合并策略的索引和合并时间" /><p>为完整起见，确切时间为</p><p></p><p>索引</p><p></p><p>合并</p><p></p><p>数据集</p><p>底线</p><p>候选人</p><p>构建</p><p>候选人</p><p>quora-E5-small</p><p>70.71s</p><p>58.25s</p><p>59.38s</p><p>40.15s</p><p>wiki-cohere-v2</p><p>203.08s</p><p>142.27s</p><p>107.27s</p><p>85.68s</p><p>要领</p><p>110.35s</p><p>105.52s</p><p>323.66s</p><p>202.2s</p><p>wiki-cohere-v3</p><p>313.43s</p><p>190.63s</p><p>165.98s</p><p>159.95s</p><p>对于多分段索引，候选者在几乎所有数据集上都更胜一筹，但 cohere v2 除外，基线略胜一筹。就单段指数而言，所有情况下的召回曲线几乎相同。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb12174abeadbcdd1/6a17e1822f4a5c4cc2fa8829/7da1be27bab5a7f5f9c9a1882ea35761a4a0ba5c-973x383.png" alt="建立索引后的 10 倍和 100 倍召回率与延迟时间对比" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf1b88d4f7fec9905/6a17e184414c64a8de9450b4/a26af30a56c55a12addee7e4fb7e8f606f366668-979x386.png" alt="恢复 @10 和 @100 与合并为单一分段后的延迟对比" /><h3>结论</h3><p>本博客中讨论的算法将在即将发布的 Lucene 10.2 以及基于该算法的 Elasticsearch 版本中提供。用户将能利用这些新版本中改进的合并性能和缩短的索引构建时间。这一变更是我们为使 Lucene 和 Elasticsearch 在矢量和混合搜索方面快速高效而不断努力的一部分。</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[向量数据库]]></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[Lucene 中的并发错误：如何修复乐观并发故障]]></title>
    <description><![CDATA[多亏了 CMU PASTA 实验室的确定性并发测试框架 Fray，我们追踪到了一个棘手的 Lucene 错误，并将其解决了]]></description>
    <content:encoded><![CDATA[<p>没错，又是一个修复漏洞的博客。但这一次有一个转折，一位开源英雄突然出现，拯救了世界。 </p><p>调试并发错误并非易事，但我们将着手进行。Fray 是 CMU PASTA 实验室推出的确定性并发测试框架，它能将不稳定的故障转化为可靠的可重现故障。得益于 Fray 聪明的阴影锁设计和精确的线程控制，我们追踪到了一个棘手的 Lucene bug，并最终将其解决。本篇文章将探讨开源英雄和工具如何让并发调试不再痛苦，让软件世界变得更加美好。</p><h2>并发错误：软件工程师的克星</h2><p>并发错误是最糟糕的。它们不仅难以修复，最难的是让它们可靠地失效。以这次测试失败为例，<a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat#testGlobalVersions</code></a> 。它会产生多个文档编写和更新线程，这对 Lucene 的乐观并发模型提出了挑战。该测试暴露了乐观并发控制中的一个竞赛条件。也就是说，文件操作可能会谎称自己是一系列操作中的最新操作😱 。这意味着，在某些条件下，更新或删除操作可能会成功，而在乐观并发约束条件下，该操作本应失败。</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>向那些讨厌 Java 堆栈跟踪的人致歉。注意，删除并不一定意味着 "删除"。它还可以表示文档的 "更新"，因为 Lucene 的段是只读的。
</p><p>Apache Lucene 通过<a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> 类管理每个写入文档的线程。该类将创建或重复使用线程进行文档编写，每个写入操作都在<a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java"><code>DocumentsWriterPerThread</code></a> (DWPT) 类中控制其信息。此外，撰写人还会跟踪<a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> (DWDQ)中删除了哪些文件。这些结构会将所有文档突变操作保存在内存中，并定期刷新，以释放内存资源并将结构持久化到磁盘上。</p><p></p><p>为了防止<a href="https://en.wikipedia.org/wiki/Blocking_(computing)">阻塞线程</a>并确保并发系统的高吞吐量，Apache Lucene 只在非常关键的部分<a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html">进行同步</a>。虽然这在实践中可能很好，但就像任何并发系统一样，也有龙的存在。</p><h2>
一个虚幻的希望</h2><p>初步调查显示，有几个关键部分没有适当同步。与特定<a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> 的所有互动都受其外围<a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> 的控制。因此，虽然在<a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> 中可能没有对单个方法进行适当的同步，但它们对世界的访问是同步的（或应该是同步的）。(我们暂且不去深究它是如何混淆所有权和使用权的--这是一个由众多贡献者共同完成的长期项目。少说两句吧）。</p><p></p><p>不过，我在<a href="https://github.com/apache/lucene/blob/40060f8b7080d06a218518445a0a1dfc520c812a/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java#L572-L576"> 冲洗过程中 发现</a> 有一处 没有同步。</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>这些操作不会同步为一个原子操作。也就是说，在创建<code>newQueue</code> 和调用<code>getMaxSeqNo</code> 之间，其他代码可能执行了<code>documentsWriter</code> 类中的序列号递增。我发现了错误！</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc55033280b45913/6a1706b4dc55dec27fe00d30/3f2617a102d28735e755a7539b047c28beda41be-1600x453.jpg" alt="" /><p>
但是，与大多数复杂的错误一样，找到根本原因并不简单。这时，一位英雄挺身而出。</p><h2>战场上的英雄</h2><p>我们的英雄登场了：<a href="https://aoli.al/">李敖</a>和他在 PASTA 实验室的同事们。我会让他解释他们是如何用 Fray 来拯救世界的。</p><p><a href="https://github.com/cmu-pasta/fray">Fray</a>是由卡内基梅隆大学<a href="https://pastalab.org/">PASTA 实验室</a>的研究人员开发的确定性并发测试框架。构建 Fray 的动机源于学术界和业界之间存在的一个明显差距：确定性并发测试在学术研究中已被广泛研究了 20 多年，但实践者们仍然依赖于压力测试--一种公认为不可靠且不稳定的方法--来测试他们的并发程序。因此，我们希望以通用性和实际应用性为首要目标，设计并实现一个确定性并发测试框架。</p><p></p><h2>核心理念</h2><p>Fray 的核心是利用一个简单而有力的原则：顺序执行。Java 的并发模型提供了一个关键<a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.3">特性--如果</a>程序中没有数据竞赛，那么所有的执行都会在顺序上保持一致。这意味着程序的行为可以用一系列程序语句来表示。</p><p>Fray 以顺序方式运行目标程序：每一步都会暂停除一个线程之外的所有线程，从而使 Fray 能够精确控制线程调度。线程是随机选择的，以模拟并发性，但选择会被记录下来，以便随后进行确定性重放。为了优化执行，Fray 只在线程即将执行同步指令（如锁定或原子/易失性访问）时执行上下文切换。数据竞赛自由的一个很好的特性是，这种有限的上下文切换足以探索任何线程交错导致的所有可观察行为<a href="https://arxiv.org/abs/2501.12618">（我们的论文</a>中有一个证明草图）。</p><p></p><h2>挑战：控制线程调度</h2><p>虽然核心理念看似简单，但实施 Fray 却面临着巨大的挑战。为了控制线程调度，Fray 必须管理每个应用程序线程的执行。乍一看，这似乎很简单--用定制的实现方法取代并发基元。然而，JVM 中的并发控制错综复杂，涉及<a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html">字节码指令</a>、<a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html">高级库</a>和<a href="https://github.com/openjdk/jdk/blob/b720517cb33c2119ec6ed85504bce321de748228/src/java.base/share/classes/java/lang/Object.java#L394">本地方法的</a>混合。</p><p></p><p>结果发现这是一个兔子洞：</p><p></p><ul><li><p>例如，每条<code>MONITORENTER</code> 指令都必须在同一方法中具有相应的<code>MONITOREXIT</code> 。如果 Fray 将<code>MONITORENTER</code> 替换为对存根/模拟的方法调用，它还需要替换<code>MONITOREXIT</code> 。</p></li><li><p>在使用<code>object.wait/notify</code> 的代码中，如果替换了<code>MONITORENTER</code> ，也必须替换相应的<code>object.wait</code> 。该替换链可延伸至<code>object.notify</code> 及更远。</p></li><li><p>JVM 会在本地代码中调用某些与并发相关的方法（例如，当线程结束时，<code>object.notify</code> ）。替换这些操作需要修改 JVM 本身。</p></li><li><p>JVM 功能（如类加载器和垃圾收集 (GC) 线程）也使用并发基元。修改这些基元会导致与这些 JVM 函数不匹配。</p></li><li><p>在 JDK 中替换并发基元往往会导致 JVM 在初始化阶段崩溃。</p></li></ul><p></p><p>这些挑战清楚地表明，全面替换并发基元是不可行的。</p><h2>
我们的解决方案：影子锁设计</h2><p>为了应对这些挑战，Fray 使用了一种新颖的影子锁机制来协调线程执行，而无需替换并发基元。影子锁充当引导线程执行的中介。例如，在获取锁之前，应用线程必须与相应的影子锁交互。影子锁决定线程能否获取锁。如果线程无法继续执行，影子锁会阻止它，并允许其他线程执行，从而避免死锁，实现可控并发。这种设计使 Fray 能够透明地控制线程交错，同时保持并发语义的正确性。每个并发基元都在影子锁框架内进行了仔细建模，以确保合理性和完整性。更多技术细节可参见我们的论文。</p><p></p><p>此外，这种设计还旨在面向未来。通过只要求对并发基元的影子锁进行检测，它可以确保与较新版本的 JVM 兼容。这是可行的，因为 JVM 中并发基元的接口相对稳定，多年来一直保持不变。</p><h2>
测试</h2><p>建立 Fray 之后，下一步就是评估。幸运的是，许多应用程序（如 Apache Lucene）已经包含并发测试。这种并发测试是常规的 JUnit 测试，会产生多个线程，执行一些工作，然后（通常）等待这些线程结束，然后断言一些属性。大多数情况下，这些测试都能通过，因为它们只进行了一次交织。更糟糕的是，如前所述，有些测试只是在 CI/CD 环境中偶尔失败，这使得这些失败极难调试。当我们使用 Fray 执行同样的测试时，我们发现了许多错误。值得注意的是，Fray 重新发现了以前报告过的由于缺乏可靠的重现而一直未修复的错误，包括本博客的重点：<a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat.testGlobalVersions</code></a> 。幸运的是，有了 Fray，我们可以确定性地重放它们，并为开发人员提供详细信息，使他们能够可靠地重现和修复问题。</p><p></p><h2>Fray 的下一步行动</h2><p>我们非常高兴地听到 Elastic 的开发人员说，Fray 对调试并发错误很有帮助。我们将继续开发 Fray，让更多的开发者可以使用它。</p><p>我们的短期目标包括增强 Fray 确定性重放时间表的能力，即使在存在其他非确定性操作（如随机值生成器或使用<code>object.hashcode</code> ）的情况下也是如此。我们还致力于提高 Fray 的可用性，使开发人员无需任何人工干预即可分析和调试现有并发测试。最重要的是，如果您在调试或测试程序中的并发问题时遇到困难，我们很乐意听取您的意见。请随时在<a href="https://github.com/cmu-pasta/fray">Fray Github 代码库中</a>创建问题。</p><p></p><h2>修复并发错误的时候到了</h2><p>多亏了李敖和 PASTA 实验室，我们现在才有了这个测试的可靠失败实例！我们终于可以修好它了关键问题在于<a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThreadPool.java"><code>DocumentsWriterPerThreadPool</code></a> 如何实现线程和资源的重复使用。</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>在这里，我们可以看到每个线程都是参照第 0 代的初始删除队列创建的。</p><p>然后，队列会在刷新时前进，正确查看队列中的前 7 个操作。</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>但是，在所有线程完成冲洗之前，又有两个线程被重新使用，用于处理另一个文件：</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>这将使<code>seqNo</code> 的增量超过假定的最大值，在冲洗过程中计算出的最大值为 7。请注意，<code>_3</code> 和 段的额外<code>numDocsInRAM</code> 。 <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>这样，Lucene 就会错误地考虑冲洗过程中文档操作的顺序，从而导致测试失败。</p><p>就像所有优秀的错误修复一样，实际修复只需<a href="https://github.com/apache/lucene/pull/13627/files">10 行代码</a>。但两位工程师花了好几天才真正弄明白：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71d78f22ce6ec215/6a1706b61949f7bcd0e7a94a/7915e5ba2feba0fb8e846da0af11bfdca470c467-1600x622.jpg" alt="" /><h2>并非所有英雄都穿斗篷</h2><p>是的，这是老生常谈，但却是事实。</p><p></p><p>并行程序调试非常重要。这些棘手的并发错误需要花费大量时间来调试和解决。虽然像 Rust 这样的新语言已经内置了一些机制来帮助防止类似的竞赛条件，但世界上大多数软件都是用<a href="https://www.rust-lang.org/">Rust</a> 以外的语言编写的。Java 经过这么多年的发展，仍然是最常用的语言之一。改进基于 JVM 语言的调试，让软件工程世界更美好。鉴于有些人认为代码将由大型语言模型编写，也许我们工程师的工作最终将只是调试糟糕的大型语言模型代码，而不是调试我们自己的糟糕代码。但是，无论软件工程的未来如何，并行程序调试对于维护和构建软件仍然至关重要。</p><p></p><p>感谢李敖和他的 PASTA 实验室同事们，是你们让这一切变得更加美好。</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 包裹 2024]]></title>
    <description><![CDATA[2024 年是 Apache Lucene 的又一个重要年份。在本博客中，我们将探讨其中的主要亮点。]]></description>
    <content:encoded><![CDATA[<p>2024 年，Apache Lucene 活动频繁，发布了许多版本，包括三年来的首次重大更新，其中包含了令人兴奋的改进和新功能。让我们来探讨其中的一些主要亮点。</p><h2>Lucene&amp; 社区</h2><p>只有得到社区的支持，项目才会强大。尽管经过 20 多年的发展，Lucene 项目仍然充满活力，并在热情和积极的贡献者的帮助下蓬勃发展。</p><p>2024 年，Lucene 项目已收到来自 98 位贡献者的 2000 多条提交和近 800 条拉取请求。贡献者的数量持续增长，新的提交者和项目管理委员会成员不断加入，帮助推动项目取得成功。</p><h2>Lucene 10</h2><p>2024 年，Lucene 10 发布了近 3 年来的首个重要版本，共有 185 位贡献者提交了 2000 多条信息。Lucene 遵循的开发模式允许在次要版本中提供许多改进和功能，而主要版本则提供了带来更多功能和现代化的机会。例如，Lucene 10 至少需要 Java 21。提高最低 Java 版本可确保 Lucene 能够继续利用现代 Java 所提供的改进。</p><p>Lucene 10 的主要重点是更好地利用运行它的硬件。让我们快速浏览一下其中的主要亮点：</p><ul><li><p><strong>更多搜索并行</strong>化--虽然搜索执行已经实现了跨网段并行化，但我们现在更进一步，实现了网段内的并行化。这就将磁盘上的表示与执行性能分离开来，即使是单个片段也能从现代系统的内核数量中获益。</p></li><li><p><strong>更好的 I/O 并行性</strong>--Lucene 使用的直接同步 I/O 模型通过预取阶段得到了增强。这将通知操作系统在不久的将来需要索引文件的一个区域，同时不会阻塞调用线程。</p></li><li><p><strong>利用稀疏索引提高 CPU 和存储效率</strong>--Lucene 10 引入了对稀疏索引的支持，在其他数据存储中，稀疏索引有时被称为主键索引或区域索引。</p></li></ul><p>有关 Lucene 10 的更多信息，请查看 Lucene 10<a href="https://www.elastic.co/search-labs/blog/apache-lucene-10-release-highlights">专文</a>。</p><h2>Lucene 研究与创新</h2><p>2024 年，Lucene 的研究和创新突飞猛进，尤其是在机器学习集成、矢量搜索和大规模数据集优化等领域，共<a href="https://scholar.google.com/scholar?as_ylo=2024&amp;q=lucene&amp;hl=en&amp;as_sdt=0,5"> 发表</a> 了 10 篇独立的 研究论文和出版物 。一些重要的研究领域和发展包括</p><ul><li><p><strong>矢量搜索和嵌入支持</strong>- Lucene 为基于矢量的搜索提供了功能强大且可扩展的解决方案，可实现大规模语义检索。通过利用 Lucene 强大的索引和搜索基础架构，用户可以将传统文本搜索的优点与现代矢量搜索的高级功能相结合，使 Lucene 成为适用于各种搜索和信息检索任务的全面解决方案。</p></li><li><p><strong>混合搜索模型</strong>- 研究还深入到混合搜索技术，Lucene 将传统的基于关键字的搜索与现代的基于向量的检索相结合。通过将基于术语的索引与密集的矢量表示合并，Lucene 可以提供更准确、与上下文更相关的搜索结果，缩小了传统搜索引擎的精确性与语义搜索的灵活性之间的差距。</p></li></ul><p>2024 年正在进行的研究工作表明，Lucene 能够适应现代搜索技术不断发展的需求，特别是在人工智能、语义搜索和大数据应用方面。该项目作为一个功能强大、灵活高效的平台，在传统和前沿搜索应用案例中不断发展壮大。</p><h2>2024 年发布 Lucene</h2><p>尽管这并不能完全反映情况，但发行量之大彰显了社区的持续奉献精神和活力。这些更新包括对向量搜索性能和效率的重大增强、对 madvise 的支持、对张贴列表解码的优化、通过 SIMD 进一步提高速度等等。</p><p>以下是完整的发布清单：</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>(2024-09-28)</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>(2024-06-27)</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>(2024-01-29)</p></li></ul><p>您可以在<a href="https://projects.apache.org/project.html?lucene-core">Lucene Core</a>页面找到更多信息和发布说明。此外，还有相应的<a href="https://projects.apache.org/project.html?lucene-pylucene">PyLucene</a>版本。</p><h2>总结</h2><p>随着 Lucene 日渐成熟，它也因其敬业而充满活力的社区而继续蓬勃发展。正如我们所看到的，2024 年是极其富有成效的一年，现在我们展望 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[Lucene 错误大冒险：修复索引损坏异常]]></title>
    <description><![CDATA[有时，编写一行代码需要花费数天时间。在这里，我们可以一窥一位工程师为修复潜在的 Apache Lucene 索引损坏而历时多日的痛苦和调试过程。]]></description>
    <content:encoded><![CDATA[<h2>做好准备： </h2><p>这个博客与往常不同。这不是对新功能的解释，也不是教程。这就是花了三天时间编写的一行代码。我们将修复一个潜在的 Apache Lucene 索引损坏问题。我希望你们能有一些收获：</p><ul><li><p>只要有足够的时间和合适的工具，所有缺陷测试都是可重复的</p></li><li><p>多层测试是实现稳健系统的关键。然而，测试级别越高，调试和重现的难度就越大。</p></li><li><p>睡眠是一个出色的调试器</p></li></ul><h2>Elasticsearch 如何测试</h2><p>在 Elastic，我们有大量针对 Elasticsearch 代码库运行的测试。有些是简单而集中的功能测试，有些是单节点 "快乐路径 "集成测试，还有一些则试图破坏集群，以确保在故障情况下一切正常。当测试持续失败时，工程师或工具自动化会创建一个 github 问题并标记出来，以便特定团队进行调查。这个<a href="https://github.com/elastic/elasticsearch/issues/105122">特殊的错误是</a>在最后一种测试中发现的。这些测试非常棘手，有时只能在多次运行后才能重复。</p><h2>这项测试究竟在测试什么？</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt58aab468587a7d35/6a17dd8e3e03d74b8d4f2b5f/63268f3b5714ebf9df8070ec2e3c4f0486ed822e-1600x1088.jpg" alt="github issue: https://github.com/elastic/elasticsearch/issues/105122" /><p>这个测试很有意思。它会创建一个特定映射，并将其应用于主分区。然后尝试创建副本。关键区别在于，当副本尝试解析文档时，测试会注入一个异常，从而导致恢复以一种令人惊讶（但在意料之中）的方式失败。</p><p></p><p>然而，一切都在按预期进行，但有一个重要的问题。在测试清理过程中，我们对一致性进行了验证，在此，测试遇到了一个障碍。</p><p>
该测试未能以预期方式失败。在一致性检查过程中，我们将验证所有复制的 Lucene 段文件和主文件是否一致。意思是，未被破坏和完全复制。部分数据或损坏的数据比完全故障更糟糕。以下是故障的可怕简短堆栈跟踪。</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>不知何故，在强制复制失败期间，复制的分片最终损坏了！让我用通俗易懂的语言解释一下错误的关键部分。</p><p></p><p>Lucene 是一种基于段的架构，这意味着每个段都知道并管理自己的只读文件。正在通过其<a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/index/SegmentCoreReaders.java">SegmentCoreReaders</a>验证这一特定网段，以确保一切正常。每个核心阅读器都存储有元数据，可显示特定段落存在哪些字段类型和文件。但是，在验证<a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90PointsFormat.java">Lucene90PointsFormat</a> 时，某些预期文件丢失了。有了<code>_0.cfs</code> 文件段，我们预计会有一个名为<code>kdi</code> 的点格式文件。<code>cfs</code> 代表"复合文件系统" ，Lucene 有时会将所有字段类型和所有小文件合并为一个较大的文件，以提高复制效率和资源利用率。事实上，所有三个点文件扩展名都不见了：<code>kdd</code>、<code>kdi</code> 和<code>kdm</code> 都不见了。我们怎么会出现 Lucene 片段期望找到一个点文件，但它却不见了的情况呢？这似乎是一个可怕的损坏错误！</p><p></p><h2>每个错误修复的第一步是复制它</h2><p></p><p>复制这个特殊错误的失败极其痛苦。在利用 Elasticsearch 中的<a href="https://en.wikipedia.org/wiki/Random_testing">随机值测试的</a>同时，我们确保为每个故障提供一个（希望是）可重现的随机种子，以确保可以对所有故障进行调查。除了由<a href="https://en.wikipedia.org/wiki/Race_condition">竞赛条件</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>无论我尝试多少次，这颗特殊的种子都没有在本地重复失败。但是，有一些方法可以对测试进行锻炼，使失败的重复性更高。</p><p></p><p>我们的测试套件允许通过<code>-Dtests.iters</code> 参数在同一命令中多次运行指定测试。但这还不够，我还需要确保执行线程在切换，从而增加发生竞赛条件的可能性。系统中的另一个问题是测试运行时间太长，测试运行器会超时。最后，我使用下面的噩梦 bash 来重复运行测试：</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><a href="https://github.com/ColinIanKing/stress-ng">压力</a>来了这样，您就可以快速启动一个进程，让 CPU 内核成为您的午餐。在运行失败测试的无数次迭代过程中，随机发送 stress-ng 垃圾邮件，最终让我复制了失败。更近一步要对系统施加压力，只需打开另一个终端窗口并运行</p>stress-ng --cpu 16<h2>
揭示错误</h2><p>

现在，揭示错误的测试失败大多是可重复的，是时候尝试找出原因了。这个特殊测试的奇怪之处在于，Lucene 会抛出问题，因为它期望得到点值，但测试却没有直接添加任何点值。只有文本值。这促使我考虑研究我们的<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html">乐观并发控制</a>字段最近的变化：<code>_seq_no</code> 和<code>_primary_term</code> 。这两者都作为点索引，存在于每个 Elasticsearch 文档中。</p><p></p><p>事实上，我们的<code>_seq_no</code> 映射器确实在<a href="https://github.com/elastic/elasticsearch/pull/105036">提交后</a>发生了变化！是的！这一定是原因！但是，我的兴奋是短暂的。这只是改变了字段添加到文档的顺序。在这一更改之前，<code>_seq_no</code> 字段是最后添加到文档中的。之后，他们先加入。向 Lucene 文档添加字段的顺序不可能导致此故障...</p><p></p><p>没错，更改字段添加顺序导致了故障。这令人惊讶，原来是 Lucene 本身的一个错误！更改字段解析顺序不应改变文档解析行为。</p><p></p><h2>Lucene 中的错误</h2><p>事实上，Lucene 中的错误主要集中在以下条件上：</p><ul><li><p>为点值字段建立索引（例如<code>_seq_no</code>)</p></li><li><p>在分析过程中尝试为文本字段抛出的问题建立索引</p></li><li><p>在这种奇怪的状态下，我们会打开一个来自作者的<a href="https://blog.mikemccandless.com/2011/06/lucenes-near-real-time-search-is-fast.html">近实时阅读器</a>，体验文本索引分析异常</p></li></ul><p>但无论我尝试多少种方法，都无法完全复制。我在整个 Lucene 代码库中直接添加了用于调试的暂停点。我尝试在异常路径中随机打开读者。我甚至打印了数百万兆字节的日志，试图找到发生故障的确切路径。我就是做不到。我花了一整天的时间去战斗，结果却输了。</p><p></p><p>然后我就睡了。</p><p></p><p>第二天，我重新阅读了原始堆栈跟踪，发现了下面一行：</p><p>
</p>    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)<p>在我所有的娱乐尝试中，我从未专门设置过保留合并策略。Elasticsearch 使用<a href="https://github.com/apache/lucene/blob/5f0fa2b291ff9e7d878642f025a70c15b788a470/lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java">SoftDeletesRetentionMergePolicy</a>，这样我们就能在副本中准确地复制删除，并确保我们的所有并发控制都能控制文档的实际删除时间。否则，Lucene 将完全控制并在任何合并时删除它们。</p><p></p><p>一旦我添加了这个策略，并复制了上述最基本的步骤，故障就立即复制了。</p><p>
我从来没有像现在这样高兴地打开<a href="https://github.com/apache/lucene/issues/13353"> Lucene 中的 一个</a> bug 。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89d37c3454ec298b/6a17dd90ec0f8993c75a6511/c7d9ce3de5278923a8454097e2bdf487c861168a-1600x920.jpg" alt="Github 问题 https://github.com/apache/lucene/issues/13353" /><p>
虽然在 Elasticsearch 中它本身是一个竞赛条件，但一旦所有条件都得到满足，在 Lucene 中编写一个可重复失败的测试也很简单。</p><p></p><p>最后，像所有好的 bug 一样，只用一行代码就修复了。多天的工作，只为一行代码。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdab62db3402b27f7/6a17dd92dbb4ff23e7fb55ad/2c10a02eb41da181b1dceee47186c076c9b14a90-1600x539.jpg" alt="一行代码修复" /><p>但这是值得的。</p><h2>
不是终点</h2><p>希望您喜欢和我一起经历这次狂野之旅！编写软件，尤其是像 Elasticsearch 和 Apache Lucene 这样应用广泛且复杂的软件，是一件很有成就感的事情。然而，有时却令人异常沮丧。我对软件既爱又恨。错误修复永远不会结束！</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 与 OpenSearch：向量搜索性能比较]]></title>
    <description><![CDATA[Elasticsearch 开箱即用，在向量搜索方面比 OpenSearch 快 2 倍至 12 倍]]></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 的速度提高了 12 倍</a> - Elastic 收到了来自社区的大量请求，要求澄清 Elasticsearch 和 OpenSearch 之间的性能差异，特别是在语义搜索/向量搜索领域，因此我们进行了这项性能测试，以提供清晰、数据驱动的对比——没有模棱两可之处，只有直接的事实来为我们的用户提供信息。结果表明，<strong>Elasticsearch 在向量搜索方面比 OpenSearch 快 12 倍</strong>，因此所需的计算资源更少。这反映了 Elastic 致力于将 Lucene 打造为搜索和检索用例的最佳向量数据库。</p><p>向量搜索正在彻底改变我们进行相似性搜索的方式，特别是在 AI 和机器学习等领域。随着向量嵌入模型的日益普及，在数百万个高维向量中进行高效搜索的能力变得至关重要。</p><p>在为向量数据库提供支持时，Elastic 和 OpenSearch 采取了显著不同的方法。Elastic 投入巨资优化 Apache Lucene 和 Elasticsearch，使其成为向量搜索应用程序的顶级选择。相比之下，OpenSearch 扩大了其关注范围，整合了其他向量搜索实现，并探索了超出 Lucene 范围的领域。我们对 Lucene 的关注具有战略意义，这使我们能够在 Elasticsearch 版本中提供高度集成的支持，从而形成一个功能更强大的组合，其中每个组件都能相互补充并增强彼此的功能。</p><p>本博客详细比较了 Elasticsearch 8.14 和 OpenSearch 2.14 的不同配置和向量引擎。在这项性能分析中，Elasticsearch 被证明是向量搜索操作的卓越平台，而即将推出的<a href="https://www.elastic.co/search-labs/blog/vector-similarity-computations-ludicrous-speed">功能</a>将更加<a href="https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">显著地</a>扩大差异。与 OpenSearch 相比，它在每个基准轨道上都表现出色——<strong>平均性能提高了 2 倍到 12 倍</strong>。这涉及使用不同的向量数量和维度的场景，包括 <code>so_vector</code>（2M 向量，768D）、<code>openai_vector</code>（2.5M 向量，1536D）和 <code>dense_vector</code>（10M 向量，96D），所有这些都可在<a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance">此存储库</a>中找到，并附有在 Google Cloud 上配置所有所需基础架构的 Terraform 脚本和用于运行测试的 Kubernetes 清单。</p><p>本博客中详述的结果补充了<a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap">之前发布并经过第三方验证的研究</a>的结果。该研究表明，在最常见的搜索分析操作中，Elasticsearch 比 OpenSearch 快 40%–140%；此类操作包括文本查询、排序、范围、日期直方图和术语过滤。现在我们可以添加另一个差异化因素：向量搜索。</p><h2>开箱即用，性能提升高达 12 倍</h2><p>我们在四个向量数据集上的基准测试涉及近似 KNN 和精确 KNN 搜索，考虑了不同的大小、维度和配置，总共进行了 <code>40.189.820</code> 次未缓存的搜索请求。结果：<strong>Elasticsearch 在向量搜索中比 OpenSearch 快 12 倍</strong>，因此需要更少的计算资源。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt34b83c6eba3bcb6e/6a17d727dbb4ff18d6fb54fb/cdb26e91f085b90e9b12aeb8fee53b04d365ecae-1440x1156.webp" alt="p90 平均值" /><p>图 1：Elasticsearch 和 OpenSearch 中不同组合的 ANN 和精确 KNN 的分组任务。</p><p>像 <code>knn-10-100</code> 这样的组表示 KNN 搜索， 和 。在 HNSW 向量搜索中， 决定了查询向量要检索的最近邻居数量。它指定了要找到多少个相似向量。 设置每个段要检索的候选向量数量。更多的候选者可以提高准确性，但需要更多的计算资源。</p><p>我们还测试了不同的量化技术，并利用特定引擎进行了优化。各个轨道、任务和向量引擎的详细结果如下。</p><h2>精确 KNN 和近似 KNN</h2><p>在处理不同的数据集和用例时，向量搜索的正确方法会有所不同。在本博客中，所有标记为 <code>knn-*</code> 的任务，如 <code>knn-10-100</code>，使用 <strong>近似 KNN</strong>，而 <code>script-score-*</code> 指的是 <strong>精确 KNN</strong>，但它们之间有什么区别，为什么它们很重要？</p><p>本质上，如果您处理的是更大规模的数据集，首选方法是近似 K 最近邻 (ANN)，因为它具有更出色的可扩展性。对于可能需要过滤过程的规模较小的数据集，精确 KNN 方法是理想选择。</p><p>精确 KNN 使用暴力方法，计算数据集中一个向量与其他每个向量之间的距离。然后对这些距离进行排序，找出  个最近的邻居。虽然这种方法能确保精确匹配，但对于大型高维数据集来说，它在可扩展性方面面临挑战。然而，在很多情况下，需要使用精确 KNN：</p><ul><li><p><strong>重新评分</strong>：在涉及词汇或语义搜索后进行基于向量的重新评分时，精确 KNN 至关重要。例如，在产品搜索引擎中，可以根据文本查询（例如，关键字、类别）筛选初始搜索结果，然后使用与筛选出的项目相关的向量进行更准确的相似性评估。</p></li><li><p><strong>个性化</strong>：处理大量用户时，如果每个用户都由相对较少数量（例如 100 万）的不同向量表示，则按用户特定的元数据（例如 user_id）对索引进行排序，并使用向量进行暴力评分会变得高效。这种方法能够根据精准的向量比较，根据用户的个人偏好为用户量身定制个性化推荐或内容交付。</p></li></ul><p>因此，精确 KNN 确保了基于向量相似度的最终排名和推荐精准且符合用户偏好。</p><p>另一方面，近似 KNN（或 ANN）采用的方法使得数据搜索比精确 KNN 更快、更高效，尤其是在大型高维数据集中。ANN 并不采用蛮力方法（即测量查询与所有点之间的精确最近距离，从而带来计算和扩展挑战），而是使用某些技术来有效地重构数据集中可搜索向量的索引和维度。虽然这可能会导致轻微的不准确，但它显著提高了搜索过程的速度，使其成为处理大型数据集的有效替代方案。</p><p>在本博客中，所有表述为 <code>knn-*</code> 的任务，例如 <code>knn-10-100</code>，均使用<strong>近似 KNN</strong>，而 <code>script-score-*</code> 指的是<strong>精确 KNN</strong>。</p><h2>测试方法</h2><p>虽然 Elasticsearch 和 OpenSearch 在 BM25 搜索操作的 API 方面相似，但由于后者是前者的分支，向量搜索并非如此，因为它是在分支之后才引入的。在算法方面，OpenSearch 采取了与 Elasticsearch 不同的方法，除了 <code>lucene</code> 之外，还引入了另外两个引擎——<code>nmslib</code> 和 <code>faiss</code>，每个引擎都有其特定的配置和限制（例如，OpenSearch 中的 <code>nmslib</code> 不支持使用筛选器，而筛选器是许多用例的一项基本功能）。</p><p>这三个引擎都使用分层可导航小世界 (HNSW) 算法，该算法对于近似最近邻搜索非常高效，尤其在处理高维数据时表现出色。需要注意的是，<code>faiss</code> 还支持第二种算法 <code>ivf</code>，但由于它需要对数据集进行预训练，因此我们将仅关注 HNSW。HNSW 的核心理念是将数据组织成多层连接图表，每一层代表数据集的不同粒度。搜索从最顶层的粗略视图开始，逐步深入到越来越精细的层级，直至到达最底层。</p><p>这两个搜索引擎在受控环境中的相同条件下进行了测试，以确保测试的公平性。所采用的方法与<a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap#testing-methodology">之前发布的性能比较</a>类似，为 Elasticsearch、OpenSearch 和 Rally 配备了专用节点池。<a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/main/terraform/main.tf">terraform 脚本</a>（与所有源一起）可用于配置具有以下功能的 Kubernetes 集群：</p><ul><li><p>1 个适用于 Elasticsearch 的节点池，包含 3 台 <code>e2-standard-32</code> 计算机（128GB RAM 和 32 个 CPU）</p></li><li><p>1 个 Node 池用于 OpenSearch，配备 3 台 <code>e2-standard-32</code> 机器（128GB RAM 和 32 个 CPU）</p></li><li><p>1 个适用于 Rally 的节点池，包含 2 台 <code>t2a-standard-16</code> 计算机（64GB RAM 和 16 个 CPU）</p></li></ul><p>每个“轨道”（或测试）的每种配置都运行了 10 次，其中包括不同的引擎、不同的配置和不同的向量类型。轨道上的任务会根据轨道的不同重复 1,000 到 10,000 次。如果某个轨道中的某个任务由于网络超时而失败，则所有任务都将被丢弃，因此所有结果都代表了顺利开始并完成的轨道。所有测试结果都经过统计验证，确保改进并非偶然。</p><h2>详细结果</h2><p>为什么要使用第 99 个百分位而不是平均延迟来进行比较？考虑一个假设的例子：某个社区的平均房价。平均价格可能表明一个昂贵的地区，但仔细观察后，可能会发现大多数房屋的价值要低得多，只有少数豪华属性抬高了平均价格。这说明了平均价格如何无法准确代表该地区房屋价值的完整范围。这就好比检查响应时间，平均值可能会掩盖关键问题。</p><h4>任务</h4><ul><li><p>近似 KNN，k:10 n:50</p></li><li><p>使用 k:10 n:100 的近似 KNN</p></li><li><p>近似 KNN，k:100 n:1000</p></li><li><p>使用 k:10 n:50 和关键字筛选器的近似 KNN</p></li><li><p>使用 k:10 n:100 和关键字筛选器的近似 KNN</p></li><li><p>使用 k:100 n:1000 和关键字筛选器的近似 KNN</p></li><li><p>使用 k:10 n:100 结合索引的近似 KNN</p></li><li><p>精确 KNN（脚本分数）</p></li></ul><h4>向量引擎</h4><ul><li><p><code>lucene</code> 在 Elasticsearch 和 OpenSearch 中，版本均为 9.10</p></li><li><p><code>faiss</code> 在 OpenSearch 中</p></li><li><p><code>nmslib</code> 在 OpenSearch 中</p></li></ul><h4>向量类型</h4><ul><li><p><code>hnsw</code> 在 Elasticsearch 和 OpenSearch 中</p></li><li><p><code>int8_hnsw</code> 在 Elasticsearch 中（采用自动 8 位量化的 HNSW：<a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">链接</a>）</p></li><li><p><code>sq_fp16 hnsw </code>在 OpenSearch 中（采用自动 16 位量化的 HNSW：<a href="https://opensearch.org/docs/2.14/search-plugins/knn/knn-vector-quantization#faiss-16-bit-scalar-quantization">链接</a>）</p></li></ul><h4>开箱即用和并行分段搜索</h4><p>您可能知道，Lucene 是一个用 Java 编写的高性能文本搜索引擎库，它是 Elasticsearch、OpenSearch 和 Solr 等许多搜索平台的核心。Lucene 的核心是将数据组织成多个段，这些段本质上是独立的索引，使 Lucene 能够更高效地执行搜索。因此，当您向任何基于 Lucene 的搜索引擎发出搜索请求时，您的搜索最终将在这些段中按顺序或并行执行。</p><p>OpenSearch 将并发分段搜索作为可选标记引入，默认情况下不使用该标记，您必须使用特殊的索引设置 <code>index.search.concurrent_segment_search.enabled</code> 启用该标记，<a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/">此处</a>有详细说明，但有一些<a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/#other-considerations">限制</a>。</p><p>另一方面，Elasticsearch 能以<a href="https://github.com/elastic/elasticsearch/pull/101230">开箱即用的</a>方式并发搜索各网段，因此我们在本博客中进行的比较除了考虑不同的向量引擎和向量类型外，还将考虑不同的配置：</p><ul><li><p>Elasticsearch ootb：开箱即用的 Elasticsearch，支持并行分段搜索；</p></li><li><p>OpenSearch ootb：未启用并行分段搜索；</p></li><li><p>OpenSearch css：启用并发段搜索</p></li></ul><p>现在，让我们深入了解每个已测试的向量数据集的详细结果：</p><h2>250 万个向量，1536 个维度（openai_vector）</h2><p>从最简单的轨道开始，但在维度方面也是最大的，<a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a>——它使用了 <a href="https://huggingface.co/datasets/BeIR/nq">NQ 数据集</a>，并通过使用 OpenAI 的 <a href="https://openai.com/blog/new-and-improved-embedding-model">text-embedding-ada-002 模型</a>生成的嵌入来丰富该数据集。它是最简单的，因为它仅测试近似 KNN，并且只有 5 个任务。它既能独立测试（不进行索引），也能与索引一起测试，并且能使用单个客户端和 8 个同时运行的客户端进行测试。</p><h3>任务</h3><ul><li><p><strong>standalone-search-knn-10-100-multiple-clients</strong>：同时使用 8 个客户端搜索 250 万个向量 (k:10, n:100)</p></li><li><p><strong>standalone-search-knn-100-1000-multiple-clients</strong>：使用 8 个客户端同时搜索 250 万个向量（k: 100，n: 1000）</p></li><li><p><strong>standalone-search-knn-10-100-single-client</strong>：使用单个客户端在 250 万个向量上搜索（k: 10，n: 100）</p></li><li><p><strong>standalone-search-knn-100-1000-single-client</strong>：使用单个客户端在 250 万个向量上搜索（k: 100，n: 1000）</p></li><li><p><strong>parallel-documents-indexing-search-knn-10-100</strong>：在 250 万个向量上搜索，同时索引另外 100,000 个文档 (k:10, n：100)</p></li></ul><p>平均 p99 性能概述如下：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf5791848eb0c12fb/6a17d72925daab9cae08a09e/eea0b2b49c690baada3e09d6968e513bfffe51a9-1440x318.webp" alt="openai_vector 表" /><p>在此我们观察到，在执行索引（即读写）的同时进行向量搜索时，Elasticsearch 比 OpenSearch <strong>快 3 到 8 倍</strong>，而在不进行索引的情况下（:10, :100），速度提高了 <strong>2 倍到 3 倍</strong>，其中 k 和 n 相同。对于 :100 和 :1000（<em>standalone-search-knn-100-1000-single-client</em> 和 <em>standalone-search-knn-100-1000-multiple-clients</em>），Elasticsearch 的平均速度比 OpenSearch 快 <strong>2 到 7 倍</strong>。</p><p>详细结果显示了比较的确切案例和向量引擎：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdbe41d3187ced7ec/6a17d72a445de951c44cff4c/a7a761ed631d3e6211beb83d9d93d752d10123c9-1440x1728.webp" alt="openai_vector" /><h4>召回</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>一千万个向量，96 个维度 (dense_vector)</h2><p>在具有 1000 万个向量和 96 维的 <a href="https://github.com/elastic/rally-tracks/tree/master/dense_vector">dense_vector</a> 中。它基于 <a href="https://big-ann-benchmarks.com/">Yandex DEEP1B</a> 图像数据集。该数据集是从名为 <code>learn.350M.fbin</code> 的“样本数据”文件的前 1000 万个向量创建的。搜索操作使用来自“查询数据”文件查询的向量。<code>public.10K.fbin</code>。</p><p>Elasticsearch 和 OpenSearch 在此数据集上的表现都非常出色，尤其是在<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html">强制合并</a>之后，这通常是在只读索引上进行的，类似于对索引进行碎片整理，使其成为一个单一的“表”以供搜索。</p><h3>任务</h3><p>每个任务预热 100 个请求，然后测量 1000 个请求</p><ul><li><p><strong>knn-search-10-100</strong>：在 1000 万个向量上搜索（k: 10，n: 100）</p></li><li><p><strong>knn-search-100-1000</strong>：在 1000 万个向量上搜索（k：100，n：1000）</p></li><li><p><strong>knn-search-10-100-force-merge</strong>：在强制合并后搜索 1,000 万个向量 (k:10, n:100)</p></li><li><p><strong>knn-search-100-1000-force-merge</strong>：在强制合并后搜索 1000 万个向量 (k: 100, n: 1000)</p></li><li><p><strong>knn-search-100-1000-concurrent-with-indexing</strong>：在 1,000 万个向量上进行搜索，同时更新<a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/challenges/default.json#L76C36-L76C37">数据集的 5%</a> (k:100, n:1000)</p></li><li><p><strong>script-score-query</strong>：对 <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/queries.json">2000 个特定向量</a>进行精确的 KNN 搜索。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4629d06af85eb96c/6a17d72c6864a423e7b685dc/174995e0a2156d86359cdb7aa446dfaae6312ea4-1440x316.webp" alt="dense_vector" /><p>Elasticsearch 和 OpenSearch 在近似 KNN 上表现良好。当索引在 <em>knn-搜索-100-1000-force-merge</em> 和 <em>knn-搜索-10-100-force-merge</em> 中合并（即只有一个段）时，OpenSearch 在使用 <code>nmslib</code> 和 <code>faiss</code> 时表现优于其他，即使它们都在 15 毫秒左右并且都非常接近。</p><p>但是，当索引在 <em>knn-search-10-100</em> 和 <em>knn-search-100-1000</em> 中具有多个分段（这是索引接收其文档更新的典型情况）时，Elasticsearch 将延迟保持在约 7 毫秒和 16 毫秒左右，而所有其他 OpenSearch 引擎则较慢。</p><p>此外，当同时搜索和写入索引时 (<em>knn-search-100-1000-concurrent-with-indexing</em>)，Elasticsearch 将延迟保持在 15 毫秒以下（13.8 毫秒），比开箱即用的 OpenSearch 快近 <strong>4 倍</strong>（49.3 毫秒），并且在启用并发段搜索时仍然更快（17.9 毫秒），但由于过于接近，所以意义不大。</p><p>至于精确 KNN，差距则大得多：Elasticsearch 比 OpenSearch<strong>快 6 倍</strong>（约 260 毫秒对约 1600 毫秒）。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt254f43bcaa3dbfc2/6a17d72ddbb4ffc780fb54ff/17aec6be31117440bc4d1f99984aed95df1c4f6b-1440x1728.webp" alt="dense_vector" /><h4>召回</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>200 万个向量，768 个维度 (so_vector)</h2><p>此<a href="https://github.com/elastic/rally-tracks/tree/master/so_vector">轨道</a> <code>so_vector</code> 源自于 2022 年 4 月 21 日下载的 <a href="https://archive.org/download/stackexchange/stackoverflow.com-Posts.7z">StackOverflow 帖子转储</a>。它仅包含问题文档——所有代表答案的文档都已删除。每个问题的标题都已使用句子转换器模型 <a href="https://huggingface.co/sentence-transformers/multi-qa-mpnet-base-cos-v1">multi-qa-mpnet-base-cos-v1</a> 编码成一个向量。此数据集包含前 200 万个问题。</p><p>与前一个轨道不同，这里的每个文档都包含除向量之外的其他字段，以支持诸如带筛选和混合搜索功能的近似 KNN 等测试功能。此测试中明显缺少适用于 OpenSearch 的 <code>nmslib</code>，<a href="https://opensearch.org/docs/latest/search-plugins/knn/filter-search-knn/#k-nn-search-with-filters">因为它不支持筛选器</a>。</p><h3>任务</h3><p>每个任务会预热 100 个请求，然后测量 100 个请求。请注意，为了简单起见，我们对任务进行了分组，因为测试包含 16 种搜索类型 * 2 个不同的 k 值 * 3 个不同的 n 值。</p><ul><li><p><strong>knn-10-50</strong>：在没有筛选器的情况下搜索 200 万个向量 (k:10, n:50)</p></li><li><p><strong>knn-10-50-filtered</strong>：<a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">使用筛选器</a>在 200 万个向量上进行搜索 (k:10, n:50)</p></li><li><p><strong>knn-10-50-after-force-merge</strong>：在强制合并并使用过滤器后，对 200 万个向量进行搜索（k: 10，n: 50）</p></li><li><p><strong>knn-10-100</strong>：在没有过滤器的情况下搜索 200 万个向量（k: 10，n: 100）</p></li><li><p><strong>KNN-10-100-filtered</strong>：在 200 万个向量上<a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">使用过滤器</a>进行搜索（k: 10，n: 100）</p></li><li><p><strong>knn-10-100-after-force-merge</strong>：在强制合并后，使用筛选器在 200 万个向量上进行搜索 (k:10, n:100)</p></li><li><p><strong>knn-100-1000</strong>：在没有过滤器的情况下搜索 200 万个向量（k:100，n:1000）</p></li><li><p><strong>knn-100-1000-filtered</strong>：<a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">使用筛选器</a>在 200 万个向量上进行搜索 (k:100, n:1000)</p></li><li><p><strong>knn-100-1000-after-force-merge</strong>：在强制合并后使用过滤器在 200 万个向量上搜索（k：100，n：1000）</p></li><li><p><strong>exact-knn</strong>：<a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json#L56">带筛选器和不带筛选器</a>的精确 KNN 搜索。</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8b44e1306af35877/6a17d72f577262aca11bca3e/d4ed2982d55370cad4b2048b23ce97caa55017c0-1440x316.webp" alt="so_vector 表" /><p>在这项测试中，Elasticsearch <strong>始终比 OpenSearch 的开箱即用速度快</strong>，OpenSearch 仅在两种情况下更快，但差距不大（<em>knn-10-100</em> 和 <em>knn-100-1000</em>）。将<em>knn-10-50</em>、<em>knn-10-100</em> 和 <em>knn-100-1000</em> 与筛选器结合使用的任务显示出高达 <strong>7 倍</strong>的差异（112 毫秒对 803 毫秒）。</p><p>执行“强制合并”操作后，两种解决方案的性能似乎趋于平稳，这是可以理解的，从 <em>knn-10-50-after-force-merge</em>、<em>knn-10-100-after-force-merge</em> 和 <em>knn-100-1000-after-force-merge</em> 中可以明显看出这一点。在这些任务中，<code>faiss</code> 的速度更快。</p><p>在精确 KNN 的性能上差异再次明显，这次 Elasticsearch 比 OpenSearch <strong>快 13 倍</strong>（约 385 毫秒对比 5262 毫秒）。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf80ccc7c2df84559/6a17d7314b055de00d43203f/615cb9228eb05ddd2e9512b3a6a5bc88d4088a1a-1440x1440.webp" alt="so_vector" /><h4>召回</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 和 Lucene 明显胜出</h2><p>在 Elastic，我们坚持不懈地对 Apache Lucene 和 Elasticsearch 进行创新，以确保我们能够为搜索和检索用例（包括 RAG (Retrieval-Augmented Generation)）提供一流的向量数据库。我们最近取得的进展极大地提升了性能，使向量搜索比以前<a href="https://search-labs.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">更快、更节省空间</a>，这是在 Lucene 9.10 所取得的成果基础上实现的。这篇博客介绍了一项研究，该研究表明，在比较最新版本时，Elasticsearch 的速度比 OpenSearch 快多达 12 倍。</p><p>值得注意的是，这两款产品都使用相同版本的 Lucene（<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/release-notes-8.14.0.html">Elasticsearch 8.14 发行说明</a> 和 <a href="https://github.com/opensearch-project/OpenSearch/blob/2.14/release-notes/opensearch.release-notes-2.14.0.md">OpenSearch 2.14 发行说明</a>）。</p><p>Elastic 的创新步伐不仅将为我们的本地部署和 Elastic Cloud 客户提供更多服务，还将为使用我们的<a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">无状态平台</a>的客户提供更多服务。在提供对<a href="https://www.elastic.co/search-labs/blog/int4-scalar-quantization-in-lucene">标量量化到 int4</a> 等功能的支持时，我们将进行严格的测试，以确保客户能够在不显著减少召回率的情况下使用这些技术，这与<a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">我们对 int8 的测试</a>类似。</p><p>由于 AI 和机器学习应用程序的普及，向量搜索效率正成为现代搜索引擎中不可或缺的功能。对于寻求能够满足大容量、高复杂性向量数据需求的强大搜索引擎的组织来说，Elasticsearch 是最佳选择。</p><p>无论是扩展已建立的平台还是启动新项目，集成 Elasticsearch 以满足向量搜索需求都是一项战略性举措，将带来切实的长期效益。Elasticsearch 凭借其公认的性能优势，已准备好支撑搜索领域的下一波创新浪潮。</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[向量数据库]]></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[了解 Lucene 中的标量量化]]></title>
    <description><![CDATA[探索 Elastic 如何将标量量化引入 Lucene，包括自动字节量化、按段量化&amp; 性能见解。]]></description>
    <content:encoded><![CDATA[<h2>Lucene 中的自动字节量化</h2><p>虽然 HNSW 是一种强大而灵活的矢量存储和搜索方式，但要快速运行，它确实需要大量内存。例如，查询 1MM float32 向量维）大约需要1,000,000 * 4 * (768 + 12) = 3120000000 字节（约 3GB内存）。一旦开始搜索大量向量，成本就会变得很高。减少使用约内存的一种方法是通过字节量化。Lucene 以及 Elasticsearch 支持矢量索引已有一段时间，但建立这些矢量一直是用户的责任。这种情况即将改变，因为我们在 Lucene 中引入了标量量化。</p><h2>标量量化 101</h2><p>所有量化技术都被视为原始数据的有损转换。这意味着，为了节省篇幅，有些信息会丢失。有关标量量化的深入解释，请参阅：<a href="https://www.elastic.co/search-labs/scalar-quantization-101">标量量化 101</a>在高层次上，标量量化是一种有损压缩技术。通过一些简单的计算，可以节省大量空间，而对召回的影响却很小。</p><h2>探索建筑</h2><p>习惯于使用 Elasticsearch 的人可能已经熟悉了这些概念，但这里还是要简要介绍一下搜索文档的分布情况。</p><p>每个 Elasticsearch 索引都由<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#size-your-shards">多个分片</a>组成。虽然每个分片只能分配给一个节点，但每个索引有多个分片，可以跨节点并行计算。</p><p>每个分区都由一个<a href="https://lucene.apache.org/core/9_8_0/core/org/apache/lucene/index/package-summary.html">Lucene 索引</a>组成。Lucene 索引由多个只读段组成。在索引过程中，文件会被缓冲并定期刷新到只读段中。当满足某些条件时，这些片段可以在后台合并成一个更大的片段。所有这些都是可配置的，也有其自身的复杂性。但是，当我们谈到分段和合并时，我们指的是只读的 Lucene 分段以及这些分段的自动定期合并。<a href="https://www.elastic.co/search-labs/vector-search-elasticsearch-rationale">下面我们将深入探讨</a>分段合并和设计决策。</p><h2>Lucene 中每个分段的量化</h2><p>Lucene 中的每个分段都存储了以下内容：单个向量、HNSW 图表指数、量化向量和计算出的量化值。为简洁起见，我们将重点讨论 Lucene 如何存储量化向量和原始向量。对于每个片段，我们在文件中记录原始矢量，在 文件中记录量化矢量和单个校正乘法浮点，在文件中记录量化的元数据。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1861d0af970fd096/6a17d9887f6f15a45dc099c0/507a4d578f8a5d2225b11d16ac1fc0b7dd39eaa1-1207x228.png" alt=".vec文件" /><p>图 1：原始矢量存储文件的简化布局。由于 数值为 4 字节，因此占用由于我们正在进行量化，因此在 HNSW 搜索时不会加载这些数据。只有在特别要求时才会使用（例如或在段落合并时<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#query-rescorer">重新</a>量化。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8c52b51939357280/6a17d9897b54f9130f8b3761/ffc6e8bffefc5cd36f14aae315184c89e04a6587-1440x207.png" alt=".veq 文件" /><p>图 2：的简化布局锉刀占用空间，并将在搜索过程中加载到内存中。字节用于计算校正乘数浮动，以调整评分，提高准确性和召回率。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt28e007988f81e5c0/6a17d98b25daabe23208a0d9/555cc92d7d179d8c9711cefbaaac3e0b815d2e42-1440x182.png" alt=".vemq 文件" /><p>图 3：元数据文件的简化布局。在这里，我们将跟踪量化和矢量配置，以及计算出的该分段的量化值。</p><p>因此，对于每个片段，我们不仅要存储量化向量，还要存储用于制作这些量化向量和原始向量的量化值。但是，我们为什么还要保留原始矢量呢？</p><h2>与您一起成长的量化</h2><p>由于 Lucene 会定期刷新到只读分段，因此每个分段只能看到所有数据的一部分。这意味着计算出的量化值仅直接适用于整个数据的样本集。如果样本能充分代表整个语料库，这并不是什么大问题。但 Lucene 允许您以各种方式对索引进行排序。因此，您可以对数据进行索引排序，从而增加每个分段量化计算的偏差。此外，您还可以随时刷新数据！您的样本集可能很小，甚至只有一个矢量。另一个扳手是，您可以控制合并发生的时间。虽然 Elasticsearch 已配置了默认值和定期合并，但你可以通过<a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.10/indices-forcemerge.html">_force_merge</a>API 随时要求合并。那么，我们如何在提供良好的量化效果的同时，还能保证所有这些灵活性？</p><p>Lucene 的向量量化会随时间自动调整。由于 Lucene 采用的是只读分段架构，因此我们可以保证每个分段中的数据没有变化，并在代码中明确规定何时可以更新。这意味着在分段合并过程中，我们可以根据需要调整量化值，并可能重新量化向量。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d9a2390958c3e82/6a17d98cbe6086e4fd0045d5/646baa8279719d06c05c1a4fdb80a0c060e0dd94-1440x446.png" alt="多个分段量化" /><p>图 4：具有不同量级的三个示例片段。</p><p>但重新量化不是很贵吗？它确实有一些开销，但 Lucene 会智能地处理量化，只在必要时进行完全量化。让我们以图 4 中的片段为例。让我们给段和段各提供，段只提供。Lucene 会对量化值进行加权平均，如果合并后的量化值与数据段的原始量化值足够接近，我们就不必重新量化该数据段，而会使用新合并的量化值。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte2596c10ee616cf3/6a17d98eec0f8903b85a6497/58fa20d60dc337ca7108eb01e84a07c330e87ee6-1440x447.png" alt="合并的量化值" /><p>图 5：段和段有，而段只有 份文件的合并量化示例。</p><p>在图 5 所示的情况中，我们可以看到合并后的量化值与和 中的原始量化值非常相似。 段，似乎偏差太大。因此，中的向量将根据新合并的量化值重新量化。</p><p>在一些极端情况下，合并后的量化值与任何原始量化值的差异都非常大。在这种情况下，我们将从每个分段中抽取一个样本，然后重新计算量化值。</p><h2>量化性能&amp; 数字</h2><p>那么，它的速度快吗？以下是在<code>c3-standard-8</code> GCP 实例上运行实验收集到的数据。为了确保与进行公平的比较，我们使用了一个足够大的实例来在内存中保存原始向量。我们使用最大内积法索引了 <a href="https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings">Cohere Wiki</a>向量。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfa48e3724ad97fed/6a17d98f445de96aca4cff88/81958f99b8a1397f669db48de00f264cacc6b49f-576x455.png" alt="量化召回" /><p>图 6：量化矢量与原始矢量的 Recall@10。量化矢量的搜索性能明显快于原始矢量，只需再收集 5 个矢量就能迅速恢复召回率； 可见一斑。</p><p>图 6 展示了这个故事。虽然在召回方面存在差异，这是意料之中的，但差异并不大。而且，只要再收集 5 个矢量，召回率的差异就会消失。所有这一切，段合并速度快，内存仅为向量的 1/4。</p><h2>结论</h2><p>Lucene 为难题提供了独特的解决方案。量化不需要 "训练 "或 "优化 "步骤。在 Lucene 中，它可以正常工作。如果数据发生变化，也不必担心需要 "重新训练 "矢量索引。Lucene 会检测重大变化，并在数据生命周期内自动处理这些变化。期待我们将这一功能引入 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[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[实施学术论文：从 Elasticsearch 和 Lucene 中汲取的经验教训]]></title>
    <description><![CDATA[借鉴我们使用 Elasticsearch 和 Lucene 的经验，了解将研究论文纳入软件应用程序的策略。]]></description>
    <content:encoded><![CDATA[<p>本帖分享在软件应用程序中实施学术论文的策略。它借鉴了 Elasticsearch 和 Lucene 的示例，希望能帮助其他工程师学习我们的经验。读到这些策略，你可能会想："但这只是软件开发啊！"事实的确如此：作为工程师，我们已经掌握了正确的方法和工具，只是需要加以调整，以适应新的挑战。</p><h2>背景</h2><p>在开发 Elasticsearch 的过程中，我们偶尔会遇到一个重要问题，但没有简单或既定的解决方法。人们自然会问："嗯，有没有学术论文论述过这个问题？"其他时候，学术工作是灵感的源泉。我们会遇到一篇提出新算法或数据结构的论文，然后想："这一定很有用！"以下是 Elasticsearch 和 Apache Lucene 如何结合学术工作的几个例子：</p><ul><li><p>用于<a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-aggregations-metrics-cardinality-aggregation.html"> 万有引力聚合 的</a><a href="https://research.google/pubs/pub40671/"> HyperLogLog++</a></p></li><li><p>用于<a href="https://www.elastic.co/blog/improving-response-latency-in-elasticsearch-with-adaptive-replica-selection"> 自适应复制选择</a><a href="https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-suresh.pdf"> 的 C3 算法</a></p></li><li><p>用于 Lucene 中最近向量搜索的<a href="https://arxiv.org/abs/1603.09320">层次导航小世界图 (HNSW)</a></p></li><li><p><a href="https://github.com/elastic/ml-cpp/pull/488">改进机器学习分类的</a> <a href="https://jmlr.csail.mit.edu/papers/volume17/15-308/15-308.pdf">MIC 统计</a></p></li><li><p><a href="http://engineering.nyu.edu/~suel/papers/bmw.pdf">块最大 WAND</a>，用于<a href="https://www.elastic.co/blog/faster-retrieval-of-top-hits-in-elasticsearch-with-block-max-wand">在 Lucene 中更快地检索热门话题</a></p></li><li><p>......以及<a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/query-dsl-combined-fields-query.html"> </a><a href="https://github.com/elastic/elasticsearch/blob/b2a9328890b23e7ccf6c66a3b13d6d65e453a3dd/server/src/main/java/org/elasticsearch/search/sort/BucketedSort.java#L302-L323">更多</a></p></li></ul><p>学术论文是工程师开发数据密集型系统的宝贵资源。但是，实施这些算法可能会让人望而生畏，而且容易出错--算法描述往往很复杂，重要的实际细节被省略了。测试是一项真正的挑战：例如，我们如何才能彻底测试输出结果与数据集密切相关的机器学习算法？</p><h2>像评估软件依赖性一样评估论文</h2><p>添加一个新的软件依赖关系需要仔细评估：如果其他软件包不正确、速度慢或不安全，我们的项目也可能如此。在引入依赖关系之前，开发人员一定要对其质量进行评估。</p><p>这同样适用于您正在考虑实施的学术论文。也许有人会认为，论文中发表了一种算法，它就一定是正确的、性能良好的。但是，即使通过了评审程序，学术论文也可能存在问题。也许正确性证明所依赖的假设并不现实。或者，"实验 "部分显示的性能比基线高得多，但这只在特定的数据集上成立。即使论文质量很高，其方法也可能不适合您的项目。</p><p>在考虑是否对学术论文采取 "依赖 "态度时，不妨像对待软件包一样提出同样的问题：</p><ul><li><p>图书馆是否被广泛使用并经过 "实战检验"？→ 其他软件包是否实施了本文，效果如何？</p></li><li><p>是否有性能基准？这些看起来准确和公平吗？→ 论文是否包含现实的实验？它们设计得好吗？</p></li><li><p>性能的提升是否足以证明复杂性的合理性？→ 论文是否与强有力的基线方法相比较？它比基准线高出多少？</p></li><li><p>这种方法能与我们的系统很好地整合吗？→ 算法的假设和权衡是否符合我们的使用情况？</p></li></ul><p>不知为什么，当一个软件包发布与竞争对手的性能比较时，该软件包总是最快的！如果由第三方设计基准，可能会更加平衡。同样的现象也适用于学术论文。如果一种算法不仅在原始论文中表现出色，而且还在其他论文中作为强有力的基线出现，那么它就很有可能是可靠的。</p><h2>创造性地进行测试</h2><p>学术论文中的算法通常比我们日常遇到的算法类型具有更复杂的行为。也许这是一种近似算法，用精确度来换取更快的速度。也可能是一种机器学习方法，它能接收大量数据集，并产生（有时是意想不到的）输出结果。如果我们不能简单地描述这些算法的行为特征，又如何为它们编写测试程序呢？</p><h3>关注不变式</h3><p>在设计单元测试时，我们通常会从示例的角度来思考：如果我们给算法这个示例输入，它就应该有这样的输出。遗憾的是，对于大多数数学算法来说，基于示例的测试并不能充分涵盖它们的行为。</p><p>让我们来看看 C3 算法，Elasticsearch 使用它来确定哪个节点应该处理搜索请求。它使用一个微妙的公式对每个节点进行排名，该公式结合了节点以前的服务和响应时间以及队列大小。测试几个例子并不能真正验证我们对公式的理解是否正确。退一步思考测试不变式：如果服务时间增加，节点的等级是否会降低？如果队列规模为 0，排名是否如论文所说由响应时间决定？</p><p>关注不变式可以帮助解决一些常见问题：</p><ul><li><p>这种方法是否应该与顺序无关？如果是这样，以不同的顺序传递输入数据应该会得到相同的输出结果。</p></li><li><p>算法中的某个步骤会产生类别概率吗？如果是这样，这些概率的总和应为 1。</p></li><li><p>函数是否围绕原点对称？如果是这样，翻转输入的符号应该只是翻转输出的符号。</p></li></ul><p>我们最初实施 C3 时，公式中出现了一个错误，不小心用响应时间的倒数代替了响应时间。这意味着速度较慢的节点可以排名靠前！在修复该问题时，我们<a href="https://github.com/elastic/elasticsearch/pull/70283">确保添加了不变量检查</a>，以防止将来出现错误。</p><h3>与参考实施比较</h3><p>在发表论文的同时，作者还发布了该算法的实施方案。(如果论文中包含实验，这种情况尤其可能发生，因为许多期刊都要求作者发布用于重现结果的代码）。您可以根据该参考实现测试自己的方法，确保没有遗漏算法的重要细节。</p><p>在开发用于最近邻搜索的 Lucene HNSW 实现时，我们<a href="https://issues.apache.org/jira/browse/LUCENE-9937"> 根据</a> 论文作者的 参考库进行了测试 。我们针对相同的数据集运行了 Lucene 和库，比较了其结果的准确性和计算次数。当这些数字紧密匹配时，我们就知道 Lucene 忠实地实现了算法。</p><p>在将算法集成到系统中时，通常需要进行修改或扩展，例如将其扩展到多个内核，或添加启发式算法以提高性能。最好先实施"vanilla" 版本，对照参考进行测试，然后再逐步修改。这样，您就可以确信在进行定制之前已经捕捉到了所有关键部分。</p><h3>与现有算法对决</h3><p>最后一节提出了测试不变式的另一个想法：将算法输出与更简单、更易理解的算法输出进行比较。举例来说，Lucene 中的 block-max WAND 算法可以跳过那些无法出现在顶部结果中的文档，从而加快文档检索速度。我们很难准确描述 block-max WAND 在每种情况下的表现，但我们知道，应用它不应该改变最高结果！因此，我们的测试可以生成多个随机搜索查询，然后在<a href="https://github.com/apache/lucene/blob/main/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java#L669"> 使用和不使用 WAND 优化的情况下运行这些</a> 查询，并检查 它们 的结果是否始终一致。</p><p>这些测试的一个重要方面是，它们会<a href="https://www.elastic.co/blog/elasticsearch-testing-qa-increasing-coverage-randomizing-test-runs"> 生成</a> 用于进行比较的 随机输入 。这可以帮助解决你想不到的情况，并使意想不到的问题浮出水面。例如，Lucene 的 BM25F 评分随机比较测试有助于<a href="https://issues.apache.org/jira/browse/LUCENE-10039">捕捉细微边缘情况下的错误</a>。向算法提供随机输入的想法与<a href="https://en.wikipedia.org/wiki/Fuzzing"> 模糊（fuzzing</a> ）的概念密切相关， 模糊 是计算机安全领域的一种常用测试技术。</p><p>Elasticsearch 和 Lucene 经常使用这种测试方法。如果你看到一个测试提到两个算法之间的"决斗" （TestDuelingAnalyzers、testDuelTermsQuery......），那么你就知道这个策略正在发挥作用。</p><h2>使用论文术语</h2><p>当其他开发人员使用您的代码时，他们需要查阅该文件以了解其细节。<a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/search/aggregations/metrics/HyperLogLogPlusPlus.java#L24-L39">关于 Elasticsearch 的 HyperLogLog++ 实现的评论说</a>得很好："在没有读过论文的情况下就试图理解这个类的作用，是一种冒险"。这种方法的评论也树立了一个好榜样。其中包括学术论文的链接，并重点介绍了对最初描述的算法所做的修改。</p><p>由于开发人员会根据论文来理解代码，因此使用完全相同的术语会很有帮助。由于数学符号是简洁的，这可能导致一些通常不被认为是 "好风格 "的名称，但在论文中却非常清晰。在 Elasticsearch 中，学术论文中的公式是你为数不多能遇到的神秘变量名，如<a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/node/ResponseCollectorService.java#L151">rS 和 muBarSInverse</a>。</p><p>
<em>作者推荐的阅读论文方式：喝大杯咖啡。</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12d81453c706f7cb/6a17d80e0b0bede6badd3424/d03a8e2a50e173b15a4ec3732810c63ff53321f6-1440x1081.jpg" alt="elastic-blog-academicpaper.jpg" /><h2>您可以给作者发送电子邮件</h2><p>在完成一篇难度很大的论文时，你可能会花几个小时来琢磨一个公式，不知道是自己理解错了，还是只是有一个错别字。如果这是一个开源项目，你可以在 GitHub 或 StackOverflow 上提问。但是，您能从哪里获得学术论文呢？作者看起来很忙，可能会因为你的邮件而感到厌烦。</p><p>相反，许多学者喜欢听到他们的想法被付诸实践的消息，并乐于通过电子邮件回答问题。如果您从事的是他们熟悉的产品，他们甚至可能会在网站上列出您的应用！</p><p>学术界使用软件开发中的许多相同工具公开讨论论文的趋势也在不断增长。如果论文附带软件包，您可以<a href="https://github.com/facebookresearch/faiss/issues/1928"> 在 Github 上 找到</a> 常见问题的 答案。Stack Exchange 社区（如 "理论计算机科学 "和 "交叉验证"）也包含<a href="https://cstheory.stackexchange.com/questions/49296/problem-in-the-paper-stable-minimum-space-partitioning-in-linear-time">有关热门论文的详细讨论</a>。一些会议已开始在网上发表所有论文的评论。这些评论包含了与作者<a href="https://openreview.net/forum?id=H1eA7AEtvS">的来回讨论</a>，这些讨论能让人对写作方法产生有益的见解。</p><h2>待续</h2><p>本篇文章主要介绍选择学术论文的基本知识，以及 </p><p>但并不包括实际部署算法的所有方面。例如，如果算法只是复杂系统中的一个组件，我们如何确保对该组件的更改能带来端到端的改进？如果整合算法需要进行大量修改或扩展，而原始论文又没有涵盖这些内容，该怎么办？这些都是我们希望在今后的文章中与大家分享的重要话题。</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[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>