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

Implémenter la fonctionnalité de saisie semi-automatique à l'aide de la recherche MongoDB

tl;dr

Il n'y a pas de solution simple pour ce que vous voulez, car les requêtes normales ne peuvent pas modifier les champs qu'elles renvoient. Il existe une solution (en utilisant le mapReduce ci-dessous au lieu de faire une sortie vers une collection), mais à l'exception des très petites bases de données, il n'est pas possible de le faire en temps réel.

Le problème

Comme écrit, une requête normale ne peut pas vraiment modifier les champs qu'elle renvoie. Mais il y a d'autres problèmes. Si vous voulez faire une recherche de regex à mi-chemin, vous devrez indexer tous champs, ce qui nécessiterait une quantité disproportionnée de RAM pour cette fonctionnalité. Si vous n'indexiez pas tous champs, une recherche regex entraînerait une analyse de collection, ce qui signifie que chaque document devrait être chargé à partir du disque, ce qui prendrait trop de temps pour que la saisie semi-automatique soit pratique. De plus, plusieurs utilisateurs simultanés demandant la saisie semi-automatique créeraient une charge considérable sur le backend.

La solution

Le problème est assez similaire à celui auquel j'ai déjà répondu :nous devons extraire chaque mot de plusieurs champs, supprimer les mots vides et enregistrer les mots restants avec un lien vers le ou les documents respectifs où le mot a été trouvé dans une collection . Maintenant, pour obtenir une liste de saisie semi-automatique, nous interrogeons simplement la liste de mots indexés.

Étape 1 :Utilisez une tâche de mappage/réduction pour extraire les mots

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

L'exécution de ce mapReduce sur votre exemple entraînerait db.words ressembler à ceci :

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

Notez que les mots individuels sont le _id des documents. Le _id Le champ est indexé automatiquement par MongoDB. Étant donné que les index sont essayés pour être conservés dans la RAM, nous pouvons faire quelques astuces pour à la fois accélérer l'auto-complétion et réduire la charge sur le serveur.

Étape 2 :Requête pour la saisie semi-automatique

Pour la saisie semi-automatique, nous n'avons besoin que des mots, sans les liens vers les documents. Puisque les mots sont indexés, nous utilisons une requête couverte - une requête répondue uniquement à partir de l'index, qui réside généralement dans la RAM.

Pour rester dans votre exemple, nous utiliserions la requête suivante pour obtenir les candidats à la saisie semi-automatique :

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

qui nous donne le résultat

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

Utilisation de .explain() méthode, nous pouvons vérifier que cette requête utilise uniquement l'index.

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

Notez le indexOnly:true champ.

Étape 3 :Interrogez le document réel

Bien que nous devions effectuer deux requêtes pour obtenir le document réel, puisque nous accélérons le processus global, l'expérience utilisateur devrait être assez bonne.

Étape 3.1 :Obtenir le document des words collection

Lorsque l'utilisateur sélectionne un choix de l'auto-complétion, nous devons interroger le document complet de mots afin de trouver les documents d'où provient le mot choisi pour l'auto-complétion.

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

ce qui donnerait un document comme celui-ci :

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

Étape 3.2 :Obtenez le document réel

Avec ce document, nous pouvons maintenant afficher une page avec les résultats de la recherche ou, comme dans ce cas, rediriger vers le document réel que vous pouvez obtenir :

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

Remarques

Bien que cette approche puisse sembler compliquée au premier abord (eh bien, le mapReduce est un peu), c'est en fait assez facile conceptuellement. Fondamentalement, vous négociez des résultats en temps réel (que vous n'aurez de toute façon pas à moins que vous dépensiez beaucoup beaucoup de RAM) pour la vitesse. À mon humble avis, c'est une bonne affaire. Afin de rendre plus efficace la phase plutôt coûteuse de mapReduce, la mise en œuvre de mapReduce incrémentiel pourrait être une approche - améliorer mon mapReduce, certes piraté, pourrait bien en être une autre.

Enfin et surtout, cette méthode est un piratage plutôt moche. Vous voudrez peut-être creuser dans elasticsearch ou lucene. Ces produits à mon humble avis sont beaucoup, beaucoup plus adaptés à ce que vous voulez.