След като обмислях това дълго време, смятам, че е възможно да приложите това, което искате. Той обаче не е подходящ за много големи бази данни и все още не съм разработил постепенен подход. Липсва корен и спиращите думи трябва да се дефинират ръчно.
Идеята е да се използва mapReduce за създаване на колекция от думи за търсене с препратки към документа на произход и полето, откъдето произхожда думата за търсене. След това, за действителната заявка за автоматично довършване се извършва с помощта на просто агрегиране, което използва индекс и следователно трябва да бъде доста бързо.
Така че ще работим със следните три документа
{
"name" : "John F. Kennedy",
"address" : "Kenson Street 1, 12345 Footown, TX, USA",
"note" : "loves Kendo and Sushi"
}
и
{
"name" : "Robert F. Kennedy",
"address" : "High Street 1, 54321 Bartown, FL, USA",
"note" : "loves Ethel and cigars"
}
и
{
"name" : "Robert F. Sushi",
"address" : "Sushi Street 1, 54321 Bartown, FL, USA",
"note" : "loves Sushi and more Sushi"
}
в колекция, наречена textsearch
.
Етапът карта/намаляване
Това, което основно правим, е, че ще обработим всяка дума в едно от трите полета, ще премахнем стоп думите и числата и ще запазим всяка дума с _id
на документа и полето на събитието в междинна таблица.
Анотираният код:
db.textsearch.mapReduce(
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"];
// This denotes the fields which should be processed
var fields = ["name","address","note"];
// For each field...
fields.forEach(
function(field){
// ... we split the field into single words...
var words = (document[field]).split(" ");
words.forEach(
function(word){
// ...and remove unwanted characters.
// Please note that this regex may well need to be enhanced
var cleaned = word.replace(/[;,.]/g,"")
// Next we check...
if(
// ...wether the current word is in the stopwords list,...
(stopwords.indexOf(word)>-1) ||
// ...is either a float or an integer...
!(isNaN(parseInt(cleaned))) ||
!(isNaN(parseFloat(cleaned))) ||
// or is only one character.
cleaned.length < 2
)
{
// In any of those cases, we do not want to have the current word in our list.
return
}
// Otherwise, we want to have the current word processed.
// Note that we have to use a multikey id and a static field in order
// to overcome one of MongoDB's mapReduce limitations:
// it can not have multiple values assigned to a key.
emit({'word':cleaned,'doc':document._id,'field':field},1)
}
)
}
)
},
function(key,values) {
// We sum up each occurence of each word
// in each field in every document...
return Array.sum(values);
},
// ..and write the result to a collection
{out: "searchtst" }
)
Изпълнението на това ще доведе до създаването на колекция searchtst
. Ако вече съществува, цялото му съдържание ще бъде заменено.
Ще изглежда по следния начин:
{ "_id" : { "word" : "Bartown", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Bartown", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Ethel", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "note" }, "value" : 1 }
{ "_id" : { "word" : "FL", "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "FL", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }, "value" : 1 }
{ "_id" : { "word" : "Footown", "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "address" }, "value" : 1 }
[...]
{ "_id" : { "word" : "Sushi", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "name" }, "value" : 1 }
{ "_id" : { "word" : "Sushi", "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "note" }, "value" : 2 }
[...]
Тук трябва да отбележим няколко неща. Първо, една дума може да се среща многократно, например с „FL“. Възможно е обаче да е в различни документи, както е в случая. От друга страна, една дума може да се среща многократно в едно поле на един документ. Ще използваме това в наша полза по-късно.
Второ, имаме всички полета, най-вече word
поле в съставен индекс за _id
, което би трябвало да направи следващите заявки доста бързи. Това обаче означава също, че индексът ще бъде доста голям и – както при всички индекси – има тенденция да изяжда RAM.
Етапът на агрегиране
Така че намалихме списъка с думи. Сега правим заявка за (под)низ. Това, което трябва да направим, е да намерим всички думи, започващи с низа, въведен от потребителя досега, връщайки списък с думи, съответстващи на този низ. За да можем да направим това и да получим резултатите в подходящ за нас вид, ние използваме агрегация.
Това агрегиране трябва да е доста бързо, тъй като всички необходими полета за заявка са част от съставен индекс.
Ето анотираната агрегация за случая, когато потребителят въведе буквата S
:
db.searchtst.aggregate(
// We match case insensitive ("i") as we want to prevent
// typos to reduce our search results
{ $match:{"_id.word":/^S/i} },
{ $group:{
// Here is where the magic happens:
// we create a list of distinct words...
_id:"$_id.word",
occurrences:{
// ...add each occurrence to an array...
$push:{
doc:"$_id.doc",
field:"$_id.field"
}
},
// ...and add up all occurrences to a score
// Note that this is optional and might be skipped
// to speed up things, as we should have a covered query
// when not accessing $value, though I am not too sure about that
score:{$sum:"$value"}
}
},
{
// Optional. See above
$sort:{_id:-1,score:1}
}
)
Резултатът от тази заявка изглежда по следния начин и трябва да е доста ясен:
{
"_id" : "Sushi",
"occurences" : [
{ "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "note" },
{ "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" },
{ "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "name" },
{ "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "note" }
],
"score" : 5
}
{
"_id" : "Street",
"occurences" : [
{ "doc" : ObjectId("544b7e44fd9270c1492f5834"), "field" : "address" },
{ "doc" : ObjectId("544b9811fd9270c1492f5835"), "field" : "address" },
{ "doc" : ObjectId("544bb320fd9270c1492f583c"), "field" : "address" }
],
"score" : 3
}
Оценката 5 за суши идва от факта, че думата суши се среща два пъти в полето за бележки на един от документите. Това е планирано поведение.
Въпреки че това може да е решение за бедняк, трябва да бъде оптимизирано за безброй възможни случаи на употреба и ще се нуждае от внедряване на инкрементален mapReduce, за да бъде наполовина полезно в производствени среди, то работи според очакванията. hth.
Редактиране
Разбира се, може да се изпусне $match
етап и добавете $out
етап във фазата на агрегиране, за да бъдат предварително обработени резултатите:
db.searchtst.aggregate(
{
$group:{
_id:"$_id.word",
occurences:{ $push:{doc:"$_id.doc",field:"$_id.field"}},
score:{$sum:"$value"}
}
},{
$out:"search"
})
Сега можем да направим заявка за полученото search
събиране, за да се ускорят нещата. По принцип търгувате резултати в реално време за скорост.
Редактиране 2 :В случай че се използва подходът за предварителна обработка, searchtst
колекцията от примера трябва да бъде изтрита, след като агрегацията приключи, за да се спести както дисково пространство, така и – което е по-важно – ценна RAM.