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

Mostra solo i campi corrispondenti per la ricerca di testo MongoDB

Dopo averci pensato a lungo, penso che sia possibile realizzare ciò che vuoi. Tuttavia, non è adatto per database molto grandi e non ho ancora elaborato un approccio incrementale. Manca di stemming e le parole di stop devono essere definite manualmente.

L'idea è di utilizzare mapReduce per creare una raccolta di parole di ricerca con riferimenti al documento di origine e al campo da cui ha avuto origine la parola di ricerca. Quindi, poiché la query effettiva per il completamento automatico viene eseguita utilizzando una semplice aggregazione che utilizza un indice e quindi dovrebbe essere piuttosto veloce.

Quindi lavoreremo con i seguenti tre documenti

{
  "name" : "John F. Kennedy",
  "address" : "Kenson Street 1, 12345 Footown, TX, USA",
  "note" : "loves Kendo and Sushi"
}

e

{
  "name" : "Robert F. Kennedy",
  "address" : "High Street 1, 54321 Bartown, FL, USA",
  "note" : "loves Ethel and cigars"
}

e

{
  "name" : "Robert F. Sushi",
  "address" : "Sushi Street 1, 54321 Bartown, FL, USA",
  "note" : "loves Sushi and more Sushi"
}

in una raccolta chiamata textsearch .

La fase mappa/riduzione

Fondamentalmente, elaboreremo ogni singola parola in uno dei tre campi, rimuoveremo le parole e i numeri di arresto e salveremo ogni singola parola con il _id del documento e il campo dell'occorrenza in una tabella intermedia.

Il codice annotato:

db.textsearch.mapReduce(
  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"];

    // This denotes the fields which should be processed
    var fields = ["name","address","note"];

    // For each field...
    fields.forEach(

      function(field){

        // ... we split the field into single words...
        var words = (document[field]).split(" ");

        words.forEach(

          function(word){
            // ...and remove unwanted characters.
            // Please note that this regex may well need to be enhanced
            var cleaned = word.replace(/[;,.]/g,"")

            // Next we check...
            if(
              // ...wether the current word is in the stopwords list,...
              (stopwords.indexOf(word)>-1) ||

              // ...is either a float or an integer... 
              !(isNaN(parseInt(cleaned))) ||
              !(isNaN(parseFloat(cleaned))) ||

              // or is only one character.
              cleaned.length < 2
            )
            {
              // In any of those cases, we do not want to have the current word in our list.
              return
            }
              // Otherwise, we want to have the current word processed.
              // Note that we have to use a multikey id and a static field in order
              // to overcome one of MongoDB's mapReduce limitations:
              // it can not have multiple values assigned to a key.
              emit({'word':cleaned,'doc':document._id,'field':field},1)

          }
        )
      }
    )
  },
  function(key,values) {

    // We sum up each occurence of each word
    // in each field in every document...
    return Array.sum(values);
  },
    // ..and write the result to a collection
  {out: "searchtst" }
)

L'esecuzione comporterà la creazione della raccolta searchtst . Se esisteva già, tutti i suoi contenuti verranno sostituiti.

Sarà simile a questo:

{ "_id" : { "word" : "Bartown", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Bartown", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Ethel", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "note" }, "value" : 1 }
{ "_id" : { "word" : "FL", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "FL", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Footown", "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "address" }, "value" : 1 }
[...]
{ "_id" : { "word" : "Sushi", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "name" }, "value" : 1 }
{ "_id" : { "word" : "Sushi", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "note" }, "value" : 2 }
[...]

Ci sono alcune cose da notare qui. Innanzitutto, una parola può avere più occorrenze, ad esempio con "FL". Tuttavia, potrebbe trovarsi in documenti diversi, come è il caso qui. Una parola può anche avere più occorrenze in un singolo campo di un singolo documento, d'altra parte. Lo useremo a nostro vantaggio in seguito.

In secondo luogo, abbiamo tutti i campi, in particolare la word campo in un indice composto per _id , il che dovrebbe rendere le query in arrivo piuttosto veloci. Tuttavia, questo significa anche che l'indice sarà piuttosto grande e, come per tutti gli indici, tenderà a consumare RAM.

La fase di aggregazione

Quindi abbiamo ridotto l'elenco delle parole. Ora interroghiamo una (sotto)stringa. Quello che dobbiamo fare è trovare tutte le parole che iniziano con la stringa che l'utente ha digitato fino a quel momento, restituendo un elenco di parole che corrispondono a quella stringa. Per poterlo fare e per ottenere i risultati in una forma a noi adatta, utilizziamo un'aggregazione.

Questa aggregazione dovrebbe essere abbastanza veloce, poiché tutti i campi necessari per eseguire query fanno parte di un indice composto.

Ecco l'aggregazione annotata per il caso in cui l'utente ha digitato la lettera S :

db.searchtst.aggregate(
  // We match case insensitive ("i") as we want to prevent
  // typos to reduce our search results
  { $match:{"_id.word":/^S/i} },
  { $group:{
      // Here is where the magic happens:
      // we create a list of distinct words...
      _id:"$_id.word",
      occurrences:{
        // ...add each occurrence to an array...
        $push:{
          doc:"$_id.doc",
          field:"$_id.field"
        } 
      },
      // ...and add up all occurrences to a score
      // Note that this is optional and might be skipped
      // to speed up things, as we should have a covered query
      // when not accessing $value, though I am not too sure about that
      score:{$sum:"$value"}
    }
  },
  {
    // Optional. See above
    $sort:{_id:-1,score:1}
  }
)

Il risultato di questa query è simile a questo e dovrebbe essere abbastanza autoesplicativo:

{
  "_id" : "Sushi",
  "occurences" : [
    { "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "note" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "name" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "note" }
  ],
  "score" : 5
}
{
  "_id" : "Street",
  "occurences" : [
    { "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "address" },
    { "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" },
    { "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }
  ],
  "score" : 3
}

Il punteggio di 5 per Sushi deriva dal fatto che la parola Sushi compare due volte nel campo delle note di uno dei documenti. Questo è il comportamento previsto.

Sebbene questa possa essere una soluzione scadente, deve essere ottimizzata per la miriade di casi d'uso pensabili e avrebbe bisogno di un mapReduce incrementale da implementare per essere a metà strada utile negli ambienti di produzione, funziona come previsto. hth.

Modifica

Naturalmente, si potrebbe eliminare il $match stage e aggiungi un $out fase nella fase di aggregazione per avere i risultati preelaborati:

db.searchtst.aggregate(
  {
    $group:{
      _id:"$_id.word",
      occurences:{ $push:{doc:"$_id.doc",field:"$_id.field"}},
      score:{$sum:"$value"}
     }
   },{
     $out:"search"
   })

Ora possiamo interrogare la search risultante raccolta per velocizzare le cose. Fondamentalmente scambi i risultati in tempo reale con la velocità.

Modifica 2 :Nel caso in cui venga adottato l'approccio di preelaborazione, il searchtst raccolta dell'esempio dovrebbe essere cancellata al termine dell'aggregazione per risparmiare spazio su disco e, cosa più importante, preziosa RAM.