DUICUO

ElasticSearch とは何ですか?どのように機能しますか?

ID 0、1、2 の 3 つのテキスト セグメントがあります。どのテキスト セグメントにキーワード「xiaobai」が含まれているかをすぐに見つける必要があります。

 I like xiaobai (点赞) I follow xiaobai (关注) I forward the video (转发)

これら 3 つのテキスト セグメントを順に反復処理して、各テキストに「xiaobai」が含まれているかどうかを照合し、最終的に条件を満たすテキスト ID を出力できることは簡単にわかります。
データの量が少ない場合は大きな問題ではありませんが、そのようなデータが何千億もあったらどうなるでしょうか?
これらを 1 つずつ繰り返し実行すると、このプロセスの実行には、好きな女性があなたのメッセージに返信するのにかかる時間よりもさらに長い時間がかかります。
膨大なデータからキーワードを使って関連情報を検索するようなシナリオは、オンラインショッピングの際にTaobaoやJD.com内で商品を検索するなど、非常に一般的です。そこで問題となるのは、同様の検索シナリオをどのように処理するかということです。
問題ありません。中間層を追加すれば解決できる問題はありません。もし解決できない問題があれば、別の層を追加してください。
今回追加するミドルウェア層は elasticSearch です。

Elasticsearch とは何ですか?

Elastic Search (ES とも呼ばれる) は、オープンソースの検索エンジンです。
アプリケーションデータの間に位置づけられ、データがElasticsearchに書き込まれると、アプリケーションは特定のキーワードを使ってデータを検索できるようになります。これはBaidu Searchに似た機能です。
では、どのようにそれを実現するのでしょうか? まずは転置インデックスから見ていきましょう。

転置インデックスとは何ですか?

冒頭の例に戻ると、テキストを反復処理して「xiaobai」が含まれているかどうかを確認するのは確かに非効率的です。もっと効率的な解決策はあるでしょうか?はい、テキストを分割することができます。例えば、「私はxiaobaiが好きです」というテキストは、「私」、「好き」、「xiaobai」の3つの部分に分割できます。この操作は単語分割と呼ばれ、分割された各部分は用語と呼ばれます。用語とテキストIDの関係を記録することで、上記の3つのテキストセグメントは次のようになります。

学期

テキストID


0、1、2

のように

0

小白

0、1

フォローする

1

フォワード

2

その

2

ビデオ

2

「xiaobai」を検索する場合、「xiaobai」という単語を一致させるだけで、すぐに文書ID 0と1を取得できます。しかし、問題があります。たった3つの文に、既に非常に多くの単語が含まれているのです。文書を3つに増やすと、単語の数が途方もなく膨大になってしまいます。これほど多くの単語から「xiaobai」を一致させるにはどうすればよいでしょうか?単語を1つずつ反復処理すると、O(N)という計算量になり、非常に非効率的です。

どうすればいいでしょうか?用語を辞書式に昇順に並べ替え、二分探索法を使って時間計算量をO(lgN)に直接最適化することができます。例えば、次のようになります。

学期

ドキュメントID

フォローする

1

フォワード

2


0、1、2

のように

0

その

2

ビデオ

2

小白

0、1

このソートされた用語の集合を「用語辞書」、各用語に対応する文書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 とは何か?」をお読みください。以下の点がわかるでしょう。

  • • es 内のメッセージを分類するために使用されるインデックス名は、実際には Kafka トピックです。
  • • Elasticsearch でインデックス名データを分割するために使用されるシャードは、実際には、Kafka でトピック データを分割するために使用されるパーティションと同じです。
  • • Elasticsearch では、複数のシャードを分散するために使用されるノードは、基本的に Kafka のブローカーに相当します。

Elasticsearchのアーキテクチャは、前回のエピソードで解説したKafkaやRocketMQと非常によく似ています。優れたアーキテクチャはどれも似たようなものであり、醜いアーキテクチャはそれぞれに醜いというのは事実です。優れたアーキテクチャを1つ学ぶことは、複数のミドルウェアシステムの原理を理解することに相当します。これは大きな成果です。

Elasticsearch のアーキテクチャを理解したので、2 つの実用的な例を使用してこれらの概念を結び付け、その仕組みを簡単に見てみましょう。

Elasticsearchの書き込みプロセス

  • • クライアント アプリケーションがデータ書き込み要求を開始すると、その要求はまずクラスター内の調整ノードに送信されます。
  • • コーディネーティングノードは、ハッシュルーティングに基づいてデータを書き込むデータノードとシャードを決定し、プライマリシャードを見つけてデータを書き込みます。シャードの基盤レイヤーはLuceneであるため、データは最終的にLuceneライブラリ内のセグメントに書き込まれ、転置インデックス、保存フィールド、Doc値などの様々な構造にデータが固定化されます。
  • • プライマリ シャードが正常に書き込まれると、データはレプリカ シャードに同期されます。
  • • レプリカ シャードが書き込みを完了すると、プライマリ シャードはコーディネータ ノードに ACK で応答し、書き込みが完了したことを示します。
  • 最後に、コーディネータ ノードはクライアント アプリケーションに応答し、書き込みが完了したことを示します。

ES検索プロセス

Elasticsearch の検索プロセスは、クエリフェーズとフェッチフェーズの2つのフェーズに分かれています。それぞれについて見ていきましょう。

問い合わせフェーズ。

  • クライアント アプリケーションが検索要求を開始すると、その要求はまずクラスター内の調整ノードに送信されます。
  • コーディネータノードは、インデックス名に関する情報に基づいて、インデックス名がいくつのシャードに分割されているか、およびこれらのシャードがどのデータノードに分散されているかを判断し、それらのデータノード上のシャードにリクエストを転送します。
  • 検索リクエストがシャードに到達すると、基盤となるLuceneライブラリは複数のセグメントを並行して検索し、各セグメント内の転置インデックスを使用して対応するドキュメントIDを取得し、これをドキュメント値と組み合わせてソート情報を取得します。その後、シャードは結果を集約し、コーディネーティングノードに返します。
  • コーディネータノードは、複数のシャードから取得したデータを分類して集約し、不要なデータのほとんどを破棄します。

取得フェーズ。

  • コーディネータノードはドキュメントIDを使用してデータノードにシャードを要求します。シャードの基盤となるLuceneライブラリは、セグメント内のストアドフィールドから完全なドキュメントコンテンツを取得し、コーディネータノードに返します。
  • その後、調整ノードはデータの結果をクライアントに返し、検索プロセス全体を完了します。

皆さんは理解できましたか?

要約

  • Luceneは、Elasticsearchの基盤となるスタンドアロンのテキスト検索ライブラリです。複数のセグメントで構成されており、各セグメントは完全な検索機能を備えた最小単位であり、転置インデックス、タームインデックス、保存フィールド、ドキュメント値で構成されています。
  • データは分類され、Elasticsearch 内の異なるインデックス名に保存されます。
  • インデックス名内の過剰なデータを防ぐために、データを分割するシャードの概念が導入され、パフォーマンスが向上しました。
  • 複数のシャードを複数のノードに分散し、必要に応じてノードを拡張してスケーラビリティを向上させます。
  • シャードはプライマリシャードとレプリカシャードに分割されます。プライマリシャードに障害が発生した場合、レプリカシャードが引き継ぎ、システムの可用性を向上させます。
  • ノードの役割を区別することで、システムのパフォーマンスとリソース使用率が向上し、拡張とメンテナンスが簡素化されます。
  • Elasticsearch と Kafka は非常に類似したアーキテクチャを持っているため、類推によって研究することができます。