tl;dr
对于您想要的内容没有简单的解决方案,因为普通查询无法修改它们返回的字段。有一个解决方案(使用下面的 mapReduce 内联而不是输出到集合),但除了非常小的数据库之外,不可能实时执行此操作。
问题
正如所写,普通查询无法真正修改它返回的字段。但还有其他问题。如果您想在一半的时间内进行正则表达式搜索,则必须建立索引all字段,这需要不成比例的 RAM 来实现该功能。如果你不索引all字段,正则表达式搜索会导致采集扫描 http://docs.mongodb.org/manual/core/indexes-introduction/#index-introduction,这意味着每个文档都必须从磁盘加载,这将花费太多时间来方便自动完成。此外,多个同时请求自动完成的用户会给后端带来相当大的负载。
解决方案
问题非常类似于我已经回答过的一个 https://stackoverflow.com/a/29525482/1296707:我们需要从多个字段中提取每个单词,删除停用词 https://en.wikipedia.org/wiki/Stop_words并将剩余的单词与在集合中找到的单词的相应文档的链接一起保存。现在,为了获取自动完成列表,我们只需查询索引单词列表即可。
第 1 步:使用 Map/Reduce 作业提取单词
db.yourCollection.mapReduce(
// Map function
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"];
for(var prop in document) {
// We are only interested in strings and explicitly not in _id
if(prop === "_id" || typeof document[prop] !== 'string') {
continue
}
(document[prop]).split(" ").forEach(
function(word){
// You might want to adjust this to your needs
var cleaned = word.replace(/[;,.]/g,"")
if(
// We neither want stopwords...
stopwords.indexOf(cleaned) > -1 ||
// ...nor string which would evaluate to numbers
!(isNaN(parseInt(cleaned))) ||
!(isNaN(parseFloat(cleaned)))
) {
return
}
emit(cleaned,document._id)
}
)
}
},
// Reduce function
function(k,v){
// Kind of ugly, but works.
// Improvements more than welcome!
var values = { 'documents': []};
v.forEach(
function(vs){
if(values.documents.indexOf(vs)>-1){
return
}
values.documents.push(vs)
}
)
return values
},
{
// We need this for two reasons...
finalize:
function(key,reducedValue){
// First, we ensure that each resulting document
// has the documents field in order to unify access
var finalValue = {documents:[]}
// Second, we ensure that each document is unique in said field
if(reducedValue.documents) {
// We filter the existing documents array
finalValue.documents = reducedValue.documents.filter(
function(item,pos,self){
// The default return value
var loc = -1;
for(var i=0;i<self.length;i++){
// We have to do it this way since indexOf only works with primitives
if(self[i].valueOf() === item.valueOf()){
// We have found the value of the current item...
loc = i;
//... so we are done for now
break
}
}
// If the location we found equals the position of item, they are equal
// If it isn't equal, we have a duplicate
return loc === pos;
}
);
} else {
finalValue.documents.push(reducedValue)
}
// We have sanitized our data, now we can return it
return finalValue
},
// Our result are written to a collection called "words"
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 步:查询自动完成
对于自动补全,我们只需要单词,不需要文档的链接。
由于单词已被索引,我们使用覆盖查询 http://docs.mongodb.org/manual/core/query-optimization/#covered-query– 仅从索引回答的查询,索引通常驻留在 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
field.
第三步:查询实际文档
尽管我们必须执行两次查询才能获取实际文档,但由于我们加快了整个过程,因此用户体验应该足够好。
步骤3.1:获取文件words
收藏
当用户选择自动补全选项时,我们必须查询完整的单词文档,以便找到为自动补全选择的单词的来源文档。
db.words.find({_id:"canteen"})
这会产生这样的文档:
{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }
步骤3.2:获取实际文档
有了该文档,我们现在可以显示包含搜索结果的页面,或者像在本例中一样,重定向到您可以通过以下方式获取的实际文档:
db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})
Notes
虽然这种方法一开始可能看起来很复杂(嗯,mapReduceis有点),实际上从概念上来说非常简单。基本上,您正在交易实时结果(除非您花费lotRAM)以提高速度。恕我直言,这是一笔好交易。为了使成本相当高的mapReduce阶段更加高效,实现增量mapReduce http://docs.mongodb.org/manual/tutorial/perform-incremental-map-reduce/可能是一种方法——改进我公认的被黑的mapReduce很可能是另一种方法。
最后但并非最不重要的一点是,这种方式完全是一种相当丑陋的黑客行为。您可能想深入了解 elasticsearch 或 lucene。恕我直言,这些产品非常非常适合您想要的东西。