アップデート2020-02-18
私はこの質問に再び出くわし、受け入れられた答えは同じままですが、今日それを最適化する方法を共有することを考えました(ここでもサードパーティのライブラリやツールを使用せずに-つまり、元の質問で述べたように最初から車輪を再発明します) 。
このシステムを簡素化および最適化するために、ドメインロジック層で「転置インデックスマップ」の代わりにTrie(プレフィックスツリー)を使用し、 「テーブルクエリ」 SQLテーブルの悪い習慣を完全に破棄します。例を挙げて説明します。
- アプリの一部のユーザーがすでにデータベースにいくつかのオブジェクトを追加したとします:w、wo、woo、wood、woodx。
- これらのオブジェクト(文字列/ラベル)はメモリ内のTrieによって表され、各Trieノードには、ツリー内のレベル(Think Associative Array)でそのオブジェクトのデータベース発行IDが含まれます。
- ユーザーが単語をクエリすると、Trieでその単語が検索され、検索された単語から下に向かって(つまり、そこから順番に)、関連するすべてのIDが蓄積されます。これらのIDから、必要なすべてのオブジェクトデータを取得します(キャッシュまたはDBのどちらからでも)。
- 次に、ユーザーがデータベースに新しい単語、たとえば「woodxe」を追加すると、それに応じてTrieが更新されます。
- ユーザーが「woodx」をクエリすると、以前と同じプロセスが発生し、さらに新しいIDが蓄積されます(「woodxe」の発行されたID)
- 英語の辞書には、特定の文字シーケンスで始まる単語の有限リストがあるため、下に移動してすべてのサブノードを取得することは、O(1)の複雑さの有限プロセスです。たとえば、Trieで「wood」で始まる場合、英語辞書の接頭辞として「wood」を持つサブノードのリストは有限の定数です。これらすべてのサブノードをユーザーに返すか、制限を定義するか(遅延読み込み/ページング)、上位10ヒットのみを表示するかは、個人的なアーキテクチャ上の好みです。
これが説明のための写真です(緑色で追加されているものを確認してください)
- ユーザーのクエリが複数の単語の文字列である場合、つまり「木製家具」の場合、各単語は個別に解析/トライに追加され、それに応じて一致するIDのリストが表示されます。
* Trie *は以前のアーキテクチャをどのように改善しますか?
- 「テーブルクエリ」は面倒で、悪い習慣であり、データベースに正比例して大きくなる大きなオーバーヘッドでした。削除されました。
- 私たちが持っていた「転置インデックスマップ」は、余分なメモリオーバーヘッドを作成し、新しい単語で簡単に拡張できませんでした(上記の「woodx」の例のように)。ハッシュマップのクエリはO(1)であると主張することもできますが、メモリ内にいくつかの大きなハッシュマップがあると、実際には特定のレベルで処理速度が低下し、エンジニアリング設計としては不適切と見なされます。
- トライの検索の複雑さはO(m)です。ここで、mは提供されたアルファベットの文字数です。ユーザーが照会するのは純粋に英語のアルファベットを使用した単語であるため、最大のサブツリーは使用可能な最大の英語の単語と等しくなります(定数、つまりO(1))。さらに、前述のように、英語辞書で定義された単語プレフィックスで始まるサブノードの数も一定数であるため、すべての組み合わせでのトラバーサルはO(1)です。したがって、TotalではO(1)操作です。
- したがって、Trie = Get key from Hashmap = O(1)をクエリすると、同じくらい高速になります。
- その上、このシステムでのトライの利点は次のとおりです。
- 複数の転置インデックスハッシュマップをメモリ内で実行するよりもメモリオーバーヘッドが小さい
- 一元化されたクエリツリー
- データベースに新しい単語を追加するだけで、メモリ内の既存のTrieにいくつかの新しいノードを追加するだけで簡単に拡張できます。つまり、データベースの増加と検索クエリの数の増加に関する問題はもうありません(スケーラビリティの悪夢)。
2015年10月15日更新
2012年に、私は個人的なオンラインアプリケーションを構築していましたが、学習目的で、アルゴリズムとアーキテクチャのスキルを強化するために、本質的に好奇心が強いため、実際に車輪の再発明をしたいと考えていました。Apache luceneなどを使用することもできましたが、前述したように、独自のミニ検索エンジンを構築することにしました。
質問:では、elasticsearch、luceneなどの利用可能なサービスを使用する以外に、このアーキテクチャを強化する方法は本当にありませんか?
元の質問
私は、ユーザーが特定のタイトル(たとえば、本x、本yなど)を検索するWebアプリケーションを開発しています。このデータは、リレーショナルデータベース(MySQL)にあります。
私は、データベースからフェッチされた各レコードがメモリにキャッシュされるという原則に従っています。これにより、アプリはデータベースへの呼び出しを減らすことができます。
私は、次のアーキテクチャを使用して、独自のミニ検索エンジンを開発しました。
これがその仕組みです。
- a)ユーザーがレコード名を検索する
- b)システムは、クエリがどの文字で始まるかをチェックし、そこにクエリがあるかどうかをチェックします:レコードを取得します。存在しない場合は、それを追加し、次の2つの方法を使用してデータベースから一致するすべてのレコードを取得します。
- テーブル「クエリ」(一種の履歴テーブル)にすでに存在するクエリは、IDに基づいてレコードを取得します(高速パフォーマンス)
- または、Mysql LIKE%%ステートメントを使用してレコード/IDを取得します(また、ユーザーが使用したクエリを、マップ先のIDとともに履歴テーブルクエリに保持します)。
->次に、レコードとそのIDを キャッシュに追加し、IDのみを転置インデックスマップに追加します。
- テーブル「クエリ」(一種の履歴テーブル)にすでに存在するクエリは、IDに基づいてレコードを取得します(高速パフォーマンス)
- c)結果がUIに返される
システムは正常に動作しますが、2つの主要な問題があります。それは、(過去1か月間試行していた)適切な解決策が見つからなかったことです。
最初の問題:
ポイント(b)をチェックすると、クエリ「履歴」が見つからず、Like %%ステートメントを使用する必要がある場合:クエリがデータベース内の多数のレコード(1つまたは2):
- Mysqlからレコードを取得するには時間がかかります(これが、特定の列でINDEXESを使用した理由です)
- 次に、クエリ履歴を保存します
- 次に、レコード/IDをキャッシュおよび転置インデックスマップに追加します。
2番目の問題:
アプリケーションを使用すると、ユーザーは新しいレコードを自分で追加できます。このレコードは、アプリケーションにログインしている他のユーザーがすぐに使用できます。
ただし、これを実現するには、転置インデックスマップとテーブルの「クエリ」を更新して、古いクエリが新しい単語に一致する場合に備えておく必要があります。たとえば、新しいレコード「woodX」が追加されている場合でも、古いクエリ「wood」はそれにマップされます。したがって、クエリ「wood」をこの新しいレコードに再フックするために、私が今行っていることは次のとおりです。
- 新しいレコード「woodX」が「records」テーブルに追加されます
- 次に、Like %%ステートメントを実行して、テーブル "queries"に既に存在するクエリがこのレコード(たとえば "wood")にマップされているかどうかを確認し、新しいレコードIDを使用してこのクエリを新しい行として追加します:[wood、new id]。
- 次に、メモリ内で、このリストに新しいレコードIDを追加して、転置インデックスマップの「wood」キーの値(つまりリスト)を更新します。
->したがって、リモートユーザーが「wood」を検索すると、メモリから取得されます:woodとwoodX
ここでの問題は時間の消費でもあります。(テーブルクエリ内の)すべてのクエリ履歴を新しく追加された単語と一致させるには、多くの時間がかかります(一致するクエリが多いほど、時間がかかります)。次に、メモリ内の更新にも多くの時間がかかります。
今回の問題を修正するために私が考えているのは、最初に目的の結果をユーザーに返し、次にアプリケーションに必要なデータを使用してajax呼び出しをPOSTさせて、これらすべてのUPDATEタスクを実行することです。しかし、これが悪い習慣なのか、それとも専門的でないやり方なのかはわかりません。
そのため、この1か月間(もう少し)、このアーキテクチャに最適な最適化/変更/更新を考えようとしましたが、ドキュメント検索分野の専門家ではありません(実際には、これまでに構築した最初のミニ検索エンジンです)。
この種のアーキテクチャを実現するために私が何をすべきかについてのフィードバックやガイダンスをいただければ幸いです。
前もって感謝します。
PS:
- サーブレットを使用したj2eeアプリケーションです。
- MySQL innodbを使用しています(したがって、全文検索オプションを使用できません)