tl;dr
Não existe uma solução fácil para o que você deseja, pois consultas normais não podem modificar os campos que retornam. Existe uma solução (usando o mapReduce inline abaixo em vez de fazer uma saída para uma coleção), mas, exceto para bancos de dados muito pequenos, não é possível fazer isso em tempo real.
O problema
Conforme escrito, uma consulta normal não pode realmente modificar os campos que ela retorna. Mas há outros problemas. Se você quiser fazer uma pesquisa de regex em um tempo razoável, você teria que indexar todos campos, que precisariam de uma quantidade desproporcional de RAM para esse recurso. Se você não indexasse todos campos, uma pesquisa de regex causaria uma verificação de coleção, o que significa que todos os documentos teriam que ser carregados do disco, o que levaria muito tempo para que o preenchimento automático fosse conveniente. Além disso, vários usuários simultâneos solicitando preenchimento automático criariam uma carga considerável no back-end.
A solução
O problema é bem parecido com um que já respondi:Precisamos extrair cada palavra de vários campos, remover as palavras de parada e salvar as palavras restantes junto com um link para o(s) respectivo(s) documento(s) em que a palavra foi encontrada em uma coleção . Agora, para obter uma lista de preenchimento automático, simplesmente consultamos a lista de palavras indexadas.
Etapa 1:use um trabalho de mapa/redução para extrair as palavras
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"
}
)
Executar este mapReduce em seu exemplo resultaria em
db.words
parece com isso: { "_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") ] } }
Observe que as palavras individuais são o
_id
dos documentos. O _id
campo é indexado automaticamente pelo MongoDB. Como os índices tentam ser mantidos na RAM, podemos fazer alguns truques para acelerar o preenchimento automático e reduzir a carga colocada no servidor. Etapa 2:consulta para preenchimento automático
Para autocompletar, precisamos apenas das palavras, sem os links para os documentos. Como as palavras são indexadas, usamos uma consulta coberta – uma consulta respondida apenas a partir do índice, que geralmente reside na RAM.
Para continuar com seu exemplo, usaríamos a seguinte consulta para obter os candidatos para preenchimento automático:
db.words.find({_id:/^can/},{_id:1})
que nos dá o resultado
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
Usando o
.explain()
método, podemos verificar que esta consulta usa apenas o í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
}
Observe o
indexOnly:true
campo. Etapa 3:consultar o documento real
Embora tenhamos que fazer duas consultas para obter o documento real, já que aceleramos o processo geral, a experiência do usuário deve ser boa o suficiente.
Etapa 3.1:obtenha o documento das words
coleção
Quando o usuário seleciona uma opção de autocompletar, temos que consultar o documento completo de palavras para encontrar os documentos de onde se originou a palavra escolhida para autocompletar.
db.words.find({_id:"canteen"})
o que resultaria em um documento como este:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Etapa 3.2:obtenha o documento real
Com esse documento, agora podemos mostrar uma página com resultados de pesquisa ou, como neste caso, redirecionar para o documento real que você pode obter:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Observações
Embora esta abordagem possa parecer complicada no início (bem, o mapReduce é um pouco), é realmente muito fácil conceitualmente. Basicamente, você está negociando resultados em tempo real (que você não terá de qualquer maneira, a menos que gaste um muito de RAM) para velocidade. Imho, isso é um bom negócio. Para tornar a fase mapReduce bastante cara mais eficiente, implementar o mapReduce Incremental pode ser uma abordagem – melhorar meu mapReduce reconhecidamente hackeado pode ser outra.
Por último, mas não menos importante, esse caminho é um hack bastante feio. Você pode querer se aprofundar no elasticsearch ou no lucene. Esses produtos são muito, muito mais adequados para o que você deseja.