|
ID 0、1、2 の 3 つのテキスト セグメントがあります。どのテキスト セグメントにキーワード「xiaobai」が含まれているかをすぐに見つける必要があります。 これら 3 つのテキスト セグメントを順に反復処理して、各テキストに「xiaobai」が含まれているかどうかを照合し、最終的に条件を満たすテキスト ID を出力できることは簡単にわかります。 Elasticsearch とは何ですか? Elastic Search (ES とも呼ばれる) は、オープンソースの検索エンジンです。 転置インデックスとは何ですか?冒頭の例に戻ると、テキストを反復処理して「xiaobai」が含まれているかどうかを確認するのは確かに非効率的です。もっと効率的な解決策はあるでしょうか?はい、テキストを分割することができます。例えば、「私はxiaobaiが好きです」というテキストは、「私」、「好き」、「xiaobai」の3つの部分に分割できます。この操作は単語分割と呼ばれ、分割された各部分は用語と呼ばれます。用語とテキストIDの関係を記録することで、上記の3つのテキストセグメントは次のようになります。
「xiaobai」を検索する場合、「xiaobai」という単語を一致させるだけで、すぐに文書ID 0と1を取得できます。しかし、問題があります。たった3つの文に、既に非常に多くの単語が含まれているのです。文書を3つに増やすと、単語の数が途方もなく膨大になってしまいます。これほど多くの単語から「xiaobai」を一致させるにはどうすればよいでしょうか?単語を1つずつ反復処理すると、O(N)という計算量になり、非常に非効率的です。 どうすればいいでしょうか?用語を辞書式に昇順に並べ替え、二分探索法を使って時間計算量をO(lgN)に直接最適化することができます。例えば、次のようになります。
このソートされた用語の集合を「用語辞書」、各用語に対応する文書IDなどの情報の集合を「ポスティングリスト」と呼びます。これらを合わせると、検索に使用されるデータ構造、つまり**転置インデックス**が形成されます。
しかし、転置インデックスには別の問題があります。用語辞書には膨大なデータが含まれており、メモリに保存するのは現実的ではないため、ディスクに配置する必要があります。しかし、ディスクへのクエリは処理速度が遅くなります。最適化の方法はありますか?はい、用語インデックスについてお話ししましょう。 用語インデックスとは何ですか?いくつかの接頭辞は用語間で共有されていることがわかります。例えば、「fo」は「follow」と「forward」で同じ意味です。下の図に示すように、いくつかの用語接頭辞を抽出すると、より少ないスペースでより多くの用語を表現できます。この原理に基づいて、用語辞書からいくつかの用語を抽出し、その接頭辞情報を使用して簡潔なインデックスツリーを構築できます。インデックスツリーのノードには、これらの用語のディスク上のオフセットが格納され、それらの位置を示します。このインデックスツリー構造は小さく、メモリへの保存に適しているため、用語インデックスと呼ばれます。これは検索を高速化するために使用できます。 特定の用語を探す必要がある場合は、用語インデックスを検索するだけで、用語辞書内のその用語のおおよその位置を素早く把握できます。その後、用語辞書に移動し、さらに数回検索するだけで、用語の内容を見つけることができます。 保存されたフィールドとは何ですか?この時点で検索機能は利用可能になります。しかし、問題があります。前述の転置インデックスはドキュメントIDしか取得しません。ユーザーにドキュメントコンテンツを返す前に、このIDを使ってドキュメントコンテンツ自体を検索する必要があります。そのため、ドキュメントコンテンツ全体を保存するための場所が必要になり、ここでStored Fields(行ベースのストレージ)が役立ちます。 Doc Values とは何ですか?ID を使用すると、保存されたフィールドからドキュメント コンテンツを取得できます。 ユーザーは、時間や製品価格など、特定のフィールドに基づいてドキュメントを並べ替える必要があることがよくあります。問題は、これらのフィールドがドキュメント全体に散在していることです。つまり、まず保存フィールドリストにあるドキュメントを取得し、次に並べ替えに必要な内部フィールドを抽出する必要があります。これは不可能ではありませんが、より効率的な方法があります。列指向のストレージ構造を構築することで、スペースを節約し、時間を節約できます。これにより、ドキュメント全体から特定のフィールドを一元管理できるため、単一のフィールドで一度に並べ替えることができます。この列指向のストレージ構造は、Doc Valuesと呼ばれます。 セグメント前回の記事では、検索のための転置インデックス、検索を高速化するための用語インデックス、ドキュメントの元情報を格納するための保存フィールド、そしてソートと集計のためのDoc値という4つの主要な構造を紹介しました。これらの構造は複合ファイル(「セグメント」とも呼ばれます)を形成し、完全な検索機能を備えた最小単位となります。 Luceneとは何ですか?複数のドキュメントからセグメントを生成できます。新しいドキュメントを追加して同じセグメントに書き込む場合、セグメント内の複数のデータ構造を同時に更新する必要があり、同時読み取りと書き込みのパフォーマンスに確実に影響します。では、どうすればよいでしょうか?ルールを設定しましょう。セグメントが生成されたら、それを変更することはできません。新しいドキュメントを書き込む必要がある場合は、新しいセグメントが生成されます。これにより、古いセグメントは読み取りのみを処理し、書き込み用に新しいセグメントが生成されるため、読み取りと書き込みの両方のパフォーマンスが確保されます。 しかし、セグメントの数が増えた場合、検索したいデータがどのセグメントに含まれているかをどのように判断すればよいでしょうか?問題ありません。複数のセグメントを同時に読み取るだけです。 しかし、これにより新たな問題が発生します。データ量が増加するとセグメントファイルの数も増え、ファイルハンドルの不足は避けられなくなります。確かにそうですが、これは簡単に解決できます。複数の小さなセグメントを定期的に1つの大きなセグメントにマージする「セグメントマージ」というプロセスがあります。これにより、ファイル数を適切に制御できます。 この時点で、上記の複数のセグメントは、スタンドアロンのテキスト検索ライブラリ、つまりよく知られたオープンソースの基本検索ライブラリであるLuceneを構成しています。多くの有名な検索エンジンは、本日ご紹介するElasticsearchをはじめ、Lucene上に構築されています。しかし、Luceneはあまりにも初歩的なものであり、高性能、高スケーラビリティ、高可用性といった機能が欠けています。では、Luceneを最適化する方法を見ていきましょう。 高性能Luceneは検索ライブラリとして、大量のデータを書き込むことができ、検索機能を提供します。複数の呼び出し元が同時に同じLuceneインスタンスに読み書きすると、必然的にコンピューティングリソースの競合が発生します。リソースを確保できない呼び出し元は待たなければならず、これは単なる時間の無駄です。解決策はあるのでしょうか?はい!まず、Luceneに書き込まれるデータを分類します。たとえば、スポーツニュースとゴシップニュースのデータは2つのカテゴリに分類し、それぞれにインデックス名を付けます。次に、インデックス名に基づいて新しいLuceneインスタンスの数を増やし、異なるカテゴリのデータを異なるLuceneインスタンスに書き込みます。データの読み取り時には、必要に応じて異なるインデックス名が検索されます。これにより、単一のLuceneインスタンスへの負荷が大幅に軽減されます。 しかし、単一のインデックス名には依然としてデータが多すぎる可能性があります。そのため、単一のインデックス名内の類似データは複数の部分に分割され、それぞれがシャードとなります。各シャードは本質的に独立したLuceneライブラリです。これにより、読み取りおよび書き込み操作を複数のシャードに分散できるため、競合が大幅に減少し、システムパフォーマンスが向上します。 高いスケーラビリティシャードの数が増えると、すべてのシャードが同じマシン上にある場合、単一のマシンでの CPU とメモリの使用量が過剰になり、システム全体のパフォーマンスに影響を及ぼします。 そのため、より多くのマシンをリクエストし、シャードを複数のマシン(それぞれがノード)に分散させることができます。ノードを追加することで、CPUの過剰な使用によるパフォーマンスの問題を軽減できます。 高可用性ここで、別の問題が発生します。1つのノードに障害が発生すると、そのノード内のすべてのシャードが利用できなくなります。高可用性を確保する必要があります。解決策はありますか?はい、シャードにレプリカを追加できます。シャードをプライマリシャードとレプリカシャードに分割できます。プライマリシャードはレプリカシャードにデータを同期します。レプリカシャードは同時に読み取り操作を提供でき、プライマリシャードに障害が発生した場合は、新しいプライマリシャードに昇格してシステムを正常に稼働させることができます。これにより、高可用性を確保しながらパフォーマンスを向上させることができます。 ノードの役割の差別化検索アーキテクチャは、クラスター管理、データの保存と管理、クライアントの検索要求の処理など、多くの機能をサポートする必要があります。すべてのノードがこれらの機能をすべてサポートしている場合、クラスターでデータ逼迫が発生してノードを拡張する必要が生じたときに、これらの他の機能も既存の機能と一緒に拡張されますが、それらの機能が十分で拡張する必要がないため、無駄が生じます。したがって、これらの機能を分離し、クラスター内のノードに役割を割り当て、異なる役割が異なる機能を担当するようにすることができます。たとえば、クラスターの管理を担当するノードはマスターノード、データの保存と管理を担当するノードはデータノード、クライアントの検索クエリ要求の受け入れを担当するノードはコーディネーティングノードと呼ばれます。クラスターが小さい場合、ノードは同時に複数の役割を担うことができますが、クラスターが大きくなるにつれて、各ノードは 1 つの役割しか担えなくなります。 分散化マスターノードという言葉から、マスター選出プロセスが想起されます。しかし、現在各ノードは独立しているため、ノード間のデータ連携を行うメカニズムが必要です。Kafkaに似たZookeeperのような中央ノードの導入は容易に考えられます。しかし、新しいノードを導入したくない場合、より軽量なソリューションは他にないでしょうか?はい、分散化です。ノード間にコーディネーションモジュールを導入し、Raftのようなコンセンサスアルゴリズムを用いてデータを同期することで、すべてのノードが一貫したクラスターデータの状態を参照できるようになります。これにより、ノードはマスター選出プロセスに参加し、ノードに障害が発生したかどうかなどの情報を取得できるようになります。 ESとは何ですか?さて、ここまでで、あの初歩的なLuceneは、高性能、高スケーラビリティ、高可用性、そして永続性を備えた分散検索エンジンへと進化しました。私たちはこれをElastic Search、略してESと呼んでいます。ESはHTTPインターフェースを提供しており、あらゆる言語のクライアントがHTTP経由でESにアクセスし、データのCRUD操作を実行できます。アーキテクチャの観点から見ると、ESはLuceneのような単一マシンシステムを分散システムに変換する方法を示すソリューションを提供します。 この考え方に沿うと、Lucene を改造して、MySQL データベースや、ベクター検索に特化した Faiss のようなスタンドアロンエンジンといった他のスタンドアロンシステムも利用できるようにできるでしょうか?将来、Elastic MySQL や Elastic Faiss が登場しても、それほど驚くには当たらないのではないでしょうか?大企業や次世代の大規模オープンソースプロジェクトにおける熾烈な昇進競争に関する、ちょっとしたヒントは以上です。 Elasticsearchのアーキテクチャを振り返ってみて、見覚えがあると思いませんか?そうです、前回の2回のエピソードで解説したKafkaのことです。 ElasticsearchとKafkaのアーキテクチャの違いKafka のアーキテクチャに馴染みのない方は、以前の記事「Kafka とは何か?」をお読みください。以下の点がわかるでしょう。
Elasticsearchのアーキテクチャは、前回のエピソードで解説したKafkaやRocketMQと非常によく似ています。優れたアーキテクチャはどれも似たようなものであり、醜いアーキテクチャはそれぞれに醜いというのは事実です。優れたアーキテクチャを1つ学ぶことは、複数のミドルウェアシステムの原理を理解することに相当します。これは大きな成果です。 Elasticsearch のアーキテクチャを理解したので、2 つの実用的な例を使用してこれらの概念を結び付け、その仕組みを簡単に見てみましょう。 Elasticsearchの書き込みプロセス
ES検索プロセスElasticsearch の検索プロセスは、クエリフェーズとフェッチフェーズの2つのフェーズに分かれています。それぞれについて見ていきましょう。 問い合わせフェーズ。
取得フェーズ。
皆さんは理解できましたか? 要約
|