MongoDB
 sql >> Baza danych >  >> NoSQL >> MongoDB

Zaimplementuj funkcję autouzupełniania za pomocą wyszukiwania MongoDB

tl;dr

Nie ma łatwego rozwiązania tego, co chcesz, ponieważ normalne zapytania nie mogą modyfikować pól, które zwracają. Istnieje rozwiązanie (za pomocą poniższego mapReduce inline zamiast tworzenia danych wyjściowych do kolekcji), ale z wyjątkiem bardzo małych baz danych, nie można tego zrobić w czasie rzeczywistym.

Problem

Jak napisano, zwykłe zapytanie nie może tak naprawdę modyfikować zwracanych pól. Ale są też inne problemy. Jeśli chcesz przeprowadzić wyszukiwanie wyrażeń regularnych w połowie przyzwoitego czasu, musisz zindeksować wszystkie pola, które wymagałyby nieproporcjonalnej ilości pamięci RAM dla tej funkcji. Gdybyś nie indeksował wszystkich pola, wyszukiwanie regex spowodowałoby skanowanie kolekcji, co oznacza, że ​​każdy dokument musiałby zostać załadowany z dysku, co zajęłoby zbyt dużo czasu, aby autouzupełnianie było wygodne. Co więcej, wielu jednoczesnych użytkowników żądających autouzupełniania spowodowałoby znaczne obciążenie zaplecza.

Rozwiązanie

Problem jest podobny do tego, na który już odpowiedziałem:musimy wyodrębnić każde słowo z wielu pól, usunąć słowa stop i zapisać pozostałe słowa wraz z linkiem do odpowiednich dokumentów, w których słowo zostało znalezione w kolekcji . Teraz, aby uzyskać listę autouzupełniania, po prostu wysyłamy zapytanie do listy zaindeksowanych słów.

Krok 1:Użyj mapy/redukuj zadanie, aby wyodrębnić słowa

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

Uruchomienie tego mapReduce na swoim przykładzie spowoduje powstanie db.words wyglądać tak:

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

Zwróć uwagę, że poszczególne słowa to _id dokumentów. _id pole jest indeksowane automatycznie przez MongoDB. Ponieważ indeksy stara się przechowywać w pamięci RAM, możemy wykonać kilka sztuczek, aby zarówno przyspieszyć autouzupełnianie, jak i zmniejszyć obciążenie serwera.

Krok 2:Zapytanie o autouzupełnianie

Do autouzupełniania potrzebujemy tylko słów, bez linków do dokumentów. Ponieważ słowa są indeksowane, używamy ukrytego zapytania – zapytania, na które odpowiedziano tylko z indeksu, który zwykle znajduje się w pamięci RAM.

Aby pozostać przy twoim przykładzie, użyjemy następującego zapytania, aby uzyskać kandydatów do autouzupełniania:

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

co daje nam wynik

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

Korzystanie z .explain() metody, możemy sprawdzić, czy to zapytanie używa tylko indeksu.

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

Zwróć uwagę na indexOnly:true pole.

Krok 3:Zapytanie o aktualny dokument

Chociaż będziemy musieli wykonać dwa zapytania, aby uzyskać rzeczywisty dokument, ponieważ przyspieszamy cały proces, wrażenia użytkownika powinny być wystarczająco dobre.

Krok 3.1:Pobierz dokument zawierający words kolekcja

Kiedy użytkownik wybierze opcję autouzupełniania, musimy przeszukać cały dokument zawierający słowa, aby znaleźć dokumenty, z których pochodzi słowo wybrane do autouzupełniania.

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

co dałoby taki dokument:

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

Krok 3.2:Pobierz aktualny dokument

Dzięki temu dokumentowi możemy teraz wyświetlić stronę z wynikami wyszukiwania lub, jak w tym przypadku, przekierować do właściwego dokumentu, który można uzyskać:

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

Notatki

Chociaż to podejście może początkowo wydawać się skomplikowane (cóż, mapReduce jest trochę), jest to całkiem proste koncepcyjnie. Zasadniczo handlujesz wynikami w czasie rzeczywistym (których i tak nie będziesz mieć, chyba że wydasz dużo RAM) dla szybkości. Imho, to dobry interes. Aby uczynić dość kosztowną fazę mapReduce bardziej wydajną, wdrożenie Incremental mapReduce może być podejściem – ulepszenie mojego, co prawda, zhakowanego mapReduce, może być innym rozwiązaniem.

Wreszcie, ten sposób jest dość brzydkim hackiem. Możesz zagłębić się w elasticsearch lub lucene. Te produkty imho są znacznie bardziej odpowiednie do tego, czego chcesz.




  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Node.js — tworzenie relacji z Mongoose

  2. MongoDB:Jak sprawdzić, czy pole tablicy zawiera element?

  3. Nie można użyć polecenia mongo, pokazuje polecenie nie znalezione na komputerze Mac

  4. Odpowiednik MongoServer.State w sterowniku 2.0

  5. Błąd:getaddrinfo ENOTFOUND w nodejs do pobrania wywołania