tl;dr
Er is geen gemakkelijke oplossing voor wat u wilt, aangezien normale zoekopdrachten de velden die ze retourneren niet kunnen wijzigen. Er is een oplossing (met behulp van de onderstaande mapReduce inline in plaats van uitvoer naar een verzameling), maar behalve voor zeer kleine databases is het niet mogelijk om dit in realtime te doen.
Het probleem
Zoals geschreven, kan een normale query de velden die hij retourneert niet echt wijzigen. Maar er zijn andere problemen. Als je een regex-zoekopdracht wilt doen in de helft van de tijd, moet je alles indexeren velden, die voor die functie een onevenredige hoeveelheid RAM nodig hebben. Als u alle niet zou indexeren velden, zou een regex-zoekopdracht een collectiescan veroorzaken, wat betekent dat elk document van de schijf zou moeten worden geladen, wat te veel tijd zou kosten om automatisch aan te vullen. Bovendien zouden meerdere gelijktijdige gebruikers die om automatische aanvulling verzoeken, een aanzienlijke belasting voor de backend veroorzaken.
De oplossing
Het probleem lijkt veel op het probleem dat ik al heb beantwoord:we moeten elk woord uit meerdere velden extraheren, de stopwoorden verwijderen en de resterende woorden opslaan samen met een link naar de respectieve document(en) het woord is gevonden in een verzameling . Voor het verkrijgen van een automatische aanvullingslijst, doorzoeken we eenvoudig de geïndexeerde woordenlijst.
Stap 1:Gebruik een kaart/reduceer-taak om de woorden te extraheren
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"
}
)
Het uitvoeren van deze mapReduce tegen uw voorbeeld zou resulteren in db.words
ziet er zo uit:
{ "_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") ] } }
Merk op dat de afzonderlijke woorden de _id
. zijn van de documenten. De _id
veld wordt automatisch geïndexeerd door MongoDB. Aangezien er wordt geprobeerd indexen in het RAM te bewaren, kunnen we een paar trucjes doen om zowel het automatisch aanvullen te versnellen als de belasting van de server te verminderen.
Stap 2:Query voor automatisch aanvullen
Voor automatische aanvulling hebben we alleen de woorden nodig, zonder de links naar de documenten. Aangezien de woorden zijn geïndexeerd, gebruiken we een gedekte zoekopdracht - een zoekopdracht die alleen wordt beantwoord vanuit de index, die zich meestal in het RAM bevindt.
Om bij uw voorbeeld te blijven, zouden we de volgende zoekopdracht gebruiken om de kandidaten voor automatische aanvulling te krijgen:
db.words.find({_id:/^can/},{_id:1})
wat ons het resultaat geeft
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
De .explain()
. gebruiken methode, kunnen we verifiëren dat deze zoekopdracht alleen de index gebruikt.
{
"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
}
Let op de indexOnly:true
veld.
Stap 3:Vraag het eigenlijke document op
Hoewel we twee query's moeten uitvoeren om het eigenlijke document te krijgen, omdat we het algehele proces versnellen, zou de gebruikerservaring goed genoeg moeten zijn.
Stap 3.1:Verkrijg het document van de words
collectie
Wanneer de gebruiker een keuze maakt voor automatisch aanvullen, moeten we het volledige document met woorden doorzoeken om de documenten te vinden waar het woord dat voor automatisch aanvullen is gekozen vandaan komt.
db.words.find({_id:"canteen"})
wat zou resulteren in een document als dit:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Stap 3.2:het eigenlijke document ophalen
Met dat document kunnen we nu ofwel een pagina met zoekresultaten tonen of, zoals in dit geval, doorverwijzen naar het eigenlijke document dat je kunt krijgen door:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Opmerkingen
Hoewel deze benadering in eerste instantie misschien ingewikkeld lijkt (nou, de mapReduce is een beetje), is het conceptueel vrij eenvoudig. Kortom, u handelt in realtime resultaten (die u sowieso niet zult hebben, tenzij u een lot uitgeeft van RAM) voor snelheid. Imho, dat is een goede deal. Om de nogal kostbare mapReduce-fase efficiënter te maken, zou het implementeren van Incremental mapReduce een benadering kunnen zijn - het verbeteren van mijn weliswaar gehackte mapReduce zou een andere kunnen zijn.
Last but not least, deze manier is helemaal een nogal lelijke hack. Misschien wilt u zich verdiepen in elasticsearch of luceen. Die producten zijn imho veel, veel meer geschikt voor wat je wilt.