sql >> データベース >  >> NoSQL >> MongoDB

MongoDB検索を使用してオートコンプリート機能を実装する

    tl; dr

    通常のクエリでは返されるフィールドを変更できないため、必要なものを簡単に解決することはできません。解決策はありますが(コレクションへの出力を行う代わりに、以下のmapReduceインラインを使用)、非常に小さなデータベースを除いて、これをリアルタイムで行うことはできません。

    問題

    書かれているように、通常のクエリは、返すフィールドを実際に変更することはできません。しかし、他にも問題があります。中途半端な時間で正規表現検索を実行する場合は、すべてのインデックスを作成する必要があります。 その機能のために不釣り合いな量のRAMを必要とするフィールド。 すべてのインデックスを作成しない場合 フィールドの場合、正規表現検索によりコレクションスキャンが発生します。つまり、すべてのドキュメントをディスクからロードする必要があり、オートコンプリートが便利になるには時間がかかりすぎます。さらに、オートコンプリートをリクエストする複数の同時ユーザーは、バックエンドにかなりの負荷をかけます。

    解決策

    問題は私がすでに答えたものと非常に似ています:複数のフィールドからすべての単語を抽出し、ストップワードを削除し、残りの単語を、その単語がコレクションで見つかったそれぞれのドキュメントへのリンクと一緒に保存する必要があります。ここで、オートコンプリートリストを取得するために、インデックス付きの単語リストをクエリするだけです。

    ステップ1:map/reduceジョブを使用して単語を抽出します

    db.yourCollection.mapReduce(
      // Map function
      function() {
    
        // We need to save this in a local var as per scoping problems
        var document = this;
    
        // You need to expand this according to your needs
        var stopwords = ["the","this","and","or"];
    
        for(var prop in document) {
    
          // We are only interested in strings and explicitly not in _id
          if(prop === "_id" || typeof document[prop] !== 'string') {
            continue
          }
    
          (document[prop]).split(" ").forEach(
            function(word){
    
              // You might want to adjust this to your needs
              var cleaned = word.replace(/[;,.]/g,"")
    
              if(
                // We neither want stopwords...
                stopwords.indexOf(cleaned) > -1 ||
                // ...nor string which would evaluate to numbers
                !(isNaN(parseInt(cleaned))) ||
                !(isNaN(parseFloat(cleaned)))
              ) {
                return
              }
              emit(cleaned,document._id)
            }
          ) 
        }
      },
      // Reduce function
      function(k,v){
    
        // Kind of ugly, but works.
        // Improvements more than welcome!
        var values = { 'documents': []};
        v.forEach(
          function(vs){
            if(values.documents.indexOf(vs)>-1){
              return
            }
            values.documents.push(vs)
          }
        )
        return values
      },
    
      {
        // We need this for two reasons...
        finalize:
    
          function(key,reducedValue){
    
            // First, we ensure that each resulting document
            // has the documents field in order to unify access
            var finalValue = {documents:[]}
    
            // Second, we ensure that each document is unique in said field
            if(reducedValue.documents) {
    
              // We filter the existing documents array
              finalValue.documents = reducedValue.documents.filter(
    
                function(item,pos,self){
    
                  // The default return value
                  var loc = -1;
    
                  for(var i=0;i<self.length;i++){
                    // We have to do it this way since indexOf only works with primitives
    
                    if(self[i].valueOf() === item.valueOf()){
                      // We have found the value of the current item...
                      loc = i;
                      //... so we are done for now
                      break
                    }
                  }
    
                  // If the location we found equals the position of item, they are equal
                  // If it isn't equal, we have a duplicate
                  return loc === pos;
                }
              );
            } else {
              finalValue.documents.push(reducedValue)
            }
            // We have sanitized our data, now we can return it        
            return finalValue
    
          },
        // Our result are written to a collection called "words"
        out: "words"
      }
    )
    

    このmapReduceを例に対して実行すると、db.wordsになります。 次のようになります:

        { "_id" : "can", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
        { "_id" : "canada", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
        { "_id" : "candid", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
        { "_id" : "candle", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
        { "_id" : "candy", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
        { "_id" : "cannister", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
        { "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
        { "_id" : "canvas", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
    

    個々の単語は_idであることに注意してください ドキュメントの。 _id フィールドはMongoDBによって自動的にインデックス付けされます。インデックスはRAMに保持されるように試みられているため、オートコンプリートを高速化し、サーバーにかかる負荷を軽減するために、いくつかのトリックを実行できます。

    ステップ2:オートコンプリートのクエリ

    オートコンプリートの場合、必要なのは単語のみで、ドキュメントへのリンクはありません。単語にはインデックスが付けられているため、カバーされたクエリを使用します。これは、通常RAMにあるインデックスからのみ回答されるクエリです。

    あなたの例に固執するために、次のクエリを使用してオートコンプリートの候補を取得します:

    db.words.find({_id:/^can/},{_id:1})
    

    結果が得られます

        { "_id" : "can" }
        { "_id" : "canada" }
        { "_id" : "candid" }
        { "_id" : "candle" }
        { "_id" : "candy" }
        { "_id" : "cannister" }
        { "_id" : "canteen" }
        { "_id" : "canvas" }
    

    .explain()を使用する メソッドでは、このクエリがインデックスのみを使用していることを確認できます。

            {
            "cursor" : "BtreeCursor _id_",
            "isMultiKey" : false,
            "n" : 8,
            "nscannedObjects" : 0,
            "nscanned" : 8,
            "nscannedObjectsAllPlans" : 0,
            "nscannedAllPlans" : 8,
            "scanAndOrder" : false,
            "indexOnly" : true,
            "nYields" : 0,
            "nChunkSkips" : 0,
            "millis" : 0,
            "indexBounds" : {
                "_id" : [
                    [
                        "can",
                        "cao"
                    ],
                    [
                        /^can/,
                        /^can/
                    ]
                ]
            },
            "server" : "32a63f87666f:27017",
            "filterSet" : false
        }
    

    indexOnly:trueに注意してください フィールド。

    ステップ3:実際のドキュメントをクエリする

    実際のドキュメントを取得するには2つのクエリを実行する必要がありますが、プロセス全体が高速化されるため、ユーザーエクスペリエンスは十分であるはずです。

    ステップ3.1:wordsのドキュメントを取得する コレクション

    ユーザーがオートコンプリートの選択肢を選択すると、オートコンプリート用に選択された単語が由来するドキュメントを見つけるために、単語の完全なドキュメントをクエリする必要があります。

    db.words.find({_id:"canteen"})
    

    その結果、次のようなドキュメントになります:

    { "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
    

    ステップ3.2:実際のドキュメントを取得する

    そのドキュメントを使用して、検索結果を含むページを表示するか、この場合のように、取得できる実際のドキュメントにリダイレクトすることができます。

    db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
    

    メモ

    このアプローチは最初は複雑に見えるかもしれませんが(まあ、mapReduce 少し)、それは概念的には実際にはかなり簡単です。基本的に、あなたはリアルタイムの結果を取引しています( lot を使わない限り、とにかく結果は得られません) RAMの)速度のため。イムホ、それはお得だ。かなりコストのかかるmapReduceフェーズをより効率的にするために、インクリメンタルmapReduceを実装することがアプローチになる可能性があります。明らかにハッキングされたmapReduceを改善することも別の方法かもしれません。

    最後になりましたが、この方法は全体としてかなり醜いハックです。 Elasticsearchまたはluceneを掘り下げたいと思うかもしれません。これらの製品imhoは、あなたが望むものにはるかに適しています。




    1. だからあなたのHBaseは壊れています

    2. SidekiqでRedis6のTLSを有効にする方法は?

    3. mongodb-存在しない場合はドキュメントを作成し、存在しない場合は配列にプッシュします

    4. Mongodb配列$pushおよび$pull