<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Benjamin Trent - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Benjamin Trent - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/jp/search-labs/author/benjamin-trent</link>
    </image>
    <link>https://www.elastic.co/jp/search-labs/author/benjamin-trent</link>
    <atom:link href="https://www.elastic.co/jp/search-labs/rss/author/benjamin-trent.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[jp]]></language>
    <lastBuildDate>Sun, 20 Sep 2026 14:54:41 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearchでの後期相互作用モデルのスケーリング - パート 2]]></title>
    <description><![CDATA[この記事では、後期相互作用ベクトルを大規模な本番環境のワークロードに適したものにするための技術を探ります。これには、ディスク容量の使用を減らし、計算効率を向上させることが含まれます。]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">前回のColPaliブログ</a>では、Elasticsearchを使ってビジュアル検索アプリケーションを作成する方法を探りました。ColPaliなどのモデルがアプリケーションにもたらす価値に主に焦点を当てましたが、E5などのバイエンコーダーを使ったベクトル検索に比べてパフォーマンス上の欠点があります。</p><p>このブログでは、<a href="https://www.elastic.co/search-labs/blog/elastiacsearch-colpali-document-search">パート1</a>の例を基に、後期相互作用ベクトルを大規模な本番ワークロードに対応させるために、さまざまなテクニックとElasticsearchの強力なベクトル検索ツールキットをどのように使用するかを説明します。</p><p>完全なコード例は<a href="https://github.com/elastic/elasticsearch-labs/tree/main/supporting-blog-content/colpali">GitHub</a>にあります。</p><h2>後期相互作用モデルの課題</h2><p>ColPaliは、インデックス内のドキュメントに対してページごとに1000を超えるベクトルを作成します。</p><p>これにより、後期相互作用ベクトルを扱う際に2つの課題が生じます。</p><ol><li><p>ディスク容量：これらのベクトルをすべてディスクに保存すると、大量のストレージ使用量が発生し、規模が大きくなるとコストが高くなります。</p></li><li><p>計算：ドキュメントのランキングを行う際に、<code>maxSimDotProduct()</code>比較を使用すると、各ドキュメントのすべてのベクトルをクエリのN個のベクトルと比較する必要があります。</p></li></ol><p>これらの問題に対処するためのいくつかの手法を見てみましょう。</p><h2>後期相互作用モデルを最適化する技術</h2><h3>ビットベクトル</h3><p>ディスク容量を減らすために、画像をビットベクトルに圧縮することができます。Pythonの簡単な関数を使用して、マルチベクトルをビットベクトルに変換できます。</p>def to_bit_vectors(embeddings: list) -&gt; list:
    return [
        np.packbits(np.where(np.array(embedding) &gt; 0, 1, 0))
        .astype(np.int8)
        .tobytes()
        .hex()
        for embedding in embeddings
    ]<p>この関数の基本的な概念は単純で、0より大きい値は1に、0より小さい値は0になり、これが0と1の配列となり、それをビットベクトルを表す16進数の文字列に変換するというものです。</p><p>インデックスマッピングでは、<code>element_type</code>パラメータを<code>bit</code>に設定します。</p>mappings = {
    "mappings": {
        "properties": {
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)<p>新しいビットベクトルをすべてインデックスに書き込んだら、次のコードを使ってビットベクトルをランク付けします。</p>query = "What do companies use for recruiting?"
query_vector = to_bit_vectors(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimInvHamming(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltcaf0c8f999d68701/6a17f46696142a6f46eb1c56/51b989446e4099745971e1eac27d147a78d13e0a-1600x480.png" alt="" /><p>精度を少し犠牲にすることで、ハミング距離（<code>maxSimInvHamming(...)</code>）を使用することができます。これにより、ビットマスク、SIMDなどの最適化を活用できます。ビットベクトルとハミング距離について詳しくは<a href="https://www.elastic.co/search-labs/blog/bit-vectors-in-elasticsearch">ブログをご覧ください</a>。</p><p>あるいは、クエリベクトルをビットベクトルに変換せずに、完全な忠実度の後期相互作用ベクトルで検索することもできます。</p>query = "What do companies use for recruiting?"
query_vector = create_col_pali_query_vectors(query)
es_query = {
    "_source": False,
    "query": {
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                    "query_vector": query_vector
                }
            }
        }
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2fa6c89fb451b6e0/6a17f468af47b65da9cde0d9/89f3c795c6dc44b2f7d46801320288316b47e1b2-1600x488.png" alt="ビットベクトルを用いた後期相互作用モデルの最適化結果" /><p>これにより、非対称類似関数を用いてベクトルを比較します。</p><p></p><p>2つのビットベクトル間の通常のハミング距離について考えてみましょう。以下のドキュメントベクトル<em>D</em>と</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd4b6ece1ec4dedac/6a17f4694b055d248143234e/366a70a23e37d403788327b7aefc873fd4482f5e-1235x86.png" alt="" /><p>クエリベクトル<em>Q</em>があると仮定します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7721df9c8f0d84f1/6a17f46b3e03d77cf54f2dcb/978ec31e0afbc0eaebf548a015b628c0cad84625-1247x84.png" alt="" /><p></p><p>シンプルなバイナリ量子化では、ベクトル<em>D</em>を <code>10101101</code>に、<em>Q</em>を<code>11111011</code>に変換します。ハミング距離を見つけるには、直接ビット計算が必要ですが、これは非常に高速です。この場合、ハミング距離は <code>01010110</code>であり、ビット数は4です。したがって、得点はそのハミング距離の逆数になります。類似するベクトルほどハミング距離は小さくなるため、これを反転すると類似するベクトルに高いスコアが付けられるようになります。特にここで、スコアは1/4 = <code>0.25</code>となります。</p><p>ただし、各次元の大きさが失われることに注意してください。<code>1</code>は<code>1</code>です。ですから、<em>Qについては</em>、<code>0.01</code> と<code>0.79</code> の違いはなくなります。単純に<code>&gt;0</code>に従って量子化しているので、Qベクトルが量子化されない小さなトリックを実行できます。これではビット単位の超高速演算はできませんが、Dが量子化されたままなので、ストレージコストは低く抑えられます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blted50315518e2d599/6a17f46c414c6463709452d3/508e8498ba969534d2a0131d431c75735fc27cb9-1399x611.png" alt="" /><p>つまり、これにより<em>Q</em>で提供される情報が保持され、距離推定の品質が向上し、ストレージが小さく抑えられます。</p><p>ビットベクトルを使用することで、ディスク容量とクエリ時の計算負荷を大幅に節約できます。しかし、できることはまだあります。</p><h3>平均ベクトル</h3><p>数十万、数百万のドキュメントにわたって検索をスケールするには、ビットベクトルがもたらすパフォーマンスの恩恵でさえ十分ではないでしょう。このようなタイプのワークロードにスケールするには、ElasticsearchのHNSWインデックス構造をベクトル検索に活用したいと思うでしょう。</p><p>ColpAliはドキュメントごとに約1000ベクトルを生成しますが、これは多すぎ、HNSWグラフに追加することはできません。したがって、ベクトルの数を減らす必要があります。これを行うには、画像を埋め込むときにColPaliによって生成されたすべてのドキュメントベクトルの平均を取得して、ドキュメントの意味の単一の表現を作成します。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc053425fbfcf8de8/6a17f46f148009faf1b488aa/7c3b4dffb70bd95f67deb35f8c01e73c5286c2ab-1476x1102.png" alt="すべての遅延相互作用ベクトルの平均ベクトル" /><p>現時点では、これはElastic自体では不可能であり、Elasticsearchに取り込む前にベクトルを前処理する必要があります。 </p><p>Logstashまたは取り込みパイプラインでこれを行うことができますが、ここでは単純なPython関数を使用します。</p>def to_avg_vector(vectors):
    vectors_array = np.array(vectors)
    
    avg_vector = np.mean(vectors_array, axis=0)
    
    norm = np.linalg.norm(avg_vector)
    if norm &gt; 0:
        normalized_avg_vector = avg_vector / norm
    else:
        normalized_avg_vector = avg_vector

    return normalized_avg_vector.tolist()<p>また、ドット積類似度を使用できるようにベクトルを正規化しています。</p><p>ColPaliのベクトルをすべて平均ベクトルに変換したら、それらをdense_vectorフィールドにインデックスできます。</p>mappings = {
    "mappings": {
        "properties": {
            "avg_vector": {
                "type": "dense_vector",
                "dims": 128,
                "index": True,
                "similarity": "dot_product"
            },
            "col_pali_vectors": {
                "type": "rank_vectors",
                "element_type": "bit"
            }
        }
    }
}

es.indices.create(index=INDEX_NAME, body=mappings)<p>これによって、後期相互作用ベクトルとともにさらに多くの情報が保存されるため、合計ディスク使用量が増加することを考慮する必要があります。さらに、HNSWグラフを保持するために追加のRAMを使用して、何十億ものベクトルにわたって検索をスケールできるようにします。RAMの使用を減らすために、人気の <a href="https://www.elastic.co/search-labs/blog/optimized-scalar-quantization-elasticsearch">BBQ機能</a>を活用できます。その結果、通常では不可能な膨大なデータセットに対して高速な検索結果が得られます。</p><p>knnクエリで検索するだけで最も関連性の高いドキュメントを見つけられるようになりました。</p>query = "What do companies use for recruiting?"
query_vector = to_avg_vector(create_col_pali_query_vectors(query))
es_query = {
    "_source": False,
    "knn": {
        "field": "avg_vector",
        "query_vector": query_vector,
        "k": 10,
        "num_candidates": 100
    },
    "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf4ac6b7fd444f935/6a17f471a29299d76ad02dc0/a500f9907020f032c3b1c7a48ed8c759dc2a1dd4-1600x498.png" alt="" /><p>以前最高だったマッチは残念ながら3位に落ちました。</p><p>この問題を解決するために、多段階の取得を行うことができます。最初の段階では、knnクエリを使って数百万件の文書からクエリに最適な候補を検索します。第2段階では、ColPaliの後期相互作用ベクトルの忠実度が高い上位k（ここでは10）のみを再ランクしています。</p>query = "What do companies use for recruiting?"
col_pali_vector = create_col_pali_query_vectors(query)
avg_vector = to_avg_vector(col_pali_vector)
es_query = {
  "_source": False,
  "retriever": {
    "rescorer": {
      "retriever": {
        "knn": {
          "field": "avg_vector",
          "query_vector": avg_vector,
          "k": 10,
          "num_candidates": 100
        }
      },
      "rescore": {
        "window_size": 10,
        "query": {
          "rescore_query": {
            "script_score": {
              "query": {
                "match_all": {}
              },
              "script": {
                "source": "maxSimDotProduct(params.query_vector, 'col_pali_vectors')",
                "params": {
                  "query_vector": col_pali_vector
                }
              }
            }
          }
        }
      }
    }
  },
  "size": 5
}<img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt265edd5f37be40b7/6a17f4733e9e45583dba15e6/797f490860af87fcf29f34668a5c4511419c6fd0-1600x501.png" alt="後期相互作用モデルを最適化するための平均ベクトル使用の結果" /><p>ここでは、8.18で導入された<a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.18/retriever.html#rescorer-retriever">リスコアリトリーバー</a>を使用して結果を再ランク付けしています。再採点後、ベストマッチが再び1位になったことがわかります。 </p><p>注：本番環境では、maxSim関数が以前として相対的に高パフォーマンスであるため、10よりはるかに高いkを使うことができます。</p><h3>トークンプーリング</h3><p>トークンプーリングは、白い背景のパッチなどの冗長な情報をプールすることで、マルチベクトル埋め込みのシーケンスの長さを短縮します。この技術により、ページのシグナルの大部分を維持しながら埋め込みの数を減らすことができます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt3d643e6ac908f640/6a17f4754b055d3783432352/09eae0b768f4b450e555d7e53e57561bd52de2c0-1412x1056.png" alt="後期相互作用モデルの最適化のためのトークンプーリング" /><p>トークンプーリングは、クラスタリングアルゴリズムを使用して、ドキュメント内の類似したトークン埋め込みをクラスターにグループ化することによって機能します。次に、各クラスターのベクトルの平均を計算して、単一の集約表現を作成します。この集約されたベクトルはグループ内の元のトークンを置き換え、ドキュメントシグナルを大幅に失うことなくベクトルの合計数を削減します。</p><p>ColPaliの論文では、ほとんどのデータセットに対して初期プールファクター値を3とすることを提案しており、これにより元のパフォーマンスの97.8%を保持しつつ、ベクトルの総数を66.7%削減しています。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt2d78a15c6944a1ea/6a17f477414c64a2929452d9/343cc9eaf54af8c7a125d4115838f3c7d5659de0-1600x1007.png" alt="後期相互作用モデル最適化のためのプール係数" /><p>しかし、注意も必要です。非常に密度が高く、テキストが多く、余白がほとんどない「Shift」データセットは、プールファクターが増加するにつれてパフォーマンスが急速に低下します。</p><p>プールされたベクトルを作成するには、colpali_engineライブラリを利用できます。</p>from colpali_engine.compression.token_pooling import HierarchicalTokenPooler

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

def pool_vectors(embedding: list) -&gt; list:
    tensor = torch.tensor(embedding).unsqueeze(0)
    pooled = pooler.pool_embeddings(tensor)
    return pooled.squeeze(0).tolist()<p>これで、次元が約66.7%削減されたベクトルが得られました。通常通りにインデックス化し、<code>maxSimDotProduct()</code>機能で検索することができます。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc460c14031814ede/6a17f4794202292bf629f72d/2412c5db7d79a590b96d42fe01f140c42f010612-1600x481.png" alt="後期相互作用モデルの結果" /><p>結果の精度が若干犠牲になりますが、良好な検索結果を得ることができます。</p><p>ヒント：pool_factorを高くすれば（100-200）、平均的なベクトルソリューションとここで説明したソリューションの中間を取ることもできます。ドキュメントあたり5-10ベクトル程度であれば、HNSWインデックスを活用するためにネストされたフィールドでインデックスを作成することが可能になります。</p><h2>クロスエンコーダー、後期相互作用とバイエンコーダーの比較</h2><p>これまでに学んだことを踏まえて、ColPaliやColBERTなどの後期相互作用モデルを他のAI検索手法と比較すると、どのような位置づけになるでしょうか。</p><p>maxSim関数はクロスエンコーダーと比較して安価ですが、クエリとドキュメントのペアごとに2つのベクトルを比較するだけのバイエンコーダーを使用したベクトル検索よりも、依然として多くの比較と計算を必要とします。 </p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbfd97f3bba3e5b17/6a17f47be31791b0052d5943/75e1fc9e601aa7a6e88f137565c1919166c7a71c-1480x458.png" alt="クロスエンコーダー、後期相互作用モデルとバイエンコーダーの比較" /><p>このため、後期相互作用モデルについては、一般的にトップkの検索結果のリランキングにのみ使用することを推奨します。また、フィールドタイプの名前であるrank_vectorsにもこれを反映しています。</p><p>では、クロスエンコーダーはどうでしょうか。クエリ時に実行するコストが安いため、後期相互作用モデルの方が優れているのでしょうか。これもまた、状況によります。クロスエンコーダーは一般的に高品質な結果を生成しますが、クエリとドキュメントのペアが変換器モデルを通して完全に処理する必要があるため、多くの計算リソースを必要とします。また、ベクトルのインデキシングを必要とせず、ステートレスな方法で動作できるという利点もあります。その結果は次のようになります。</p><ul><li><p>使用ディスク容量の削減</p></li><li><p>よりシンプルなシステム</p></li><li><p>検索結果の品質向上</p></li><li><p>レイテンシが高いため、深いリランキングが困難</p></li></ul><p>一方、後期相互作用モデルでは、この計算の一部をインデックス時にオフロードできるため、クエリのコストが削減されます。その代償として、ベクトルをインデックスする必要があり、インデックスパイプラインがより複雑になり、これらのベクトルを保存するためにより多くのディスク領域が必要になります。</p><p>特にColPaliの場合、画像には大量のデータが含まれているため、画像からの情報の分析は非常に高価です。この場合、クエリ時にこの情報を評価するとリソースが大量に消費され、処理が遅くなるため、トレードオフはColPaliなどの後期相互作用モデルを使用する方に傾きます。 </p><p>ColBertのように、ほとんどのクロスエンコーダーのようにテキストデータを処理する後期相互作用モデル（例：elastic-rerank-v1）では、ディスクの節約とシンプルさのメリットを享受するためにクロスエンコーダーを使用する方が有利になる可能性があります。</p><p>ユースケースに合わせてこれらの長所と短所を比較検討し、最適な検索アプリケーションを構築するためにElasticsearchが提供するさまざまなツールを試してみることをお勧めします。</p><h2>まとめ</h2><p>このブログでは、ColpAliのような後期相互作用モデルをElasticsearchの大規模ベクトル検索用に最適化するさまざまな手法を紹介しました。後期相互作用モデルは検索効率とランキング品質の間の強力なバランスを実現しますが、ストレージと計算に関連する課題ももたらします。</p><p>これらの課題に対処するために、私たちは次の点を検討しました。</p><ul><li><p>ハミング距離や非対称最大類似度などの効率的な類似度計算を活用しながら、ディスク容量を大幅に削減する<strong>ビットベクトル</strong>。</p></li><li><p>複数の埋め込みを単一の密な表現に圧縮する<strong>平均ベクトル</strong>。これにより、HNSW インデックスによる効率的な検索が可能になります。</p></li><li><p>冗長な埋め込みを賢明にマージしながら意味の整合性を保守し、クエリ時の演算処理の負担を軽減する<strong>トークンプーリング</strong>。</p></li></ul><p>Elasticsearchは、ニーズに基づいて検索アプリケーションをカスタマイズおよび最適化するための強力なツールキットを提供します。検索速度、ランキング品質、ストレージ効率のどれを優先するかに関係なく、これらのツールとテクニックを使用すると、実際のアプリケーションのニーズに応じてパフォーマンスと品質のバランスをとることができます。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/late-interaction-model-colpali-scale</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/late-interaction-model-colpali-scale</guid>
    <category><![CDATA[関連性]]></category>
    <category><![CDATA[Vector Database]]></category>
    <dc:creator><![CDATA[Peter Straßer,Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt97a536033e6b0a56/6a17f47dfbc5f88c86491c0d/c780b78a07573f2df1cfef8b29a7109f839b0ab3-1200x628.png" length="0" type="image/png"/>
    <pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Lucene の同時実行バグ: 楽観的同時実行の失敗を修正する方法]]></title>
    <description><![CDATA[CMUのPASTAラボの決定論的並行性テストフレームワークであるFrayのおかげで、私たちはLuceneの厄介なバグを追跡し、それを潰すことができました。]]></description>
    <content:encoded><![CDATA[<p>はい、またバグ修正のブログです。しかし、この事件には意外な展開があり、オープンソースのヒーローが飛び込んできて事態を収拾するのです。 </p><p>並行性のバグをデバッグするのは簡単ではありませんが、ここで取り上げます。CMU の PASTA ラボによる決定論的同時実行テスト フレームワークである Fray は、不安定な障害を確実に再現可能な障害に変えます。Fray の巧妙なシャドウ ロック設計と正確なスレッド制御のおかげで、私たちは厄介な Lucene のバグを追跡し、ついにそれを撲滅することができました。この投稿では、オープンソースのヒーローとツールが、並行処理のデバッグの負担を軽減し、ソフトウェアの世界を大きく改善している仕組みについて説明します。</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">フラッシュ中に同期されていない場所が 1 か所</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>私たちのヒーロー、PASTA ラボの<a href="https://aoli.al/">Ao Li</a>と彼の同僚の登場です。フレイでどうやって彼らが危機を救ったのか、彼に説明してもらいましょう。</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 は、ターゲット プログラムを順次実行することで動作します。各ステップで、1 つを除くすべてのスレッドを一時停止し、スレッドのスケジュールを正確に制御できるようにします。同時実行をシミュレートするためにスレッドはランダムに選択されますが、選択内容は後続の確定的な再生のために記録されます。実行を最適化するために、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>クラス ローダーやガベージ コレクション (GC) スレッドなどの JVM 機能でも、同時実行プリミティブが使用されます。これらのプリミティブを変更すると、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 テストです。ほとんどの場合、これらのテストは 1 つのインターリーブのみを実行するため合格します。さらに悪いことに、前述のように、一部のテストは CI/CD 環境でたまにしか失敗しないため、これらの失敗をデバッグするのは非常に困難です。同じテストを Fray で実行したところ、多数のバグが発見されました。特に、Fray は、このブログの焦点である<a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat.testGlobalVersions</code></a>を含め、信頼できる再現がないため未修正のまま残っていた、以前に報告されたバグを再発見しました。幸いなことに、Fray を使用すると、それらを確定的に再生して開発者に詳細な情報を提供できるため、問題を確実に再現して修正することができます。</p><p></p><h2>フレイの次のステップ</h2><p>Elastic の開発者から、Fray が並行性バグのデバッグに役立ったという話を聞き、大変嬉しく思っています。私たちは、より多くの開発者が Fray を利用できるようにするために、引き続き取り組んでいきます。</p><p>私たちの短期的な目標には、乱数ジェネレータや<code>object.hashcode</code>の使用など、他の非決定論的な操作が存在する場合でも、スケジュールを決定論的に再生する Fray の機能を強化することが含まれます。また、Fray の使いやすさを向上させ、開発者が手動介入なしに既存の同時実行テストを分析およびデバッグできるようにすることを目指しています。最も重要なことは、プログラム内の同時実行の問題のデバッグやテストで課題に直面している場合は、ぜひご連絡ください。遠慮なく、 <a href="https://github.com/cmu-pasta/fray">Fray Github リポジトリ</a>に問題を投稿してください。</p><p></p><h2>同時実行バグを修正する時間</h2><p>Ao Li と 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>しかし、すべてのスレッドがフラッシュを完了する前に、2 つのスレッドが追加のドキュメントに再利用されます。</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>です。しかし、実際に理解するまでに 2 人のエンジニアが数日かかりました。</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 は、何年も経った今でも、最も使用されている言語の 1 つです。JVM ベースの言語でのデバッグを改善することで、ソフトウェア エンジニアリングの世界がより良くなります。また、一部の人々がコードは大規模言語モデルによって記述されると考えていることを考えると、エンジニアとしての私たちの仕事は、最終的には自分自身の悪いコードだけでなく、悪い LLM コードをデバッグすることだけになるかもしれません。しかし、ソフトウェア エンジニアリングの将来がどうであろうと、並行プログラムのデバッグはソフトウェアの保守と構築にとって重要なままです。</p><p></p><p>これをさらに素晴らしいものにしてくれた PASTA ラボの Ao Li 氏と同僚の皆さんに感謝します。</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 のバグアドベンチャー: 破損したインデックス例外の修正]]></title>
    <description><![CDATA[場合によっては、1 行のコードを書くのに数日かかることがあります。ここでは、Apache Lucene インデックスの潜在的な破損を修正するためにエンジニアが数日間にわたって苦労してデバッグする様子を垣間見ることができます。]]></description>
    <content:encoded><![CDATA[<h2>準備しておきましょう: </h2><p>このブログはいつもと違います。新しい機能の説明やチュートリアルではありません。これは、記述に 3 日かかった 1 行のコードです。Apache Lucene インデックスの潜在的な破損を修正します。皆さんが理解して頂けるよう、いくつかのポイントを挙げておきます。</p><ul><li><p>十分な時間と適切なツールがあれば、すべての不安定なテストは再現可能である</p></li><li><p>堅牢なシステムには、多層的なテストが重要です。ただし、テストのレベルが上がるにつれて、デバッグと再現がますます難しくなります。</p></li><li><p>Sleepは優れたデバッガーです</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の問題: https://github.com/elastic/elasticsearch/issues/105122" /><p>この特定のテストは興味深いものです。特定のマッピングを作成し、それをプライマリ シャードに適用します。次にレプリカを作成しようとします。主な違いは、レプリカがドキュメントを解析しようとすると、テストによって例外が挿入され、その結果、予期しない (しかし予想どおりの) 方法で回復が失敗するという点です。</p><p></p><p>すべては期待通りに機能していましたが、1つ大きな問題がありました。テストのクリーンアップ中に一貫性を検証したところ、このテストで問題が発生しました。</p><p>
このテストは予想どおりに失敗しました。整合性チェック中に、複製された Lucene セグメント ファイルとプライマリ 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 は、より効率的なレプリケーションとリソース利用のために、すべてのフィールド タイプとすべての小さなファイルを 1 つの大きなファイルに結合することがあります。実際、ポイント ファイル拡張子<code>kdd</code> 、 <code>kdi</code> 、 <code>kdm</code>の 3 つすべてが欠落していました。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>確かに<a href="https://github.com/elastic/elasticsearch/pull/105036">コミットによって</a><code>_seq_no</code>マッパーが変更されました。はい！原因はきっとこれだ！しかし、私の興奮は長くは続かなかった。これにより、ドキュメントにフィールドが追加される順序のみが変更されました。この変更の前は、 <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>これまでの再現の試みにおいて、私は保持マージポリシーを具体的に設定したことはありません。<a href="https://github.com/apache/lucene/blob/5f0fa2b291ff9e7d878642f025a70c15b788a470/lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java">SoftDeletesRetentionMergePolicy</a>は Elasticsearch によって使用され、レプリカ内の削除を正確に複製し、ドキュメントが実際に削除されるタイミングをすべての同時実行制御が管理できるようにします。それ以外の場合、Lucene は完全な制御権を持ち、マージ時にそれらを削除します。</p><p></p><p>このポリシーを追加し、上記の最も基本的な手順を再現すると、障害はすぐに再現されました。</p><p>
<a href="https://github.com/apache/lucene/issues/13353">Lucene でバグを</a>発見してこれほどうれしかったことはありません。</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>結局、すべてのバグと同様に、たった 1 行のコードで修正されました。たった 1 行のコードのために、数日間の作業が必要でした。</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdab62db3402b27f7/6a17dd92dbb4ff23e7fb55ad/2c10a02eb41da181b1dceee47186c076c9b14a90-1600x539.jpg" alt="1行のコード修正" /><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[Luceneにおけるスカラー量子化の理解]]></title>
    <description><![CDATA[自動バイト量子化、セグメントごとの量子化、パフォーマンス分析など、Elastic が Lucene にスカラー量子化をどのように導入したかをご覧ください。]]></description>
    <content:encoded><![CDATA[<h2>Luceneにおける自動バイト量子化</h2><p>HNSW はベクトルを保存および検索するための強力かつ柔軟な方法ですが、高速に実行するには大量のメモリが必要です。たとえば、768 次元の 1MM float32 ベクトルをクエリするには、およその RAM が必要です。大量のベクトルを検索し始めると、コストが高くなります。メモリ使用量を約削減する方法の 1 つは、バイト量子化を使用することです。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>で構成されます。各シャードは 1 つのノードにのみ割り当てることができますが、インデックスごとに複数のシャードを割り当てることで、ノード間で並列計算が可能になります。</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 バイトなので、 dimension量子化しているため、これらは 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 ではさまざまな方法でインデックスを並べ替えることができます。したがって、セグメントごとの分位数計算にバイアスを追加する方法でソートされたデータをインデックス化することができます。また、いつでも好きなときにデータをフラッシュできます。サンプル セットは、たとえ 1 つのベクトルだけでも非常に小さい可能性があります。さらにもう一つの問題は、マージがいつ発生するかを制御できることです。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: 異なる分位数を持つ 3 つのセグメントの例。</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>量子化性能と数値</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>
  </channel>
</rss>