sql >> Databasteknik >  >> NoSQL >> MongoDB

Implementera automatisk kompletteringsfunktion med MongoDB-sökning

tl;dr

Det finns ingen enkel lösning för vad du vill ha, eftersom vanliga frågor inte kan ändra fälten de returnerar. Det finns en lösning (med hjälp av nedanstående mapReduce inline istället för att göra en utdata till en samling), men förutom för mycket små databaser är det inte möjligt att göra detta i realtid.

Problemet

Som skrivet kan en normal fråga inte riktigt ändra fälten den returnerar. Men det finns andra problem. Om du vill göra en regex-sökning på halvvägs anständig tid, måste du indexera alla fält, som skulle behöva en oproportionerligt mycket RAM-minne för den funktionen. Om du inte skulle indexera alla fält, skulle en regex-sökning orsaka en samlingsskanning, vilket innebär att varje dokument måste laddas från disken, vilket skulle ta för lång tid för autokomplettering för att vara bekvämt. Dessutom skulle flera samtidiga användare som begär autokompletterande skapa avsevärd belastning på backend.

Lösningen

Problemet är ganska likt ett jag redan har svarat på:Vi måste extrahera varje ord ur flera fält, ta bort stopporden och spara de återstående orden tillsammans med en länk till respektive dokument som ordet hittades i en samling . Nu, för att få en autokompletteringslista, frågar vi helt enkelt den indexerade ordlistan.

Steg 1:Använd ett karta/förminska jobb för att extrahera orden

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

Att köra denna mapReduce mot ditt exempel skulle resultera i db.words se ut så här:

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

Observera att de enskilda orden är _id av dokumenten. _id fältet indexeras automatiskt av MongoDB. Eftersom index försöker hållas i RAM, kan vi göra några knep för att både påskynda autokomplettering och minska belastningen på servern.

Steg 2:Fråga efter autoslutförande

För autokomplettering behöver vi bara orden, utan länkarna till dokumenten. Eftersom orden är indexerade använder vi en täckt fråga – en fråga som bara besvaras från indexet, som vanligtvis finns i RAM.

För att hålla fast vid ditt exempel skulle vi använda följande fråga för att få kandidaterna för autokomplettering:

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

vilket ger oss resultatet

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

Använd .explain() metod kan vi verifiera att denna fråga bara använder indexet.

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

Notera indexOnly:true fältet.

Steg 3:Fråga själva dokumentet

Även om vi kommer att behöva göra två frågor för att få det faktiska dokumentet, eftersom vi påskyndar den övergripande processen, bör användarupplevelsen vara tillräckligt bra.

Steg 3.1:Hämta dokumentet med words samling

När användaren väljer ett val av autokomplettering måste vi fråga efter det fullständiga dokumentet med ord för att hitta dokumenten varifrån ordet som valts för autokomplettering kommer från.

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

vilket skulle resultera i ett dokument som detta:

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

Steg 3.2:Hämta själva dokumentet

Med det dokumentet kan vi nu antingen visa en sida med sökresultat eller, som i det här fallet, omdirigera till det faktiska dokumentet som du kan få genom att:

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

Anteckningar

Även om detta tillvägagångssätt kan tyckas komplicerat till en början (tja, mapReduce är lite), är det faktiskt ganska lätt konceptuellt. I grund och botten handlar du med realtidsresultat (vilket du ändå inte kommer att få om du inte spenderar mycket RAM) för hastighet. Imho, det är en bra affär. För att göra den ganska kostsamma mapReduce-fasen mer effektiv kan det vara ett sätt att implementera Incremental mapReduce – att förbättra min visserligen hackade mapReduce kan mycket väl vara en annan.

Sist men inte minst är det här sättet ett ganska fult hack helt och hållet. Du kanske vill gräva i elasticsearch eller lucene. Dessa produkter är mycket, mycket mer lämpade för vad du vill ha.




  1. Arbetar med @cache_page() dekoratör i django-redis-cache

  2. implementera out-of-proces cache med Redis i Windows Azure

  3. Vad är standardsessionens timeout och hur man konfigurerar det när du använder vårsessionen med Redis som backend

  4. Hur man kör råa mongodb-kommandon från pymongo