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

Mostrar solo campos coincidentes para la búsqueda de texto MongoDB

Después de pensar en esto mucho tiempo, creo que es posible implementar lo que quieres. Sin embargo, no es adecuado para bases de datos muy grandes y aún no he desarrollado un enfoque incremental. Carece de lematización y las palabras vacías deben definirse manualmente.

La idea es usar mapReduce para crear una colección de palabras de búsqueda con referencias al documento de origen y el campo de donde se originó la palabra de búsqueda. Luego, la consulta real para el autocompletado se realiza mediante una agregación simple que utiliza un índice y, por lo tanto, debería ser bastante rápida.

Entonces trabajaremos con los siguientes tres documentos

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

y

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

y

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

en una colección llamada textsearch .

El mapa/etapa de reducción

Básicamente, lo que hacemos es que procesaremos todas y cada una de las palabras en uno de los tres campos, eliminaremos las palabras vacías y los números y guardaremos todas y cada una de las palabras con el _id del documento. y el campo de la ocurrencia en una tabla intermedia.

El código anotado:

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

Ejecutar esto resultará en la creación de la colección searchtst . Si ya existía, todo su contenido será reemplazado.

Se verá algo como esto:

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

Hay algunas cosas a tener en cuenta aquí. En primer lugar, una palabra puede tener varias apariciones, por ejemplo, con "FL". Sin embargo, puede estar en documentos diferentes, como es el caso aquí. Por otro lado, una palabra también puede tener múltiples ocurrencias en un solo campo de un solo documento. Usaremos esto a nuestro favor más adelante.

En segundo lugar, tenemos todos los campos, sobre todo la word campo en un índice compuesto para _id , lo que debería hacer que las próximas consultas sean bastante rápidas. Sin embargo, esto también significa que el índice será bastante grande y, como ocurre con todos los índices, tiende a consumir RAM.

La etapa de agregación

Así que hemos reducido la lista de palabras. Ahora buscamos una (sub)cadena. Lo que debemos hacer es encontrar todas las palabras que comiencen con la cadena que el usuario escribió hasta el momento, devolviendo una lista de palabras que coincidan con esa cadena. Para poder hacer esto y obtener los resultados en una forma adecuada para nosotros, usamos una agregación.

Esta agregación debería ser bastante rápida, ya que todos los campos necesarios para consultar forman parte de un índice compuesto.

Aquí está la agregación anotada para el caso cuando el usuario escribió la letra 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}
  }
)

El resultado de esta consulta se parece a esto y debería explicarse por sí mismo:

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

La puntuación de 5 para Sushi proviene del hecho de que la palabra Sushi aparece dos veces en el campo de notas de uno de los documentos. Este es un comportamiento previsto.

Si bien esta puede ser una solución para pobres, debe optimizarse para la miríada de casos de uso imaginables y necesitaría implementar un mapReduce incremental para ser medio útil en entornos de producción, funciona como se esperaba. h.

Editar

Por supuesto, uno podría eliminar el $match escenario y agregue un $out etapa en la fase de agregación para tener los resultados preprocesados:

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

Ahora, podemos consultar la search resultante colección para acelerar las cosas. Básicamente, intercambias resultados en tiempo real por velocidad.

Editar 2 :En caso de que se adopte el enfoque de preprocesamiento, el searchtst la colección del ejemplo debe eliminarse una vez finalizada la agregación para ahorrar espacio en disco y, lo que es más importante, RAM valiosa.