仅显示 MongoDB 文本搜索的匹配字段
Show only matching fields for MongoDB text search
我是 Mongo 的新手,想为 Web 前端实现文本搜索功能。我已将所有文本字段添加到 "text" 索引中的集合中,因此搜索会在所有字段中找到匹配项。文档可能很重。
问题是当我收到整个匹配文档而不仅仅是匹配字段时。我只想获得与文档 _id
一起的匹配字段,因此我可以在 Web 预输入中显示一个提示,当用户选择一个匹配项时,我可以通过 [=] 加载整个文档11=].
有一个 $project
运算符,但问题是我不知道匹配项将出现在哪个文本字段中。
这个想了半天,觉得可以实现你想要的。但是,对于非常大的数据库,它不是 suitable 并且我还没有制定出增量方法。它缺少词干提取,必须手动定义停用词。
想法是使用 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
的集合中。
map/reduce阶段
我们基本上做的是,我们将处理三个字段之一中的每个单词,删除停用词和数字,并将每个单词与文档的 _id
和出现的字段一起保存在中间 table.
注释代码:
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"。但是,它可能在不同的文档中,就像这里的情况一样。另一方面,一个词也可以在单个文档的单个字段中多次出现。我们稍后会利用它。
其次,我们拥有所有字段,最值得注意的是 _id
的复合索引中的 word
字段,这将使接下来的查询非常快。然而,这也意味着索引将非常大,并且 - 对于所有索引 - 往往会耗尽 RAM。
聚合阶段
所以我们减少了单词列表。现在我们查询一个(子)字符串。
我们需要做的是找到所有以用户输入的字符串开头的单词,返回匹配该字符串的单词列表。为了能够做到这一点并为我们获得 suitable 形式的结果,我们使用聚合。
这种聚合应该非常快,因为所有需要查询的字段都是复合索引的一部分。
这是用户输入字母 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
}
Sushi 的 5 分是因为单词 Sushi 在其中一份文档的注释字段中出现了两次。这是预期的行为。
虽然这可能是一个穷人的解决方案,需要针对无数可想象的用例进行优化,并且需要实施增量 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
集合,以节省磁盘空间space 和 – 更重要的 – 宝贵的 RAM。
我是 Mongo 的新手,想为 Web 前端实现文本搜索功能。我已将所有文本字段添加到 "text" 索引中的集合中,因此搜索会在所有字段中找到匹配项。文档可能很重。
问题是当我收到整个匹配文档而不仅仅是匹配字段时。我只想获得与文档 _id
一起的匹配字段,因此我可以在 Web 预输入中显示一个提示,当用户选择一个匹配项时,我可以通过 [=] 加载整个文档11=].
有一个 $project
运算符,但问题是我不知道匹配项将出现在哪个文本字段中。
这个想了半天,觉得可以实现你想要的。但是,对于非常大的数据库,它不是 suitable 并且我还没有制定出增量方法。它缺少词干提取,必须手动定义停用词。
想法是使用 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
的集合中。
map/reduce阶段
我们基本上做的是,我们将处理三个字段之一中的每个单词,删除停用词和数字,并将每个单词与文档的 _id
和出现的字段一起保存在中间 table.
注释代码:
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"。但是,它可能在不同的文档中,就像这里的情况一样。另一方面,一个词也可以在单个文档的单个字段中多次出现。我们稍后会利用它。
其次,我们拥有所有字段,最值得注意的是 _id
的复合索引中的 word
字段,这将使接下来的查询非常快。然而,这也意味着索引将非常大,并且 - 对于所有索引 - 往往会耗尽 RAM。
聚合阶段
所以我们减少了单词列表。现在我们查询一个(子)字符串。 我们需要做的是找到所有以用户输入的字符串开头的单词,返回匹配该字符串的单词列表。为了能够做到这一点并为我们获得 suitable 形式的结果,我们使用聚合。
这种聚合应该非常快,因为所有需要查询的字段都是复合索引的一部分。
这是用户输入字母 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
}
Sushi 的 5 分是因为单词 Sushi 在其中一份文档的注释字段中出现了两次。这是预期的行为。
虽然这可能是一个穷人的解决方案,需要针对无数可想象的用例进行优化,并且需要实施增量 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
集合,以节省磁盘空间space 和 – 更重要的 – 宝贵的 RAM。