Na hier lang over nagedacht te hebben, denk ik dat het mogelijk is om te implementeren wat je wilt. Het is echter niet geschikt voor zeer grote databases en ik heb nog geen incrementele aanpak uitgewerkt. Het mist stammen en stopwoorden moeten handmatig worden gedefinieerd.
Het idee is om met mapReduce een verzameling zoekwoorden te maken met verwijzingen naar het herkomstdocument en het veld waar het zoekwoord vandaan komt. Vervolgens wordt de eigenlijke zoekopdracht voor de automatische aanvulling gedaan met behulp van een eenvoudige aggregatie die een index gebruikt en daarom vrij snel zou moeten zijn.
We werken dus met de volgende drie documenten
{
"name" : "John F. Kennedy",
"address" : "Kenson Street 1, 12345 Footown, TX, USA",
"note" : "loves Kendo and Sushi"
}
en
{
"name" : "Robert F. Kennedy",
"address" : "High Street 1, 54321 Bartown, FL, USA",
"note" : "loves Ethel and cigars"
}
en
{
"name" : "Robert F. Sushi",
"address" : "Sushi Street 1, 54321 Bartown, FL, USA",
"note" : "loves Sushi and more Sushi"
}
in een verzameling genaamd textsearch
.
De kaart/verkleiningsfase
Wat we eigenlijk doen, is dat we elk woord in een van de drie velden verwerken, stopwoorden en cijfers verwijderen en elk woord opslaan met de _id
van het document. en het veld van het voorkomen in een tussentabel.
De geannoteerde code:
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" }
)
Als u dit uitvoert, wordt de verzameling searchtst
gemaakt . Als het al bestond, wordt alle inhoud vervangen.
Het ziet er ongeveer zo uit:
{ "_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 }
[...]
Er zijn een paar dingen om hier op te merken. Allereerst kan een woord meerdere keren voorkomen, bijvoorbeeld met "FL". Het kan echter in verschillende documenten staan, zoals hier het geval is. Een woord kan daarentegen ook meerdere keren voorkomen in een enkel veld van een enkel document. We zullen dit later in ons voordeel gebruiken.
Ten tweede hebben we alle velden, met name het word
veld in een samengestelde index voor _id
, wat de komende vragen behoorlijk snel zou moeten maken. Dit betekent echter ook dat de index behoorlijk groot zal zijn en - zoals voor alle indices - de neiging heeft om RAM op te eten.
De aggregatiefase
Daarom hebben we de lijst met woorden ingekort. Nu zoeken we naar een (sub)tekenreeks. Wat we moeten doen, is alle woorden vinden die beginnen met de tekenreeks die de gebruiker tot nu toe heeft ingetypt, en een lijst met woorden teruggeven die met die tekenreeks overeenkomen. Om dit te kunnen doen en de resultaten in een voor ons passende vorm te krijgen, gebruiken we een aggregatie.
Deze aggregatie zou vrij snel moeten zijn, aangezien alle benodigde velden om te zoeken deel uitmaken van een samengestelde index.
Hier is de geannoteerde aggregatie voor het geval dat de gebruiker de letter S
heeft getypt :
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}
}
)
Het resultaat van deze zoekopdracht ziet er ongeveer zo uit en zou vrij duidelijk moeten zijn:
{
"_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
}
De score van 5 voor Sushi komt voort uit het feit dat het woord Sushi twee keer voorkomt in het notitieveld van een van de documenten. Dit is bedoeld gedrag.
Hoewel dit misschien een armzalige oplossing is, moet het worden geoptimaliseerd voor de talloze denkbare gebruiksscenario's en moet een incrementele mapReduce worden geïmplementeerd om halverwege nuttig te zijn in productieomgevingen, maar het werkt zoals verwacht. h.
Bewerken
Je kunt natuurlijk de $match
. laten vallen en voeg een $out
. toe stap in de aggregatiefase om de resultaten voorbewerkt te hebben:
db.searchtst.aggregate(
{
$group:{
_id:"$_id.word",
occurences:{ $push:{doc:"$_id.doc",field:"$_id.field"}},
score:{$sum:"$value"}
}
},{
$out:"search"
})
Nu kunnen we de resulterende search
. opvragen verzamelen om de zaken te bespoedigen. In principe ruil je realtime resultaten in voor snelheid.
Bewerk 2 :In het geval dat de voorbewerkingsbenadering wordt gevolgd, wordt de searchtst
verzameling van het voorbeeld moet worden verwijderd nadat de aggregatie is voltooid om zowel schijfruimte als - belangrijker - kostbaar RAM-geheugen te besparen.