tl;dr
Det finns ingen enkel lösning för vad du vill ha, eftersom vanliga frågor inte kan ändra fälten de returnerar. Det finns en lösning (med hjälp av nedanstående mapReduce inline istället för att göra en utdata till en samling), men förutom för mycket små databaser är det inte möjligt att göra detta i realtid.
Problemet
Som skrivet kan en normal fråga inte riktigt ändra fälten den returnerar. Men det finns andra problem. Om du vill göra en regex-sökning på halvvägs anständig tid, måste du indexera alla fält, som skulle behöva en oproportionerligt mycket RAM-minne för den funktionen. Om du inte skulle indexera alla fält, skulle en regex-sökning orsaka en samlingsskanning, vilket innebär att varje dokument måste laddas från disken, vilket skulle ta för lång tid för autokomplettering för att vara bekvämt. Dessutom skulle flera samtidiga användare som begär autokompletterande skapa avsevärd belastning på backend.
Lösningen
Problemet är ganska likt ett jag redan har svarat på:Vi måste extrahera varje ord ur flera fält, ta bort stopporden och spara de återstående orden tillsammans med en länk till respektive dokument som ordet hittades i en samling . Nu, för att få en autokompletteringslista, frågar vi helt enkelt den indexerade ordlistan.
Steg 1:Använd ett karta/förminska jobb för att extrahera orden
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"
}
)
Att köra denna mapReduce mot ditt exempel skulle resultera i db.words
se ut så här:
{ "_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") ] } }
Observera att de enskilda orden är _id
av dokumenten. _id
fältet indexeras automatiskt av MongoDB. Eftersom index försöker hållas i RAM, kan vi göra några knep för att både påskynda autokomplettering och minska belastningen på servern.
Steg 2:Fråga efter autoslutförande
För autokomplettering behöver vi bara orden, utan länkarna till dokumenten. Eftersom orden är indexerade använder vi en täckt fråga – en fråga som bara besvaras från indexet, som vanligtvis finns i RAM.
För att hålla fast vid ditt exempel skulle vi använda följande fråga för att få kandidaterna för autokomplettering:
db.words.find({_id:/^can/},{_id:1})
vilket ger oss resultatet
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
Använd .explain()
metod kan vi verifiera att denna fråga bara använder indexet.
{
"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
}
Notera indexOnly:true
fältet.
Steg 3:Fråga själva dokumentet
Även om vi kommer att behöva göra två frågor för att få det faktiska dokumentet, eftersom vi påskyndar den övergripande processen, bör användarupplevelsen vara tillräckligt bra.
Steg 3.1:Hämta dokumentet med words
samling
När användaren väljer ett val av autokomplettering måste vi fråga efter det fullständiga dokumentet med ord för att hitta dokumenten varifrån ordet som valts för autokomplettering kommer från.
db.words.find({_id:"canteen"})
vilket skulle resultera i ett dokument som detta:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Steg 3.2:Hämta själva dokumentet
Med det dokumentet kan vi nu antingen visa en sida med sökresultat eller, som i det här fallet, omdirigera till det faktiska dokumentet som du kan få genom att:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Anteckningar
Även om detta tillvägagångssätt kan tyckas komplicerat till en början (tja, mapReduce är lite), är det faktiskt ganska lätt konceptuellt. I grund och botten handlar du med realtidsresultat (vilket du ändå inte kommer att få om du inte spenderar mycket RAM) för hastighet. Imho, det är en bra affär. För att göra den ganska kostsamma mapReduce-fasen mer effektiv kan det vara ett sätt att implementera Incremental mapReduce – att förbättra min visserligen hackade mapReduce kan mycket väl vara en annan.
Sist men inte minst är det här sättet ett ganska fult hack helt och hållet. Du kanske vill gräva i elasticsearch eller lucene. Dessa produkter är mycket, mycket mer lämpade för vad du vill ha.