MongoDB
 sql >> Base de Dados >  >> NoSQL >> MongoDB

Implemente o recurso de preenchimento automático usando a pesquisa do MongoDB

tl;dr


Não existe uma solução fácil para o que você deseja, pois consultas normais não podem modificar os campos que retornam. Existe uma solução (usando o mapReduce inline abaixo em vez de fazer uma saída para uma coleção), mas, exceto para bancos de dados muito pequenos, não é possível fazer isso em tempo real.

O problema


Conforme escrito, uma consulta normal não pode realmente modificar os campos que ela retorna. Mas há outros problemas. Se você quiser fazer uma pesquisa de regex em um tempo razoável, você teria que indexar todos campos, que precisariam de uma quantidade desproporcional de RAM para esse recurso. Se você não indexasse todos campos, uma pesquisa de regex causaria uma verificação de coleção, o que significa que todos os documentos teriam que ser carregados do disco, o que levaria muito tempo para que o preenchimento automático fosse conveniente. Além disso, vários usuários simultâneos solicitando preenchimento automático criariam uma carga considerável no back-end.

A solução


O problema é bem parecido com um que já respondi:Precisamos extrair cada palavra de vários campos, remover as palavras de parada e salvar as palavras restantes junto com um link para o(s) respectivo(s) documento(s) em que a palavra foi encontrada em uma coleção . Agora, para obter uma lista de preenchimento automático, simplesmente consultamos a lista de palavras indexadas.

Etapa 1:use um trabalho de mapa/redução para extrair as palavras

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

Executar este mapReduce em seu exemplo resultaria em db.words parece com isso:
    { "_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") ] } }

Observe que as palavras individuais são o _id dos documentos. O _id campo é indexado automaticamente pelo MongoDB. Como os índices tentam ser mantidos na RAM, podemos fazer alguns truques para acelerar o preenchimento automático e reduzir a carga colocada no servidor.

Etapa 2:consulta para preenchimento automático


Para autocompletar, precisamos apenas das palavras, sem os links para os documentos. Como as palavras são indexadas, usamos uma consulta coberta – uma consulta respondida apenas a partir do índice, que geralmente reside na RAM.

Para continuar com seu exemplo, usaríamos a seguinte consulta para obter os candidatos para preenchimento automático:
db.words.find({_id:/^can/},{_id:1})

que nos dá o resultado
    { "_id" : "can" }
    { "_id" : "canada" }
    { "_id" : "candid" }
    { "_id" : "candle" }
    { "_id" : "candy" }
    { "_id" : "cannister" }
    { "_id" : "canteen" }
    { "_id" : "canvas" }

Usando o .explain() método, podemos verificar que esta consulta usa apenas o índice.
        {
        "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
    }

Observe o indexOnly:true campo.

Etapa 3:consultar o documento real


Embora tenhamos que fazer duas consultas para obter o documento real, já que aceleramos o processo geral, a experiência do usuário deve ser boa o suficiente.

Etapa 3.1:obtenha o documento das words coleção


Quando o usuário seleciona uma opção de autocompletar, temos que consultar o documento completo de palavras para encontrar os documentos de onde se originou a palavra escolhida para autocompletar.
db.words.find({_id:"canteen"})

o que resultaria em um documento como este:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }

Etapa 3.2:obtenha o documento real


Com esse documento, agora podemos mostrar uma página com resultados de pesquisa ou, como neste caso, redirecionar para o documento real que você pode obter:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})

Observações


Embora esta abordagem possa parecer complicada no início (bem, o mapReduce é um pouco), é realmente muito fácil conceitualmente. Basicamente, você está negociando resultados em tempo real (que você não terá de qualquer maneira, a menos que gaste um muito de RAM) para velocidade. Imho, isso é um bom negócio. Para tornar a fase mapReduce bastante cara mais eficiente, implementar o mapReduce Incremental pode ser uma abordagem – melhorar meu mapReduce reconhecidamente hackeado pode ser outra.

Por último, mas não menos importante, esse caminho é um hack bastante feio. Você pode querer se aprofundar no elasticsearch ou no lucene. Esses produtos são muito, muito mais adequados para o que você deseja.