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.