tl;dr
Non esiste una soluzione semplice per ciò che desideri, poiché le normali query non possono modificare i campi che restituiscono. C'è una soluzione (usando l'inline mapReduce qui sotto invece di fare un output su una raccolta), ma fatta eccezione per database molto piccoli, non è possibile farlo in tempo reale.
Il problema
Come scritto, una query normale non può davvero modificare i campi che restituisce. Ma ci sono altri problemi. Se vuoi eseguire una ricerca regex in un tempo decente, dovresti indicizzare tutti campi, che richiederebbero una quantità sproporzionata di RAM per quella funzione. Se non indicizzassi tutti campi, una ricerca regex causerebbe una scansione della raccolta, il che significa che ogni documento dovrebbe essere caricato dal disco, il che richiederebbe troppo tempo perché il completamento automatico sia conveniente. Inoltre, più utenti simultanei che richiedono il completamento automatico creerebbero un carico considerevole sul back-end.
La soluzione
Il problema è abbastanza simile a quello a cui ho già risposto:dobbiamo estrarre ogni parola da più campi, rimuovere le parole di arresto e salvare le parole rimanenti insieme a un collegamento ai rispettivi documenti la parola è stata trovata in una raccolta . Ora, per ottenere un elenco di completamento automatico, interroghiamo semplicemente l'elenco di parole indicizzate.
Fase 1:usa un lavoro mappa/riduci per estrarre le parole
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"
}
)
L'esecuzione di questo mapReduce rispetto al tuo esempio risulterebbe in db.words
assomiglia a questo:
{ "_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") ] } }
Nota che le singole parole sono il _id
dei documenti. Il _id
campo viene indicizzato automaticamente da MongoDB. Poiché si tenta di mantenere gli indici nella RAM, possiamo fare alcuni trucchi sia per accelerare il completamento automatico che per ridurre il carico sul server.
Passaggio 2:query per il completamento automatico
Per il completamento automatico, abbiamo bisogno solo delle parole, senza i collegamenti ai documenti. Poiché le parole sono indicizzate, utilizziamo una query coperta, una query con risposta solo dall'indice, che di solito risiede nella RAM.
Per attenersi al tuo esempio, utilizzeremo la seguente query per ottenere i candidati per il completamento automatico:
db.words.find({_id:/^can/},{_id:1})
che ci dà il risultato
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
Usando il .explain()
metodo, possiamo verificare che questa query utilizzi solo l'indice.
{
"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
}
Nota il indexOnly:true
campo.
Fase 3:interroga il documento vero e proprio
Anche se dovremo fare due query per ottenere il documento vero e proprio, poiché acceleriamo il processo generale, l'esperienza dell'utente dovrebbe essere abbastanza buona.
Fase 3.1:Ottieni il documento delle words
raccolta
Quando l'utente seleziona una scelta per il completamento automatico, dobbiamo interrogare il documento completo di parole per trovare i documenti da cui ha avuto origine la parola scelta per il completamento automatico.
db.words.find({_id:"canteen"})
che risulterebbe in un documento come questo:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Fase 3.2:ottieni il documento vero e proprio
Con quel documento, ora possiamo mostrare una pagina con risultati di ricerca o, come in questo caso, reindirizzare al documento vero e proprio che puoi ottenere:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Note
Anche se questo approccio può sembrare complicato all'inizio (beh, mapReduce è un po'), è piuttosto facile concettualmente. Fondamentalmente, stai scambiando risultati in tempo reale (che comunque non avrai a meno che tu non spenda un lotto di RAM) per la velocità. Imho, è un buon affare. Al fine di rendere più efficiente la fase mapReduce, piuttosto costosa, l'implementazione di Incremental mapReduce potrebbe essere un approccio:un altro potrebbe essere il miglioramento della mia mapReduce, dichiaratamente compromessa.
Ultimo ma non meno importante, questo modo è un trucco piuttosto brutto nel complesso. Potresti voler approfondire elasticsearch o lucene. Quei prodotti imho sono molto, molto più adatti a ciò che desideri.