tl;dr
No existe una solución fácil para lo que desea, ya que las consultas normales no pueden modificar los campos que devuelven. Hay una solución (usar el mapReduce a continuación en línea en lugar de hacer una salida a una colección), pero a excepción de bases de datos muy pequeñas, no es posible hacer esto en tiempo real.
El problema
Tal como está escrito, una consulta normal realmente no puede modificar los campos que devuelve. Pero hay otros problemas. Si desea realizar una búsqueda de expresiones regulares en un tiempo medio decente, tendría que indexar todo campos, que necesitarían una cantidad desproporcionada de RAM para esa función. Si no indexaras todos campos, una búsqueda de expresiones regulares provocaría un escaneo de colección, lo que significa que cada documento tendría que cargarse desde el disco, lo que llevaría demasiado tiempo para que el autocompletado sea conveniente. Además, varios usuarios simultáneos que solicitan el autocompletado crearían una carga considerable en el backend.
La solución
El problema es bastante similar a uno que ya he respondido:necesitamos extraer cada palabra de varios campos, eliminar las palabras vacías y guardar las palabras restantes junto con un enlace a los documentos respectivos. La palabra se encontró en una colección. . Ahora, para obtener una lista de autocompletado, simplemente consultamos la lista de palabras indexadas.
Paso 1:Use un trabajo de mapa/reducción para extraer las palabras
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"
}
)
Ejecutar este mapReduce contra su ejemplo daría como resultado db.words
luce así:
{ "_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") ] } }
Tenga en cuenta que las palabras individuales son el _id
de los documentos El _id
MongoDB indexa automáticamente el campo. Dado que se intenta mantener los índices en la RAM, podemos hacer algunos trucos para acelerar el autocompletado y reducir la carga del servidor.
Paso 2:Consulta de autocompletado
Para el autocompletado, solo necesitamos las palabras, sin los enlaces a los documentos. Dado que las palabras están indexadas, usamos una consulta cubierta, una consulta respondida solo desde el índice, que generalmente reside en la RAM.
Para seguir con su ejemplo, usaríamos la siguiente consulta para obtener los candidatos para el autocompletado:
db.words.find({_id:/^can/},{_id:1})
lo que nos da el resultado
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
Usando .explain()
método, podemos verificar que esta consulta usa solo el índice.
{
"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
}
Tenga en cuenta el indexOnly:true
campo.
Paso 3:consulta el documento real
Aunque tendremos que hacer dos consultas para obtener el documento real, ya que aceleramos el proceso general, la experiencia del usuario debería ser lo suficientemente buena.
Paso 3.1:Obtenga el documento de las words
colección
Cuando el usuario selecciona una opción de autocompletado, tenemos que consultar el documento completo de palabras para encontrar los documentos de donde se originó la palabra elegida para autocompletar.
db.words.find({_id:"canteen"})
lo que daría como resultado un documento como este:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Paso 3.2:Obtenga el documento real
Con ese documento, ahora podemos mostrar una página con resultados de búsqueda o, como en este caso, redirigir al documento real que puede obtener:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Notas
Si bien este enfoque puede parecer complicado al principio (bueno, mapReduce es un poco), es realmente bastante fácil conceptualmente. Básicamente, está negociando resultados en tiempo real (que de todos modos no tendrá a menos que gaste mucho de RAM) para la velocidad. En mi humilde opinión, eso es un buen negocio. Para hacer que la costosa fase mapReduce sea más eficiente, implementar Incremental mapReduce podría ser un enfoque; mejorar mi mapReduce ciertamente pirateado podría ser otro.
Por último, pero no menos importante, esta forma es un truco bastante feo. Es posible que desee profundizar en elasticsearch o lucene. Esos productos en mi humilde opinión son mucho, mucho más adecuados para lo que quieres.