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

Implementa la funzione di completamento automatico utilizzando la ricerca MongoDB

tl;dr

Non esiste una soluzione semplice per ciò che desideri, poiché le normali query non possono modificare i campi che restituiscono. C'è una soluzione (usando l'inline mapReduce qui sotto invece di fare un output su una raccolta), ma fatta eccezione per database molto piccoli, non è possibile farlo in tempo reale.

Il problema

Come scritto, una query normale non può davvero modificare i campi che restituisce. Ma ci sono altri problemi. Se vuoi eseguire una ricerca regex in un tempo decente, dovresti indicizzare tutti campi, che richiederebbero una quantità sproporzionata di RAM per quella funzione. Se non indicizzassi tutti campi, una ricerca regex causerebbe una scansione della raccolta, il che significa che ogni documento dovrebbe essere caricato dal disco, il che richiederebbe troppo tempo perché il completamento automatico sia conveniente. Inoltre, più utenti simultanei che richiedono il completamento automatico creerebbero un carico considerevole sul back-end.

La soluzione

Il problema è abbastanza simile a quello a cui ho già risposto:dobbiamo estrarre ogni parola da più campi, rimuovere le parole di arresto e salvare le parole rimanenti insieme a un collegamento ai rispettivi documenti la parola è stata trovata in una raccolta . Ora, per ottenere un elenco di completamento automatico, interroghiamo semplicemente l'elenco di parole indicizzate.

Fase 1:usa un lavoro mappa/riduci per estrarre le parole

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

L'esecuzione di questo mapReduce rispetto al tuo esempio risulterebbe in db.words assomiglia a questo:

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

Nota che le singole parole sono il _id dei documenti. Il _id campo viene indicizzato automaticamente da MongoDB. Poiché si tenta di mantenere gli indici nella RAM, possiamo fare alcuni trucchi sia per accelerare il completamento automatico che per ridurre il carico sul server.

Passaggio 2:query per il completamento automatico

Per il completamento automatico, abbiamo bisogno solo delle parole, senza i collegamenti ai documenti. Poiché le parole sono indicizzate, utilizziamo una query coperta, una query con risposta solo dall'indice, che di solito risiede nella RAM.

Per attenersi al tuo esempio, utilizzeremo la seguente query per ottenere i candidati per il completamento automatico:

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

che ci dà il risultato

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

Usando il .explain() metodo, possiamo verificare che questa query utilizzi solo l'indice.

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

Nota il indexOnly:true campo.

Fase 3:interroga il documento vero e proprio

Anche se dovremo fare due query per ottenere il documento vero e proprio, poiché acceleriamo il processo generale, l'esperienza dell'utente dovrebbe essere abbastanza buona.

Fase 3.1:Ottieni il documento delle words raccolta

Quando l'utente seleziona una scelta per il completamento automatico, dobbiamo interrogare il documento completo di parole per trovare i documenti da cui ha avuto origine la parola scelta per il completamento automatico.

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

che risulterebbe in un documento come questo:

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

Fase 3.2:ottieni il documento vero e proprio

Con quel documento, ora possiamo mostrare una pagina con risultati di ricerca o, come in questo caso, reindirizzare al documento vero e proprio che puoi ottenere:

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

Note

Anche se questo approccio può sembrare complicato all'inizio (beh, mapReduce è un po'), è piuttosto facile concettualmente. Fondamentalmente, stai scambiando risultati in tempo reale (che comunque non avrai a meno che tu non spenda un lotto di RAM) per la velocità. Imho, è un buon affare. Al fine di rendere più efficiente la fase mapReduce, piuttosto costosa, l'implementazione di Incremental mapReduce potrebbe essere un approccio:un altro potrebbe essere il miglioramento della mia mapReduce, dichiaratamente compromessa.

Ultimo ma non meno importante, questo modo è un trucco piuttosto brutto nel complesso. Potresti voler approfondire elasticsearch o lucene. Quei prodotti imho sono molto, molto più adatti a ciò che desideri.