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

Nur übereinstimmende Felder für die MongoDB-Textsuche anzeigen

Nachdem ich lange darüber nachgedacht habe, denke ich, dass es möglich ist, das umzusetzen, was Sie wollen. Es ist jedoch nicht für sehr große Datenbanken geeignet und ich habe noch keinen inkrementellen Ansatz ausgearbeitet. Es fehlt die Wortstammbildung und Stoppwörter müssen manuell definiert werden.

Die Idee ist, mit mapReduce eine Sammlung von Suchwörtern mit Verweisen auf das Ursprungsdokument und das Feld, aus dem das Suchwort stammt, zu erstellen. Dann erfolgt die eigentliche Abfrage für die Autovervollständigung über eine einfache Aggregation, die einen Index verwendet und daher recht schnell sein sollte.

Wir werden also mit den folgenden drei Dokumenten arbeiten

{
  "name" : "John F. Kennedy",
  "address" : "Kenson Street 1, 12345 Footown, TX, USA",
  "note" : "loves Kendo and Sushi"
}

und

{
  "name" : "Robert F. Kennedy",
  "address" : "High Street 1, 54321 Bartown, FL, USA",
  "note" : "loves Ethel and cigars"
}

und

{
  "name" : "Robert F. Sushi",
  "address" : "Sushi Street 1, 54321 Bartown, FL, USA",
  "note" : "loves Sushi and more Sushi"
}

in einer Sammlung namens textsearch .

Die Map/Reduce-Stufe

Was wir im Grunde tun, ist, dass wir jedes einzelne Wort in einem der drei Felder verarbeiten, Stoppwörter und Zahlen entfernen und jedes einzelne Wort mit der _id des Dokuments speichern und das Feld des Vorkommens in einer Zwischentabelle.

Der kommentierte Code:

db.textsearch.mapReduce(
  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"];

    // This denotes the fields which should be processed
    var fields = ["name","address","note"];

    // For each field...
    fields.forEach(

      function(field){

        // ... we split the field into single words...
        var words = (document[field]).split(" ");

        words.forEach(

          function(word){
            // ...and remove unwanted characters.
            // Please note that this regex may well need to be enhanced
            var cleaned = word.replace(/[;,.]/g,"")

            // Next we check...
            if(
              // ...wether the current word is in the stopwords list,...
              (stopwords.indexOf(word)>-1) ||

              // ...is either a float or an integer... 
              !(isNaN(parseInt(cleaned))) ||
              !(isNaN(parseFloat(cleaned))) ||

              // or is only one character.
              cleaned.length < 2
            )
            {
              // In any of those cases, we do not want to have the current word in our list.
              return
            }
              // Otherwise, we want to have the current word processed.
              // Note that we have to use a multikey id and a static field in order
              // to overcome one of MongoDB's mapReduce limitations:
              // it can not have multiple values assigned to a key.
              emit({'word':cleaned,'doc':document._id,'field':field},1)

          }
        )
      }
    )
  },
  function(key,values) {

    // We sum up each occurence of each word
    // in each field in every document...
    return Array.sum(values);
  },
    // ..and write the result to a collection
  {out: "searchtst" }
)

Wenn Sie dies ausführen, wird die Sammlung searchtst erstellt . Wenn es bereits existierte, wird sein gesamter Inhalt ersetzt.

Es sieht in etwa so aus:

{ "_id" : { "word" : "Bartown", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Bartown", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Ethel", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "note" }, "value" : 1 }
{ "_id" : { "word" : "FL", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "FL", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Footown", "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "address" }, "value" : 1 }
[...]
{ "_id" : { "word" : "Sushi", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "name" }, "value" : 1 }
{ "_id" : { "word" : "Sushi", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "note" }, "value" : 2 }
[...]

Hier gibt es einiges zu beachten. Zunächst einmal kann ein Wort mehrfach vorkommen, zum Beispiel bei „FL“. Es kann sich jedoch um andere Dokumente handeln, wie es hier der Fall ist. Andererseits kann ein Wort auch mehrfach in einem einzelnen Feld eines einzelnen Dokuments vorkommen. Wir werden dies später zu unserem Vorteil nutzen.

Zweitens haben wir alle Felder, insbesondere das word Feld in einem zusammengesetzten Index für _id , was die kommenden Abfragen ziemlich schnell machen sollte. Das bedeutet aber auch, dass der Index ziemlich groß wird und – wie alle Indizes – dazu neigt, RAM zu verbrauchen.

Die Aggregationsphase

Also haben wir die Liste der Wörter reduziert. Jetzt fragen wir nach einer (Unter-)Zeichenfolge. Was wir tun müssen, ist, alle Wörter zu finden, die mit der Zeichenfolge beginnen, die der Benutzer bisher eingegeben hat, und eine Liste von Wörtern zurückzugeben, die mit dieser Zeichenfolge übereinstimmen. Um dies tun zu können und die Ergebnisse in einer für uns geeigneten Form zu erhalten, verwenden wir eine Aggregation.

Diese Aggregation sollte ziemlich schnell sein, da alle erforderlichen abzufragenden Felder Teil eines zusammengesetzten Indexes sind.

Hier ist die kommentierte Aggregation für den Fall, dass der Benutzer den Buchstaben S eingegeben hat :

db.searchtst.aggregate(
  // We match case insensitive ("i") as we want to prevent
  // typos to reduce our search results
  { $match:{"_id.word":/^S/i} },
  { $group:{
      // Here is where the magic happens:
      // we create a list of distinct words...
      _id:"$_id.word",
      occurrences:{
        // ...add each occurrence to an array...
        $push:{
          doc:"$_id.doc",
          field:"$_id.field"
        } 
      },
      // ...and add up all occurrences to a score
      // Note that this is optional and might be skipped
      // to speed up things, as we should have a covered query
      // when not accessing $value, though I am not too sure about that
      score:{$sum:"$value"}
    }
  },
  {
    // Optional. See above
    $sort:{_id:-1,score:1}
  }
)

Das Ergebnis dieser Abfrage sieht in etwa so aus und sollte ziemlich selbsterklärend sein:

{
  "_id" : "Sushi",
  "occurences" : [
    { "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "note" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "name" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "note" }
  ],
  "score" : 5
}
{
  "_id" : "Street",
  "occurences" : [
    { "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "address" },
    { "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }
  ],
  "score" : 3
}

Die Note 5 für Sushi ergibt sich daraus, dass das Wort Sushi zweimal im Notizfeld eines der Dokumente vorkommt. Dies ist beabsichtigtes Verhalten.

Obwohl dies eine Lösung für arme Leute sein mag, für die unzähligen denkbaren Anwendungsfälle optimiert werden muss und ein inkrementelles mapReduce implementiert werden müsste, um in Produktionsumgebungen halbwegs nützlich zu sein, funktioniert es wie erwartet. hth.

Bearbeiten

Natürlich könnte man das $match weglassen stage und füge ein $out hinzu Stufe in der Aggregationsphase, um die Ergebnisse vorverarbeiten zu lassen:

db.searchtst.aggregate(
  {
    $group:{
      _id:"$_id.word",
      occurences:{ $push:{doc:"$_id.doc",field:"$_id.field"}},
      score:{$sum:"$value"}
     }
   },{
     $out:"search"
   })

Jetzt können wir die resultierende search abfragen Sammlung, um die Dinge zu beschleunigen. Grundsätzlich tauschen Sie Echtzeit-Ergebnisse gegen Geschwindigkeit.

Bearbeiten 2 :Falls der Vorverarbeitungsansatz gewählt wird, wird der searchtst Sammlung des Beispiels sollte nach Abschluss der Aggregation gelöscht werden, um sowohl Speicherplatz als auch – noch wichtiger – wertvollen Arbeitsspeicher zu sparen.