MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Afficher uniquement les champs correspondants pour la recherche de texte MongoDB

Après avoir longuement réfléchi à cela, je pense qu'il est possible de mettre en œuvre ce que vous voulez. Cependant, il ne convient pas aux très grandes bases de données et je n'ai pas encore élaboré d'approche incrémentielle. Il n'y a pas de radical et les mots vides doivent être définis manuellement.

L'idée est d'utiliser mapReduce pour créer une collection de mots de recherche avec des références au document d'origine et au champ d'où provient le mot de recherche. Ensuite, pour la requête proprement dite, l'auto-complétion est effectuée à l'aide d'une simple agrégation qui utilise un index et, par conséquent, devrait être assez rapide.

Nous allons donc travailler avec les trois documents suivants

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

et

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

et

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

dans une collection appelée textsearch .

L'étape mapper/réduire

Ce que nous faisons essentiellement, c'est que nous traiterons chaque mot dans l'un des trois champs, supprimerons les mots vides et les chiffres et enregistrerons chaque mot avec le _id du document et le champ de l'occurrence dans une table intermédiaire.

Le code annoté :

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

Son exécution entraînera la création de la collection searchtst . S'il existait déjà, tout son contenu sera remplacé.

Cela ressemblera à ceci :

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

Il y a quelques choses à noter ici. Tout d'abord, un mot peut avoir plusieurs occurrences, par exemple avec "FL". Cependant, il peut se trouver dans des documents différents, comme c'est le cas ici. Un mot peut également avoir plusieurs occurrences dans un même champ d'un même document, d'autre part. Nous utiliserons cela à notre avantage plus tard.

Deuxièmement, nous avons tous les champs, notamment le word champ dans un index composé pour _id , ce qui devrait accélérer les requêtes à venir. Cependant, cela signifie également que l'index sera assez volumineux et - comme pour tous les index - a tendance à consommer de la RAM.

L'étape d'agrégation

Nous avons donc réduit la liste des mots. Maintenant, nous recherchons une (sous-)chaîne. Ce que nous devons faire est de trouver tous les mots commençant par la chaîne que l'utilisateur a tapée jusqu'à présent, en renvoyant une liste de mots correspondant à cette chaîne. Afin de pouvoir le faire et d'obtenir les résultats sous une forme qui nous convient, nous utilisons une agrégation.

Cette agrégation devrait être assez rapide, puisque tous les champs nécessaires à interroger font partie d'un index composé.

Voici l'agrégation annotée pour le cas où l'utilisateur a tapé la lettre 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}
  }
)

Le résultat de cette requête ressemble à ceci et devrait être assez explicite :

{
  "_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 note de 5 pour Sushi vient du fait que le mot Sushi apparaît deux fois dans le champ note d'un des documents. C'est un comportement intentionnel.

Bien que cela puisse être une solution de pauvre homme, doit être optimisé pour les myriades de cas d'utilisation imaginables et nécessiterait l'implémentation d'un mapReduce incrémentiel afin d'être à mi-chemin utile dans les environnements de production, cela fonctionne comme prévu. hth.

Modifier

Bien sûr, on pourrait laisser tomber le $match étape et ajoutez un $out étape dans la phase d'agrégation afin d'avoir les résultats prétraités :

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

Maintenant, nous pouvons interroger la search résultante collecte afin d'accélérer les choses. En gros, vous échangez des résultats en temps réel contre de la vitesse.

Modifier 2  :Si l'approche de prétraitement est adoptée, le searchtst La collection de l'exemple doit être supprimée une fois l'agrégation terminée afin d'économiser à la fois de l'espace disque et, plus important encore, de la précieuse RAM.