简述
想要实现的功能并不容易,因为普通查询无法修改它们返回的字段。虽然有一种解决方案(使用下面的MapReduce内联而不是输出到集合),但除了非常小的数据库外,在实时情况下不可能这样做。
问题
按照原来的写法,普通查询无法真正修改它们返回的字段。但还有其他问题。如果您想在相当不错的时间内进行正则表达式搜索,则需要对所有字段进行索引,这将需要 disproportional 的 RAM 来进行该功能。如果您不对所有字段进行索引,则正则表达式搜索会导致集合扫描,这意味着必须从磁盘加载每个文档,这将花费太多时间,使自动完成变得不方便。此外,多个同时请求自动完成的用户会对后端产生相当大的负载。
解决方案
问题与我已经回答过的一个问题非常相似:我们需要从多个字段中提取出每个单词,删除停用词,并将剩余的单词与找到该单词的相应文档的链接一起保存在集合中。现在,为了获取自动完成列表,我们只需查询索引的单词列表。
步骤1:使用MapReduce作业提取单词
db.yourCollection.mapReduce(
function() {
var document = this;
var stopwords = ["the","this","and","or"];
for(var prop in document) {
if(prop === "_id" || typeof document[prop] !== 'string') {
continue
}
(document[prop]).split(" ").forEach(
function(word){
var cleaned = word.replace(/[;,.]/g,"")
if(
stopwords.indexOf(cleaned) > -1 ||
!(isNaN(parseInt(cleaned))) ||
!(isNaN(parseFloat(cleaned)))
) {
return
}
emit(cleaned,document._id)
}
)
}
},
function(k,v){
var values = { 'documents': []};
v.forEach(
function(vs){
if(values.documents.indexOf(vs)>-1){
return
}
values.documents.push(vs)
}
)
return values
},
{
finalize:
function(key,reducedValue){
var finalValue = {documents:[]}
if(reducedValue.documents) {
finalValue.documents = reducedValue.documents.filter(
function(item,pos,self){
var loc = -1;
for(var i=0;i<self.length;i++){
if(self[i].valueOf() === item.valueOf()){
loc = i;
break
}
}
return loc === pos;
}
);
} else {
finalValue.documents.push(reducedValue)
}
return finalValue
},
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阶段更加高效,实施增量式mapReduce可能是一种方法——改进我所谓的“黑客”mapReduce也可能是另一种方法。
最后,这种方式总体上是一种相当丑陋的hack。你可能想深入研究elasticsearch或lucene。在我看来,这些产品更适合你想要的。