MongoDB
 sql >> Datenbank >  >> NoSQL >> MongoDB

Implementieren Sie die Funktion zur automatischen Vervollständigung mithilfe der MongoDB-Suche

tl;dr

Es gibt keine einfache Lösung für das, was Sie wollen, da normale Abfragen die Felder, die sie zurückgeben, nicht ändern können. Es gibt eine Lösung (unter Verwendung des untenstehenden mapReduce inline, anstatt eine Ausgabe in eine Sammlung zu machen), aber außer bei sehr kleinen Datenbanken ist es nicht möglich, dies in Echtzeit zu tun.

Das Problem

Wie bereits geschrieben, kann eine normale Abfrage die zurückgegebenen Felder nicht wirklich ändern. Aber es gibt andere Probleme. Wenn Sie in halbwegs vernünftiger Zeit eine Regex-Suche durchführen möchten, müssten Sie alle indizieren Felder, die für diese Funktion unverhältnismäßig viel RAM benötigen würden. Wenn Sie nicht alle indizieren würden Felder, würde eine Regex-Suche einen Sammlungsscan verursachen, was bedeutet, dass jedes Dokument von der Festplatte geladen werden müsste, was zu viel Zeit in Anspruch nehmen würde, als dass die automatische Vervollständigung sinnvoll wäre. Darüber hinaus würden mehrere gleichzeitige Benutzer, die eine automatische Vervollständigung anfordern, eine erhebliche Belastung des Backends verursachen.

Die Lösung

Das Problem ist ziemlich ähnlich zu dem, das ich bereits beantwortet habe:Wir müssen jedes Wort aus mehreren Feldern extrahieren, die Stoppwörter entfernen und die verbleibenden Wörter zusammen mit einem Link zu den jeweiligen Dokumenten, in denen das Wort gefunden wurde, in einer Sammlung speichern . Um nun eine Autovervollständigungsliste zu erhalten, fragen wir einfach die indizierte Wortliste ab.

Schritt 1:Verwenden Sie einen Map/Reduce-Job, um die Wörter zu extrahieren

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"
  }
)

Das Ausführen dieses mapReduce gegen Ihr Beispiel würde zu db.words führen so aussehen:

    { "_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") ] } }

Beachten Sie, dass die einzelnen Wörter die _id sind der Dokumente. Die _id -Feld wird automatisch von MongoDB indiziert. Da versucht wird, Indizes im RAM zu halten, können wir ein paar Tricks anwenden, um sowohl die automatische Vervollständigung zu beschleunigen als auch die Belastung des Servers zu reduzieren.

Schritt 2:Abfrage zur automatischen Vervollständigung

Für die automatische Vervollständigung benötigen wir nur die Wörter, ohne die Links zu den Dokumenten. Da die Wörter indiziert sind, verwenden wir eine verdeckte Abfrage – eine Abfrage, die nur aus dem Index beantwortet wird, der sich normalerweise im RAM befindet.

Um bei Ihrem Beispiel zu bleiben, würden wir die folgende Abfrage verwenden, um die Kandidaten für die automatische Vervollständigung zu erhalten:

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

was uns das Ergebnis liefert

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

Verwenden von .explain() -Methode können wir überprüfen, ob diese Abfrage nur den Index verwendet.

        {
        "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
    }

Beachten Sie den indexOnly:true Feld.

Schritt 3:Abfrage des eigentlichen Dokuments

Obwohl wir zwei Abfragen durchführen müssen, um das eigentliche Dokument zu erhalten, sollte die Benutzererfahrung gut genug sein, da wir den Gesamtprozess beschleunigen.

Schritt 3.1:Holen Sie sich das Dokument der words Sammlung

Wenn der Benutzer eine Option für die automatische Vervollständigung auswählt, müssen wir das vollständige Wortdokument abfragen, um die Dokumente zu finden, aus denen das für die automatische Vervollständigung ausgewählte Wort stammt.

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

was zu einem Dokument wie diesem führen würde:

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

Schritt 3.2:Holen Sie sich das eigentliche Dokument

Mit diesem Dokument können wir nun entweder eine Seite mit Suchergebnissen anzeigen oder, wie in diesem Fall, auf das eigentliche Dokument umleiten, das Sie erhalten können:

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

Notizen

Während dieser Ansatz auf den ersten Blick kompliziert erscheinen mag (na ja, mapReduce ist ein bisschen), es ist eigentlich ziemlich einfach konzeptionell. Im Grunde handeln Sie in Echtzeit mit Ergebnissen (die Sie ohnehin nicht haben werden, es sei denn, Sie geben ein Lot aus RAM) für Geschwindigkeit. Imho, das ist ein guter Deal. Um die ziemlich kostspielige mapReduce-Phase effizienter zu gestalten, könnte die Implementierung von inkrementellem mapReduce ein Ansatz sein – die Verbesserung meines zugegebenermaßen gehackten mapReduce könnte ein anderer sein.

Zu guter Letzt ist dieser Weg insgesamt ein ziemlich hässlicher Hack. Vielleicht möchten Sie sich mit Elasticsearch oder Lucene befassen. Diese Produkte sind meiner Meinung nach viel besser geeignet für das, was Sie wollen.