tl;dr
Der er ingen nem løsning på det, du ønsker, da normale forespørgsler ikke kan ændre de felter, de returnerer. Der er en løsning (ved at bruge nedenstående mapReduce inline i stedet for at lave et output til en samling), men bortset fra meget små databaser, er det ikke muligt at gøre dette i realtid.
Problemet
Som skrevet kan en normal forespørgsel ikke rigtig ændre de felter, den returnerer. Men der er andre problemer. Hvis du vil lave en regex-søgning på halvvejs anstændig tid, skal du indeksere alle felter, som ville kræve en uforholdsmæssig mængde RAM til den funktion. Hvis du ikke ville indeksere alle felter, ville en regex-søgning forårsage en samlingsscanning, hvilket betyder, at hvert dokument skal indlæses fra disken, hvilket ville tage for lang tid, før autofuldførelse er praktisk. Ydermere ville flere samtidige brugere, der anmoder om autofuldførelse, skabe betydelig belastning på backend.
Løsningen
Problemet ligner meget det, jeg allerede har besvaret:Vi skal udtrække hvert ord ud af flere felter, fjerne stopordene og gemme de resterende ord sammen med et link til det eller de respektive dokumenter, ordet blev fundet i en samling . Nu, for at få en autofuldførelsesliste, forespørger vi blot på den indekserede ordliste.
Trin 1:Brug et kort/reducer job til at udtrække ordene
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"
}
)
At køre denne mapReduce mod dit eksempel ville resultere i db.words
se sådan her ud:
{ "_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") ] } }
Bemærk, at de individuelle ord er _id
af dokumenterne. _id
feltet indekseres automatisk af MongoDB. Da indekser forsøges opbevaret i RAM, kan vi lave et par tricks for både at fremskynde autofuldførelse og reducere belastningen på serveren.
Trin 2:Forespørgsel om autofuldførelse
Til autofuldførelse har vi kun brug for ordene uden links til dokumenterne. Da ordene er indekseret, bruger vi en dækket forespørgsel – en forespørgsel, der kun besvares fra indekset, som normalt ligger i RAM.
For at holde fast i dit eksempel ville vi bruge følgende forespørgsel til at få kandidaterne til autofuldførelse:
db.words.find({_id:/^can/},{_id:1})
hvilket giver os resultatet
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
Brug .explain()
metode, kan vi bekræfte, at denne forespørgsel kun bruger indekset.
{
"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
}
Bemærk indexOnly:true
felt.
Trin 3:Forespørg på det faktiske dokument
Selvom vi bliver nødt til at lave to forespørgsler for at få det faktiske dokument, da vi fremskynder den overordnede proces, bør brugeroplevelsen være god nok.
Trin 3.1:Hent dokumentet med words
indsamling
Når brugeren vælger et valg af autofuldførelse, skal vi forespørge i det komplette dokument med ord for at finde de dokumenter, hvorfra det valgte ord til autofuldførelse stammer fra.
db.words.find({_id:"canteen"})
hvilket ville resultere i et dokument som dette:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Trin 3.2:Hent det faktiske dokument
Med det dokument kan vi nu enten vise en side med søgeresultater eller, som i dette tilfælde, omdirigere til det faktiske dokument, som du kan få ved at:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Bemærkninger
Selvom denne tilgang kan virke kompliceret i starten (vel, mapReduce er lidt), er det faktisk ret nemt konceptuelt. Dybest set handler du med resultater i realtid (som du alligevel ikke får, medmindre du bruger masse RAM) for hastighed. Imho, det er en god aftale. For at gøre den temmelig dyre mapReduce-fase mere effektiv, kunne implementering af Incremental mapReduce være en fremgangsmåde – at forbedre mit ganske vist hackede mapReduce kunne meget vel være en anden.
Sidst men ikke mindst er denne måde et ret grimt hack i det hele taget. Du vil måske grave i elasticsearch eller lucene. Disse produkter er imho meget, meget mere egnede til det, du ønsker.