web-dev-qa-db-ja.com

高速タグ検索のアルゴリズム

問題は次のとおりです。

  • 一連の単純なエンティティEがあり、各エンティティには一連のタグTが添付されています。各エンティティには、任意の数のタグを付けることができます。エンティティの総数は1億に近く、タグの総数は約5000です。

したがって、初期データは次のようなものです。

E1 - T1, T2, T3, ... Tn
E2 - T1, T5, T100, ... Tk
..
Ez - T10, T12, ... Tl

この初期データが更新されることはほとんどありません。

  • どういうわけか私のアプリは次のようなタグに論理式を生成します:

    T1&T2&T3 | (T5&!T6)

  • 指定した式に一致するエンティティの数を計算する必要があります(注-エンティティではなく、数のみ)。もちろん、これは完全に正確ではないかもしれません。

私が今持っているのは、単純なインメモリテーブルルックアップです。これにより、シングルスレッドでの実行時間が5〜10秒になります。

私は好奇心旺盛ですが、これを効率的に処理する方法はありますか?どのようなアプローチをお勧めしますか?このための一般的なアルゴリズムまたはデータ構造はありますか?

更新

要求に応じて少し説明を加えます。

  1. Tオブジェクトは、実際には比較的短い定数文字列です。しかし、それは実際には問題ではありません-常にいくつかのIDを割り当てて整数を操作することができます。
  2. 私たちは間違いなくそれらを分類することができます。
16
Andy

自己結合を使用して、EntityCategory参照エンティティとeid参照カテゴリの間にリンクテーブルcidがあるsqlでこれを行います。

    select count(ec1.eid)
    from EntityCategory ec1 
    left join EntityCategory ec2 on ec1.eid=ec2.eid 
    left join EntityCategory ec3 on ec1.eid=ec3.eid 
    ...
    where 
      ec1.cid={categoryId1} and 
      ec2.cid={categoryId2} and
      ec3.cid={categoryId3} ...
4
k3b

この回答を書いた後、私はおそらく「広すぎる」としてフラグを立てる必要があります。さまざまな戦略について古くから話し合うことができ、最終的にはベンチマークをデータで実行する必要があります。

各タグは整数で効率的に表すことができます。各エンティティには一連のタグがあります。正しいセット実装を選択することが重要です。Bツリーとソートされた配列の両方が可能です。このセットでは、メンバーシップテストのみを行います。どちらの構造もO(log t)(エンティティごとにtタグを使用)でこれを行うため、配列がより密集しているため、配列を使用します。

O(n・log t・p)演算ですべてのエンティティをフィルタリングできるようになりました。ここで、pは述語決定ツリーの平均パス長です。この決定木は、決定にすばやく到達できるように順序付けできます。統計データがないと、共通の部分式を除外することしかできません。

エンティティが検索される順序はそれほど重要ではありません。一方、0からiまでのインデックスのエンティティにはすべて特定のタグがあり、残りのエンティティにはないように並べ替えると便利です。これにより、この特定のタグを検索するときにnが削減されます(決定ツリーでは、これが最初のテストになるはずです)。これは複数のレベルに拡張できますが、これは物事を複雑にし、O(2kkレベルのメモリ。複数のレベルがある場合、最も高いゲインを持つタグを最初に決定する必要があります。ゲインは、ルックアップする必要のないエンティティの数とそれらを破棄する確率の積です。ゲインは、50:50の確率で、またはエンティティの50%がこの特定のタグを持つときに最大になります。これにより、アクセスパターンが不明な場合でも最適化できます。

使用する各タグでエンティティにインデックスを付けるセットを作成することもできます。1つのセットは、T1のすべてのエンティティを含み、次のセットはT2のエンティティです。明らかな(空間と時間)最適化は、セットにすべての要素の半分以上が含まれている場合に停止し、これらの要素を保存しますしないでくださいこのタグがあります–このようにして、すべてのタグのインデックスを構築します½ · n · tスペースより少なくなります(合計でtタグを使用)。補足セットを保存すると、他の最適化がさらに困難になる可能性があることに注意してください。繰り返しますが、私はセットのために(ソートされた)配列を使用します。

エンティティを整数範囲で表す場合、連続範囲の開始メンバーと終了メンバーのみを格納することで、インデックスセットに使用されるスペースを圧縮できます。実装面では、これはおそらくエントリが範囲の境界であるか通常のエントリであるかを示すためにハイビットで行われます。

インデックスセット(つまり、タグの統計)がある場合は、述語を最適化して、可能性の低いプロパティが最初にテストされるようにすることができます(フェイルファスト戦略)。つまり、T1が一般的で、T2がまれである場合、T1 & T2インデックスセットのすべてのエントリを反復処理し、T2の各要素をテストすることにより、述語T1を評価する必要があります。

ソートされた配列を使用してインデックスセットを実装する場合、多くの評価ステップをマージ操作として実装できます。 T1 & T2は、T1およびT2リストを取得し、大きい配列のサイズのターゲット配列を割り当て、両方の入力が空になるまで次のアルゴリズムを実行することを意味します。T1[0] < T2[0]の場合、T1++(ヘッドを破棄)。 T1[0] > T2[0]の場合、T2++です。両方のヘッドが等しい場合は、その数をターゲット配列にコピーし、3つのポインター(T1T2、target)をすべてインクリメントします。述語がT1 | T2の場合、要素は破棄されず、小さい方の要素がコピーされます。 T1 & ¬T2形式の述語は、マージ戦略を使用して実装することもできますが、¬T1またはT1 | ¬T2は実装できません。

これは、述語ディシジョンツリーを注文するときに考慮する必要があります。補数は、&のRHSで発生するか、最後に最終カウントが決定され、実際の要素を見る必要がないときに発生します。

インデックスセットを使用しなくても、各スレッドはエンティティの一部をフィルタリングして、合計できる述語に一致する要素の数を返すことができます。インデックスセットを使用する場合、各スレッドにはディシジョンツリーの1つのノードが割り当てられます。順序付けられたセットに対応する2つの入力ストリームを取り、マージされたストリームを返します。デシジョンツリーの各ノードには、その部分式を満たすすべてのエンティティを表す対応するセットがあり、セットの順序付けにより、セットをマージするためにセット全体を一度に知る必要がないことに注意してください。 。

インデックス付きセットのマージやエンティティのリストによるフィルタリングなど、さまざまな戦略をある程度組み合わせることができます。フィルタリングには、非常に予測可能なパフォーマンスがあります。クエリが非常に限定的で、インデックスセットの使用により検索スペースが大幅に減少する場合は、マージ操作の方が適している可能性があります。多くの大きな入力セットをマージすると、ブルートフォース検索よりもパフォーマンスが大幅に低下する可能性があることに注意することが重要です。非常に最適化されたアルゴリズムは、入力サイズ、クエリ構造、および統計的指標に基づいて適切な戦略を選択します。

余談ですが、最初の実行を高速化していなくても、同様のクエリが将来実行されることが予想される場合、部分的な結果をキャッシュすることは有益です。

3
amon