sql >> Database >  >> NoSQL >> MongoDB

Functie voor automatisch aanvullen implementeren met behulp van MongoDB-zoekopdracht

tl;dr

Er is geen gemakkelijke oplossing voor wat u wilt, aangezien normale zoekopdrachten de velden die ze retourneren niet kunnen wijzigen. Er is een oplossing (met behulp van de onderstaande mapReduce inline in plaats van uitvoer naar een verzameling), maar behalve voor zeer kleine databases is het niet mogelijk om dit in realtime te doen.

Het probleem

Zoals geschreven, kan een normale query de velden die hij retourneert niet echt wijzigen. Maar er zijn andere problemen. Als je een regex-zoekopdracht wilt doen in de helft van de tijd, moet je alles indexeren velden, die voor die functie een onevenredige hoeveelheid RAM nodig hebben. Als u alle niet zou indexeren velden, zou een regex-zoekopdracht een collectiescan veroorzaken, wat betekent dat elk document van de schijf zou moeten worden geladen, wat te veel tijd zou kosten om automatisch aan te vullen. Bovendien zouden meerdere gelijktijdige gebruikers die om automatische aanvulling verzoeken, een aanzienlijke belasting voor de backend veroorzaken.

De oplossing

Het probleem lijkt veel op het probleem dat ik al heb beantwoord:we moeten elk woord uit meerdere velden extraheren, de stopwoorden verwijderen en de resterende woorden opslaan samen met een link naar de respectieve document(en) het woord is gevonden in een verzameling . Voor het verkrijgen van een automatische aanvullingslijst, doorzoeken we eenvoudig de geïndexeerde woordenlijst.

Stap 1:Gebruik een kaart/reduceer-taak om de woorden te extraheren

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

Het uitvoeren van deze mapReduce tegen uw voorbeeld zou resulteren in db.words ziet er zo uit:

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

Merk op dat de afzonderlijke woorden de _id . zijn van de documenten. De _id veld wordt automatisch geïndexeerd door MongoDB. Aangezien er wordt geprobeerd indexen in het RAM te bewaren, kunnen we een paar trucjes doen om zowel het automatisch aanvullen te versnellen als de belasting van de server te verminderen.

Stap 2:Query voor automatisch aanvullen

Voor automatische aanvulling hebben we alleen de woorden nodig, zonder de links naar de documenten. Aangezien de woorden zijn geïndexeerd, gebruiken we een gedekte zoekopdracht - een zoekopdracht die alleen wordt beantwoord vanuit de index, die zich meestal in het RAM bevindt.

Om bij uw voorbeeld te blijven, zouden we de volgende zoekopdracht gebruiken om de kandidaten voor automatische aanvulling te krijgen:

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

wat ons het resultaat geeft

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

De .explain() . gebruiken methode, kunnen we verifiëren dat deze zoekopdracht alleen de index gebruikt.

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

Let op de indexOnly:true veld.

Stap 3:Vraag het eigenlijke document op

Hoewel we twee query's moeten uitvoeren om het eigenlijke document te krijgen, omdat we het algehele proces versnellen, zou de gebruikerservaring goed genoeg moeten zijn.

Stap 3.1:Verkrijg het document van de words collectie

Wanneer de gebruiker een keuze maakt voor automatisch aanvullen, moeten we het volledige document met woorden doorzoeken om de documenten te vinden waar het woord dat voor automatisch aanvullen is gekozen vandaan komt.

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

wat zou resulteren in een document als dit:

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

Stap 3.2:het eigenlijke document ophalen

Met dat document kunnen we nu ofwel een pagina met zoekresultaten tonen of, zoals in dit geval, doorverwijzen naar het eigenlijke document dat je kunt krijgen door:

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

Opmerkingen

Hoewel deze benadering in eerste instantie misschien ingewikkeld lijkt (nou, de mapReduce is een beetje), is het conceptueel vrij eenvoudig. Kortom, u handelt in realtime resultaten (die u sowieso niet zult hebben, tenzij u een lot uitgeeft van RAM) voor snelheid. Imho, dat is een goede deal. Om de nogal kostbare mapReduce-fase efficiënter te maken, zou het implementeren van Incremental mapReduce een benadering kunnen zijn - het verbeteren van mijn weliswaar gehackte mapReduce zou een andere kunnen zijn.

Last but not least, deze manier is helemaal een nogal lelijke hack. Misschien wilt u zich verdiepen in elasticsearch of luceen. Die producten zijn imho veel, veel meer geschikt voor wat je wilt.




  1. Mongoose met mongodb hoe een zojuist opgeslagen object terug te geven?

  2. Hoe converteer ik een eigenschap in MongoDB van tekst naar datumtype?

  3. Redis scan sleutels overslaan

  4. Wat is het verschil tussen opslaan en invoegen in Mongo DB?