web-dev-qa-db-ja.com

オートコンプリートのアルゴリズム?

ユーザーがGoogleで検索語を入力するときにクエリの候補を提供するために使用されるアルゴリズムについて言及しています。

主に興味があるのは:1.最も重要な結果(一致するものよりもクエリである可能性が高い)2.部分文字列に一致する3.あいまい一致

Trieまたは一般化されたtrieを使用して一致を見つけることができますが、上記の要件を満たしていません...

以前に尋ねた同様の質問 here

58
StackUnderflow

(へん)素晴らしいファジー/部分文字列マッチングアルゴリズムについては、Damn Cool Algorithmsをご覧ください。

これらは試行に取って代わるものではなく、試行におけるブルートフォースルックアップを防止します-これは依然として大きな勝利です。次に、おそらくトライのサイズを制限する方法が必要です。

  • グローバルに使用される最近/上位N個の単語のトライを保持します。
  • ユーザーごとに、そのユーザーの最近/上位N個の単語のトライを保持します。

最後に、可能な限りルックアップを防止したい...

  • キャッシュ検索結果:ユーザーが検索結果をクリックした場合、非常に迅速に検索結果を提供し、完全な部分的/ファジー検索を非同期に取得できます。
  • ルックアップ結果の事前計算:ユーザーが「appl」と入力した場合、「Apple」、「apply」で続行する可能性があります。
  • データのプリフェッチ:たとえば、Webアプリは、JSでブルートフォース検索を実行可能にするのに十分なほど小さな結果セットをブラウザーに送信できます。
56
fearlesstost

ただ言いたいのですが...この問題の良い解決策は、Ternary Search Tree以上のものを組み込むことです。 Ngrams、および帯状疱疹(フレーズ)が必要です。単語境界エラーも検出する必要があります。 「hell o」は「hello」で、「whitesocks」は「white socks」です。これらは前処理ステップです。データを適切に前処理しないと、貴重な検索結果が得られません。三項検索ツリーは、Wordとは何かを理解するのに役立つコンポーネントであり、入力されたWordがインデックス内の有効なWordでない場合に関連する単語の推測を実装するのにも役立ちます。

Googleアルゴリズムは、フレーズの提案と修正を実行します。 Googleアルゴリズムにはコンテキストの概念もあります...最初に検索する単語が天気関連であり、それらを「weatherforcst」vs「monsoonfrcst」vs「deskfrcst」と組み合わせた場合-私の推測は舞台裏でランキングが変更されている最初に出会った単語に基づいた提案-予測と天気は関連する単語であるため、予測はDid-You-Mean推測で上位になります。

単語の部分(ngram)、フレーズ用語(シングル)、単語の近接(Wordクラスタリングインデックス)、三項検索ツリー(Wordルックアップ)。

8
Ben DeMott

Googleの正確なアルゴリズムは不明ですが、 と言われています ユーザー入力の統計分析で機能します。ほとんどの場合に適さないアプローチ。より一般的な自動補完は、次のいずれかを使用して実装されます。

  • ツリー。ツリー構造(プレフィックスツリー、サフィックスツリー、dawgなど)の検索可能なテキストにインデックスを付けることにより、メモリストレージを犠牲にして非常に高速な検索を実行できます。ツリートラバーサルは、近似一致に適合させることができます。
  • パターン分割。テキストをトークン(ngram)に分割することにより、単純なハッシュスキームを使用して、パターンの出現の検索を実行できます。
  • フィルタリング。潜在的な一致のセットを見つけてから、順次アルゴリズムを適用して各候補を確認します。

completely 、a Java後者の概念のいくつかを実装するオートコンプリートライブラリをご覧ください。

soundexlevenshtein distance などのツールがあり、特定の範囲内のあいまい一致を見つけるために使用できます。

Soundexは似た音の単語を検索し、levenshtein distanceは別のWordから特定の編集距離内にある単語を検索します。

4
Ólafur Waage

FirefoxのAwesomeバーアルゴリズム をご覧ください

Googleは、何百万もの人気のあるクエリと過去の関連クエリを考慮に入れるため、便利です。

しかし、それは良い補完アルゴリズム/ UIを持っていません:

  1. 部分文字列を行いません
  2. 比較的単純なWord境界プレフィックスアルゴリズムのようです。
    例:Tomcat tut->「Tomcatチュートリアル」を正しく提案します。 Tomcat rial->提案なし)-::
  3. 「という意味ですか?」をサポートしていません-Google検索結果のように。
3
Dekel

部分文字列とあいまい一致の場合、レーベンシュタイン距離アルゴリズムはかなりうまく機能しました。オートコンプリート/提案の業界実装ほど完璧ではないように思えますが。 GoogleとMicrosoftのIntellisenseはどちらも優れた仕事をしています。なぜなら、この基本的なアルゴリズムを改良して、異種の文字列を一致させるために必要な編集操作の種類を比較検討しているからです。例えば。 2つの文字の転置は、おそらく2(挿入と削除)ではなく、1つの操作としてのみカウントする必要があります。

しかし、それでもこれで十分だと思います。 C#での実装です...

// This is the traditional Levenshtein Distance algorithem, though I've tweaked it to make
// it more like Google's autocomplete/suggest.  It returns the number of operations 
// (insert/delete/substitute) required to change one string into another, with the 
// expectation that userTyped is only a partial version of fullEntry.
// Gives us a measurement of how similar the two strings are.
public static int EditDistance(string userTyped, string fullEntry)
{
    if (userTyped.Length == 0) // all entries are assumed to be fully legit possibilities 
        return 0; // at this point, because the user hasn't typed anything.

    var inx = fullEntry.IndexOf(userTyped[0]);
    if (inx < 0) // If the 1st character doesn't exist anywhere in the entry, it's not
        return Int32.MaxValue; // a possible match.

    var lastInx = inx;
    var lastMatchCount = 0;
TryAgain:
    // Is there a better starting point?
    var len = fullEntry.Length - inx;
    var matchCount = 1;
    var k = 1;
    for (; k < len; k++)
    {
        if (k == userTyped.Length || userTyped[k] != fullEntry[k + inx])
        {
            if (matchCount > lastMatchCount)
            {
                lastMatchCount = matchCount;
                lastInx = inx;
            }
            inx = fullEntry.IndexOf(userTyped[0], inx + 1);
            matchCount = 0;
            if (inx > 0)
                goto TryAgain;
            else
                break;
        }
        else
            matchCount++;
    }
    if (k == len && matchCount > lastMatchCount)
        lastInx = inx;

    if (lastInx > 0)
        fullEntry = fullEntry.Substring(lastInx); // Jump to 1st character match, ignoring previous values 

    // The start of the Levenshtein Distance algorithem.
    var m = userTyped.Length;
    var n = Math.Min(m, fullEntry.Length);

    int[,] d = new int[m + 1, n + 1]; // "distance" - meaning number of operations.

    for (var i = 0; i <= m; i++)
        d[i, 0] = i; // the distance of any first string to an empty second string
    for (var j = 0; j <= n; j++)
        d[0, j] = j; // the distance of any second string to an empty first string

    for (var j = 1; j <= n; j++)
        for (var i = 1; i <= m; i++)
            if (userTyped[i - 1] == fullEntry[j - 1])
                d[i, j] = d[i - 1, j - 1];       // no operation required
            else
                d[i, j] = Math.Min
                           (
                             d[i - 1, j] + 1,  // a deletion
                             Math.Min(
                             d[i, j - 1] + 1,  // an insertion
                             d[i - 1, j - 1] + 1 // a substitution
                             )
                           );

    return d[m, n];
}
2
Gabe Halsmer

問題の全体的なデザインを探している場合は、 https://www.interviewbit.com/problems/search-typeahead/ でコンテンツを読んでみてください。

トライを使用する単純なアプローチでオートコンプリートを構築することから始めて、それを基に構築します。また、特定のユースケースに対応するためのサンプリングやオフライン更新などの最適化手法についても説明します。

ソリューションの拡張性を維持するには、トライデータをインテリジェントに分割する必要があります。

1
user3273189

完全に異なるデータ構造を追求するよりも、専門的なトライを構築する方がよいと思います。

その機能は、各葉に対応するWordの検索頻度を反映するフィールドがあるトライで明らかになりました。

検索クエリメソッドは、各子孫リーフノードまでの距離に各子孫リーフノードに関連付けられた検索頻度を乗算して計算された最大値を持つ子孫リーフノードを表示します。

Googleが使用するデータ構造(およびその結果としてのアルゴリズム)はおそらく非常に複雑であり、特定のアカウント(および時刻...および天気...季節)からの検索頻度など、他の多数の要因を潜在的に取り入れています...および月相...および...)。ただし、基本的なトライデータ構造は、各ノードにフィールドを追加し、検索クエリメソッドでそれらのフィールドを使用することで、あらゆる種類の特殊な検索設定に拡張できると考えています。

0
T.K.