tl;dr
Neexistuje žádné snadné řešení pro to, co chcete, protože normální dotazy nemohou změnit pole, která vracejí. Existuje řešení (pomocí níže uvedeného mapReduce inline namísto provádění výstupu do kolekce), ale kromě velmi malých databází to není možné provést v reálném čase.
Problém
Jak je napsáno, normální dotaz nemůže ve skutečnosti upravit pole, která vrací. Jsou tu ale další problémy. Pokud chcete provést vyhledávání podle regulárního výrazu za polovinu slušného času, museli byste indexovat vše pole, která by pro tuto funkci potřebovala neúměrné množství paměti RAM. Pokud byste neindexovali vše pole by vyhledávání podle regulárního výrazu způsobilo skenování kolekce, což znamená, že by se každý dokument musel načítat z disku, což by trvalo příliš dlouho, než by bylo automatické doplňování pohodlné. Kromě toho by více současných uživatelů požadujících automatické dokončování vytvořilo značné zatížení backendu.
Řešení
Problém je velmi podobný tomu, na který jsem již odpověděl:Potřebujeme extrahovat každé slovo z více polí, odstranit zastavovací slova a uložit zbývající slova spolu s odkazem na příslušné dokumenty, ve kterých bylo slovo nalezeno ve sbírce . Nyní, abychom získali seznam automatického doplňování, jednoduše se dotazujeme na seznam indexovaných slov.
Krok 1:Pomocí úlohy mapy/zmenšení extrahujte slova
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"
}
)
Spuštění tohoto mapReduce proti vašemu příkladu by vedlo k db.words
vypadat takto:
{ "_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") ] } }
Všimněte si, že jednotlivá slova jsou _id
dokumentů. _id
pole je automaticky indexováno MongoDB. Protože se indexy snažíme uchovávat v paměti RAM, můžeme udělat několik triků, jak urychlit automatické dokončování a snížit zatížení serveru.
Krok 2:Dotaz na automatické dokončování
Pro automatické doplňování potřebujeme pouze slova bez odkazů na dokumenty. Protože jsou slova indexována, používáme krytý dotaz – dotaz zodpovězený pouze z indexu, který se obvykle nachází v paměti RAM.
Abychom zůstali u vašeho příkladu, použili bychom k získání kandidátů na automatické doplňování následující dotaz:
db.words.find({_id:/^can/},{_id:1})
což nám dává výsledek
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
Pomocí .explain()
můžeme ověřit, že tento dotaz používá pouze 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
}
Všimněte si indexOnly:true
pole.
Krok 3:Dotaz na skutečný dokument
I když budeme muset udělat dva dotazy, abychom získali skutečný dokument, protože urychlíme celkový proces, uživatelská zkušenost by měla být dostatečně dobrá.
Krok 3.1:Získejte dokument words
kolekce
Když uživatel vybere možnost automatického doplňování, musíme se dotazovat na celý dokument se slovy, abychom našli dokumenty, ze kterých slovo vybrané pro automatické doplňování pochází.
db.words.find({_id:"canteen"})
výsledkem by byl dokument jako tento:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Krok 3.2:Získejte skutečný dokument
S tímto dokumentem nyní můžeme buď zobrazit stránku s výsledky vyhledávání, nebo, jako v tomto případě, přesměrovat na skutečný dokument, který můžete získat:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Poznámky
I když se tento přístup může na první pohled zdát komplikovaný (no, mapReduce je trochu), je to vlastně koncepčně docela snadné. V zásadě obchodujete s výsledky v reálném čase (které stejně nebudete mít, pokud neutratíte hodně RAM) pro rychlost. Imho, to je dobrý obchod. Aby byla poměrně nákladná fáze mapReduce efektivnější, implementace Incremental mapReduce by mohla být přístupem – vylepšení mého nepochybně hacknutého mapReduce může být jiný.
V neposlední řadě je tento způsob celkem ošklivý hack. Možná budete chtít zabrousit do elasticsearch nebo lucene. Tyto produkty jsou imho mnohem, mnohem vhodnější pro to, co chcete.