sql >> Base de Datos >  >> NoSQL >> MongoDB

Implementar la función de autocompletar usando la búsqueda de MongoDB

tl;dr

No existe una solución fácil para lo que desea, ya que las consultas normales no pueden modificar los campos que devuelven. Hay una solución (usar el mapReduce a continuación en línea en lugar de hacer una salida a una colección), pero a excepción de bases de datos muy pequeñas, no es posible hacer esto en tiempo real.

El problema

Tal como está escrito, una consulta normal realmente no puede modificar los campos que devuelve. Pero hay otros problemas. Si desea realizar una búsqueda de expresiones regulares en un tiempo medio decente, tendría que indexar todo campos, que necesitarían una cantidad desproporcionada de RAM para esa función. Si no indexaras todos campos, una búsqueda de expresiones regulares provocaría un escaneo de colección, lo que significa que cada documento tendría que cargarse desde el disco, lo que llevaría demasiado tiempo para que el autocompletado sea conveniente. Además, varios usuarios simultáneos que solicitan el autocompletado crearían una carga considerable en el backend.

La solución

El problema es bastante similar a uno que ya he respondido:necesitamos extraer cada palabra de varios campos, eliminar las palabras vacías y guardar las palabras restantes junto con un enlace a los documentos respectivos. La palabra se encontró en una colección. . Ahora, para obtener una lista de autocompletado, simplemente consultamos la lista de palabras indexadas.

Paso 1:Use un trabajo de mapa/reducción para extraer las palabras

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

Ejecutar este mapReduce contra su ejemplo daría como resultado db.words luce así:

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

Tenga en cuenta que las palabras individuales son el _id de los documentos El _id MongoDB indexa automáticamente el campo. Dado que se intenta mantener los índices en la RAM, podemos hacer algunos trucos para acelerar el autocompletado y reducir la carga del servidor.

Paso 2:Consulta de autocompletado

Para el autocompletado, solo necesitamos las palabras, sin los enlaces a los documentos. Dado que las palabras están indexadas, usamos una consulta cubierta, una consulta respondida solo desde el índice, que generalmente reside en la RAM.

Para seguir con su ejemplo, usaríamos la siguiente consulta para obtener los candidatos para el autocompletado:

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

lo que nos da el resultado

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

Usando .explain() método, podemos verificar que esta consulta usa solo el í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
    }

Tenga en cuenta el indexOnly:true campo.

Paso 3:consulta el documento real

Aunque tendremos que hacer dos consultas para obtener el documento real, ya que aceleramos el proceso general, la experiencia del usuario debería ser lo suficientemente buena.

Paso 3.1:Obtenga el documento de las words colección

Cuando el usuario selecciona una opción de autocompletado, tenemos que consultar el documento completo de palabras para encontrar los documentos de donde se originó la palabra elegida para autocompletar.

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

lo que daría como resultado un documento como este:

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

Paso 3.2:Obtenga el documento real

Con ese documento, ahora podemos mostrar una página con resultados de búsqueda o, como en este caso, redirigir al documento real que puede obtener:

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

Notas

Si bien este enfoque puede parecer complicado al principio (bueno, mapReduce es un poco), es realmente bastante fácil conceptualmente. Básicamente, está negociando resultados en tiempo real (que de todos modos no tendrá a menos que gaste mucho de RAM) para la velocidad. En mi humilde opinión, eso es un buen negocio. Para hacer que la costosa fase mapReduce sea más eficiente, implementar Incremental mapReduce podría ser un enfoque; mejorar mi mapReduce ciertamente pirateado podría ser otro.

Por último, pero no menos importante, esta forma es un truco bastante feo. Es posible que desee profundizar en elasticsearch o lucene. Esos productos en mi humilde opinión son mucho, mucho más adecuados para lo que quieres.