12

アップデート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のみを転置インデックスマップに追加します。
  • 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を使用しています(したがって、全文検索オプションを使用できません)

4

4 に答える 4

2

私は解決策を持っているふりをしませんが、ここに私の考えがあります。まず、時間のかかるクエリが好きですLIKE %%:MySQLで数十の回答に限定されたクエリを実行し、それをユーザーに返し、ユーザーがより一致するレコードを必要としているかどうかを確認するか、起動しますバックグラウンドでは、将来の検索のためのインデックス作成のニーズに応じて、完全なクエリが実行されます。

より一般的には、すべてをメモリに保存すると、ある日、メモリの消費量が多すぎる可能性があると思います。また、検索エンジンはすべてをメモリに保持することでますます高速になりますが、データが追加または更新されるときにこれらすべてのキャッシュを最新の状態に保つ必要があり、確かにますます時間がかかります。

そのため、「オープンソースフォーラムソフトウェア」(名前を思い出せませんでした)で1日見た解決策は、投稿のテキスト検索にはそれほど悪くないと思います。データが挿入されるたびに、「Words」という名前のテーブルが表示されます。 "は、既存のすべての単語と、各単語とそれが表示される投稿の間のリンクを別のテーブル(たとえば、「WordsLinks」)で追跡します。この種のソリューションには、いくつかの欠点があります。

  • データベース内の各挿入、削除、更新は非常に遅くなります
  • 検索エンジンのデータ選択を予期する必要があります。保持したことのない2文字の単語を保持することを選択した場合、完全なデータ再処理を開始しない限り、すでに記録されたデータには遅すぎます。
  • DELETEだけでなく、UPDATEとINSERTにも注意する必要があります

しかし、私はいくつかの大きな利点があると思います:

  • 計算時間はおそらく「メモリソリューション」と同じですが(最終的には)、クエリ時ではなく、データベースの作成/更新/削除ごとに分割されます。
  • 単語全体、または「で始まる」単語の検索は瞬時に行われます。インデックスを作成すると、「単語」テーブルでの検索は二分されます。また、「WordLinks」テーブルクエリは、インデックスを使用しても非常に高速です。
  • 同時に複数の単語を探すのは簡単かもしれません。見つかった単語ごとに「WordLinks」のグループを収集し、それらに対して交差を実行して、これらすべてのグループに共通の「データベースID」のみを保持します。たとえば、「tree」と「leaf」という単語を使用すると、最初のレコードはテーブルレコード{1、4、6}を提供し、2番目の単語は{1、3、6、9}を提供できます。したがって、交差点を使用すると、一般的な部分のみを保持するのは簡単です:{1、6}。
  • 単一列テーブルの「Like%%」は、さまざまなテーブルのさまざまなフィールドの多くの「Like%%」よりもおそらく高速です。そして、各データベースエンジンはいくつかのキャッシュを処理します:「Words」テーブルはメモリに保持するのに十分小さい可能性があります
  • データが膨大になると、パフォーマンスやメモリの問題が発生するリスクはわずかだと思います。
  • すべての検索が高速であるため、同義語を探すこともできます。たとえば、ユーザーが「イーサネット」で何も見つからなかった場合は、「ネットワーク」を検索します。
  • キャメルケースの単語を分割するなどのルールを適用して、たとえば「woodX」から「wood」、「X」、「woodX」の3つの単語を生成できます。各「単語」は保存と検索が非常に軽量であるため、さまざまなことができます。

必要な解決策は、メソッドのブレンドである可能性があると思います。たとえば、軽量のUPDATE、INSERT、DELETEを維持し、TRIGGERから「Words」と「WordsLinks」を起動できます。

逸話として、「すべて」(!)をメモリに保持することが決定された、私の会社によって開発されたソフトウェアを見ました。64GBのRAMを搭載したサーバーを購入することをお客様に推奨することになります。少し高価です。それは、最終的にはメモリの充満につながる可能性のある解決策を見つけたときに、私が非常に賢明である理由を説明しています。

于 2015-10-14T12:53:23.580 に答える
2

私はあなたのデザインが問題にうまく適合していないと思います。あなたが今見ている問題はその結果です。それを除けば、現在のソリューションは拡張性がありません。

考えられる解決策は次のとおりです。

  1. 信頼できるデータのみを含み、派生データを含まないようにデータベースを再設計します。したがって、すべてのキャッシュエントリはMySQLから消える必要があります。

  2. アプリケーション内のメモリにリクエストが存在する間だけデータを保持します。これにより、アプリケーションの設計がはるかに簡単になり(競合状態を考えてください)、適切な数のクライアントに拡張できます。

  3. キャッシングレイヤーを導入します。これを自分で作成するのではなく、確立された製品を使用することを強くお勧めします。これにより、アプリケーション内のすべてのカスタムビルドされたキャッシングロジックが解放され、さらに優れた作業が可能になります。

キャッシングレイヤーのRedisまたはMemcachedを確認できます。LRU戦略はここに当てはまると思います。クエリがどれほど複雑になるかによっては、Luceneのような専用のインデックス付き検索メカニズムも意味をなす場合があります。

于 2015-10-15T11:45:01.807 に答える
2

SphinxSearchServerを強くお勧めします。wchichは全文検索で最適化されています。http://sphinxsearch.com/にアクセスします。

MySQLで動作するように設計されているため、現在のワークスペースに追加されます。

于 2015-10-15T16:53:49.047 に答える
1

これはMySQLで実装できると確信していますが、Elasticsearchなどの既存の検索指向データベースを使用する方がはるかに簡単です。Luceneライブラリを使用して転置インデックスを実装し、豊富なドキュメントがあり、水平スケーリング、かなり単純なクエリ言語などをサポートしています。これまでの作業はかなり大変だったと思います。ソリューションを「本番環境グレード」にするためには、キャッシュ、競合状態、バグ、パフォーマンスの問題などを処理するためにさらに多くの作業が必要になります。

于 2015-10-15T17:17:55.767 に答える