sql >> Databáze >  >> NoSQL >> MongoDB

Implementujte funkci automatického dokončování pomocí vyhledávání MongoDB

tl;dr

Neexistuje žádné snadné řešení pro to, co chcete, protože normální dotazy nemohou změnit pole, která vracejí. Existuje řešení (pomocí níže uvedeného mapReduce inline namísto provádění výstupu do kolekce), ale kromě velmi malých databází to není možné provést v reálném čase.

Problém

Jak je napsáno, normální dotaz nemůže ve skutečnosti upravit pole, která vrací. Jsou tu ale další problémy. Pokud chcete provést vyhledávání podle regulárního výrazu za polovinu slušného času, museli byste indexovat vše pole, která by pro tuto funkci potřebovala neúměrné množství paměti RAM. Pokud byste neindexovali vše pole by vyhledávání podle regulárního výrazu způsobilo skenování kolekce, což znamená, že by se každý dokument musel načítat z disku, což by trvalo příliš dlouho, než by bylo automatické doplňování pohodlné. Kromě toho by více současných uživatelů požadujících automatické dokončování vytvořilo značné zatížení backendu.

Řešení

Problém je velmi podobný tomu, na který jsem již odpověděl:Potřebujeme extrahovat každé slovo z více polí, odstranit zastavovací slova a uložit zbývající slova spolu s odkazem na příslušné dokumenty, ve kterých bylo slovo nalezeno ve sbírce . Nyní, abychom získali seznam automatického doplňování, jednoduše se dotazujeme na seznam indexovaných slov.

Krok 1:Pomocí úlohy mapy/zmenšení extrahujte slova

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

Spuštění tohoto mapReduce proti vašemu příkladu by vedlo k db.words vypadat takto:

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

Všimněte si, že jednotlivá slova jsou _id dokumentů. _id pole je automaticky indexováno MongoDB. Protože se indexy snažíme uchovávat v paměti RAM, můžeme udělat několik triků, jak urychlit automatické dokončování a snížit zatížení serveru.

Krok 2:Dotaz na automatické dokončování

Pro automatické doplňování potřebujeme pouze slova bez odkazů na dokumenty. Protože jsou slova indexována, používáme krytý dotaz – dotaz zodpovězený pouze z indexu, který se obvykle nachází v paměti RAM.

Abychom zůstali u vašeho příkladu, použili bychom k získání kandidátů na automatické doplňování následující dotaz:

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

což nám dává výsledek

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

Pomocí .explain() můžeme ověřit, že tento dotaz používá pouze index.

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

Všimněte si indexOnly:true pole.

Krok 3:Dotaz na skutečný dokument

I když budeme muset udělat dva dotazy, abychom získali skutečný dokument, protože urychlíme celkový proces, uživatelská zkušenost by měla být dostatečně dobrá.

Krok 3.1:Získejte dokument words kolekce

Když uživatel vybere možnost automatického doplňování, musíme se dotazovat na celý dokument se slovy, abychom našli dokumenty, ze kterých slovo vybrané pro automatické doplňování pochází.

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

výsledkem by byl dokument jako tento:

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

Krok 3.2:Získejte skutečný dokument

S tímto dokumentem nyní můžeme buď zobrazit stránku s výsledky vyhledávání, nebo, jako v tomto případě, přesměrovat na skutečný dokument, který můžete získat:

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

Poznámky

I když se tento přístup může na první pohled zdát komplikovaný (no, mapReduce je trochu), je to vlastně koncepčně docela snadné. V zásadě obchodujete s výsledky v reálném čase (které stejně nebudete mít, pokud neutratíte hodně RAM) pro rychlost. Imho, to je dobrý obchod. Aby byla poměrně nákladná fáze mapReduce efektivnější, implementace Incremental mapReduce by mohla být přístupem – vylepšení mého nepochybně hacknutého mapReduce může být jiný.

V neposlední řadě je tento způsob celkem ošklivý hack. Možná budete chtít zabrousit do elasticsearch nebo lucene. Tyto produkty jsou imho mnohem, mnohem vhodnější pro to, co chcete.




  1. Jak získat zpět původní dokument po agregaci

  2. Který typ schématu v Mongoose je nejlepší pro časové razítko?

  3. Redis:Jak protnout normální množinu se setříděnou množinou?

  4. Jak mohu použít operátor LIKE na mongoose?