Setelah memikirkan ini lama, saya pikir mungkin untuk mengimplementasikan apa yang Anda inginkan. Namun, ini tidak cocok untuk database yang sangat besar dan saya belum menemukan pendekatan tambahan. Tidak memiliki stemming dan stop word harus didefinisikan secara manual.
Idenya adalah menggunakan mapReduce untuk membuat kumpulan kata pencarian dengan referensi ke dokumen asal dan bidang tempat kata pencarian berasal. Kemudian, untuk kueri aktual untuk pelengkapan otomatis dilakukan menggunakan agregasi sederhana yang menggunakan indeks dan karenanya harus agak cepat.
Jadi kami akan bekerja dengan tiga dokumen berikut
{
"name" : "John F. Kennedy",
"address" : "Kenson Street 1, 12345 Footown, TX, USA",
"note" : "loves Kendo and Sushi"
}
dan
{
"name" : "Robert F. Kennedy",
"address" : "High Street 1, 54321 Bartown, FL, USA",
"note" : "loves Ethel and cigars"
}
dan
{
"name" : "Robert F. Sushi",
"address" : "Sushi Street 1, 54321 Bartown, FL, USA",
"note" : "loves Sushi and more Sushi"
}
dalam koleksi yang disebut textsearch
.
Tahap peta/perkecil
Apa yang pada dasarnya kami lakukan adalah bahwa kami akan memproses setiap kata di salah satu dari tiga bidang, menghapus kata dan angka berhenti dan menyimpan setiap kata dengan _id
dokumen dan bidang kejadian di tabel perantara.
Kode beranotasi:
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" }
)
Menjalankan ini akan menghasilkan pembuatan koleksi searchtst
. Jika sudah ada, semua isinya akan diganti.
Ini akan terlihat seperti ini:
{ "_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 }
[...]
Ada beberapa hal yang perlu diperhatikan di sini. Pertama-tama, sebuah kata dapat memiliki beberapa kemunculan, misalnya dengan "FL". Namun, mungkin dalam dokumen yang berbeda, seperti yang terjadi di sini. Sebuah kata juga dapat memiliki beberapa kemunculan dalam satu bidang dari satu dokumen, di sisi lain. Kami akan menggunakan ini untuk keuntungan kami nanti.
Kedua, kami memiliki semua bidang, terutama word
bidang dalam indeks gabungan untuk _id
, yang seharusnya membuat kueri yang datang cukup cepat. Namun, ini juga berarti indeks akan cukup besar dan – seperti untuk semua indeks – cenderung memakan RAM.
Tahap agregasi
Jadi kami telah mengurangi daftar kata. Sekarang kita meminta (sub) string. Yang perlu kita lakukan adalah menemukan semua kata yang dimulai dengan string yang diketik pengguna sejauh ini, mengembalikan daftar kata yang cocok dengan string itu. Untuk dapat melakukan ini dan mendapatkan hasil dalam bentuk yang sesuai untuk kami, kami menggunakan agregasi.
Agregasi ini seharusnya cukup cepat, karena semua bidang yang diperlukan untuk kueri adalah bagian dari indeks gabungan.
Berikut adalah agregasi beranotasi untuk kasus ketika pengguna mengetikkan huruf 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}
}
)
Hasil kueri ini terlihat seperti ini dan seharusnya cukup jelas:
{
"_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
}
Skor 5 untuk Sushi berasal dari fakta bahwa kata Sushi muncul dua kali di kolom catatan salah satu dokumen. Ini adalah perilaku yang dimaksudkan.
Meskipun ini mungkin solusi orang miskin, perlu dioptimalkan untuk banyak sekali kasus penggunaan yang masuk akal dan akan membutuhkan mapReduce tambahan untuk diimplementasikan agar setengah berguna di lingkungan produksi, ini berfungsi seperti yang diharapkan. h.
Sunting
Tentu saja, seseorang dapat menghapus $match
panggung dan tambahkan $out
tahap dalam fase agregasi untuk mendapatkan hasil yang diproses sebelumnya:
db.searchtst.aggregate(
{
$group:{
_id:"$_id.word",
occurences:{ $push:{doc:"$_id.doc",field:"$_id.field"}},
score:{$sum:"$value"}
}
},{
$out:"search"
})
Sekarang, kita dapat mengkueri search
yang dihasilkan koleksi untuk mempercepat. Pada dasarnya Anda memperdagangkan hasil waktu nyata untuk kecepatan.
Edit 2 :Jika pendekatan pra-pemrosesan diambil, searchtst
kumpulan contoh harus dihapus setelah agregasi selesai untuk menghemat ruang disk dan – yang lebih penting – RAM yang berharga.