sql >> Database teknologi >  >> NoSQL >> MongoDB

Implementer automatisk udfyldningsfunktion ved hjælp af MongoDB-søgning

tl;dr

Der er ingen nem løsning på det, du ønsker, da normale forespørgsler ikke kan ændre de felter, de returnerer. Der er en løsning (ved at bruge nedenstående mapReduce inline i stedet for at lave et output til en samling), men bortset fra meget små databaser, er det ikke muligt at gøre dette i realtid.

Problemet

Som skrevet kan en normal forespørgsel ikke rigtig ændre de felter, den returnerer. Men der er andre problemer. Hvis du vil lave en regex-søgning på halvvejs anstændig tid, skal du indeksere alle felter, som ville kræve en uforholdsmæssig mængde RAM til den funktion. Hvis du ikke ville indeksere alle felter, ville en regex-søgning forårsage en samlingsscanning, hvilket betyder, at hvert dokument skal indlæses fra disken, hvilket ville tage for lang tid, før autofuldførelse er praktisk. Ydermere ville flere samtidige brugere, der anmoder om autofuldførelse, skabe betydelig belastning på backend.

Løsningen

Problemet ligner meget det, jeg allerede har besvaret:Vi skal udtrække hvert ord ud af flere felter, fjerne stopordene og gemme de resterende ord sammen med et link til det eller de respektive dokumenter, ordet blev fundet i en samling . Nu, for at få en autofuldførelsesliste, forespørger vi blot på den indekserede ordliste.

Trin 1:Brug et kort/reducer job til at udtrække ordene

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

At køre denne mapReduce mod dit eksempel ville resultere i db.words se sådan her ud:

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

Bemærk, at de individuelle ord er _id af dokumenterne. _id feltet indekseres automatisk af MongoDB. Da indekser forsøges opbevaret i RAM, kan vi lave et par tricks for både at fremskynde autofuldførelse og reducere belastningen på serveren.

Trin 2:Forespørgsel om autofuldførelse

Til autofuldførelse har vi kun brug for ordene uden links til dokumenterne. Da ordene er indekseret, bruger vi en dækket forespørgsel – en forespørgsel, der kun besvares fra indekset, som normalt ligger i RAM.

For at holde fast i dit eksempel ville vi bruge følgende forespørgsel til at få kandidaterne til autofuldførelse:

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

hvilket giver os resultatet

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

Brug .explain() metode, kan vi bekræfte, at denne forespørgsel kun bruger indekset.

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

Bemærk indexOnly:true felt.

Trin 3:Forespørg på det faktiske dokument

Selvom vi bliver nødt til at lave to forespørgsler for at få det faktiske dokument, da vi fremskynder den overordnede proces, bør brugeroplevelsen være god nok.

Trin 3.1:Hent dokumentet med words indsamling

Når brugeren vælger et valg af autofuldførelse, skal vi forespørge i det komplette dokument med ord for at finde de dokumenter, hvorfra det valgte ord til autofuldførelse stammer fra.

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

hvilket ville resultere i et dokument som dette:

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

Trin 3.2:Hent det faktiske dokument

Med det dokument kan vi nu enten vise en side med søgeresultater eller, som i dette tilfælde, omdirigere til det faktiske dokument, som du kan få ved at:

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

Bemærkninger

Selvom denne tilgang kan virke kompliceret i starten (vel, mapReduce er lidt), er det faktisk ret nemt konceptuelt. Dybest set handler du med resultater i realtid (som du alligevel ikke får, medmindre du bruger masse RAM) for hastighed. Imho, det er en god aftale. For at gøre den temmelig dyre mapReduce-fase mere effektiv, kunne implementering af Incremental mapReduce være en fremgangsmåde – at forbedre mit ganske vist hackede mapReduce kunne meget vel være en anden.

Sidst men ikke mindst er denne måde et ret grimt hack i det hele taget. Du vil måske grave i elasticsearch eller lucene. Disse produkter er imho meget, meget mere egnede til det, du ønsker.




  1. MongoDB:tæl antallet af elementer i et array

  2. MongoDB $in Aggregation Pipeline Operator

  3. Sådan gemmer og henter du session fra Redis

  4. Hvordan skalerer man Node.js WebSocket Redis Server?