tl;dr
Няма лесно решение за това, което искате, тъй като нормалните заявки не могат да променят полетата, които връщат. Има решение (използвайки по-долу mapReduce inline вместо да правите изход към колекция), но с изключение на много малки бази данни, не е възможно да направите това в реално време.
Проблемът
Както е написано, нормалната заявка не може наистина да промени полетата, които връща. Но има и други проблеми. Ако искате да направите търсене на регулярни изрази за половин прилично време, ще трябва да индексирате всички полета, които ще се нуждаят от непропорционално количество RAM за тази функция. Ако не искате да индексирате всички полета, търсенето на регулярни изрази би предизвикало сканиране на колекция, което означава, че всеки документ ще трябва да бъде зареден от диск, което би отнело твърде много време, за да бъде удобно автоматичното довършване. Освен това, множество едновременни потребители, които искат автоматично довършване, биха създали значително натоварване на бекенда.
Решението
Проблемът е доста подобен на този, на който вече отговорих:Трябва да извлечем всяка дума от множество полета, да премахнем спиращите думи и да запазим останалите думи заедно с връзка към съответния(ите) документ(и) думата е намерена в колекция . Сега, за да получим списък за автоматично довършване, ние просто правим заявка за индексирания списък с думи.
Стъпка 1:Използвайте задача за карта/намаляване, за да извлечете думите
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"
}
)
Изпълнението на този mapReduce спрямо вашия пример ще доведе до db.words
изглежда така:
{ "_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") ] } }
Имайте предвид, че отделните думи са _id
на документите. _id
полето се индексира автоматично от MongoDB. Тъй като индексите се опитват да се съхраняват в RAM, можем да направим няколко трика, за да ускорим автоматичното довършване и да намалим натоварването на сървъра.
Стъпка 2:Заявка за автоматично довършване
За автоматично довършване ни трябват само думите, без връзките към документите. Тъй като думите са индексирани, ние използваме покрита заявка – заявка, на която се отговаря само от индекса, който обикновено се намира в RAM.
За да се придържаме към вашия пример, ще използваме следната заявка, за да получим кандидатите за автоматично довършване:
db.words.find({_id:/^can/},{_id:1})
което ни дава резултат
{ "_id" : "can" }
{ "_id" : "canada" }
{ "_id" : "candid" }
{ "_id" : "candle" }
{ "_id" : "candy" }
{ "_id" : "cannister" }
{ "_id" : "canteen" }
{ "_id" : "canvas" }
Използване на .explain()
метод, можем да проверим, че тази заявка използва само индекса.
{
"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
}
Обърнете внимание на indexOnly:true
поле.
Стъпка 3:Потърсете действителния документ
Въпреки че ще трябва да направим две заявки, за да получим действителния документ, тъй като ускоряваме цялостния процес, потребителското изживяване трябва да е достатъчно добро.
Стъпка 3.1:Вземете документа на words
колекция
Когато потребителят избере избор на автоматично довършване, трябва да потърсим пълния документ от думи, за да намерим документите, от които произлиза избраната за автоматично довършване дума.
db.words.find({_id:"canteen"})
което би довело до документ като този:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
Стъпка 3.2:Вземете действителния документ
С този документ вече можем или да покажем страница с резултати от търсенето, или, както в този случай, да пренасочим към действителния документ, който можете да получите:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Бележки
Макар че този подход може да изглежда сложен в началото (е, mapReduce е малко), всъщност е доста лесно концептуално. По принцип търгувате с резултати в реално време (които така или иначе няма да имате, освен ако не похарчите много на RAM) за скорост. Имхо, това е добра сделка. За да направим доста скъпата фаза mapReduce по-ефективна, прилагането на Incremental mapReduce може да бъде подход – подобряването на моя хакнат mapReduce може да бъде друг.
Не на последно място, този начин е доста грозен хак като цяло. Може да искате да се поразровите в elasticsearch или lucene. Тези продукти имхо са много, много по-подходящи за това, което искате.