使用mongodb搜索实现自动完成功能

balp4ylt  于 2021-06-15  发布在  ElasticSearch
关注(0)|答案(1)|浏览(354)

我有一个 MongoDB 表格文件的收集

{
    "id": 42,
    "title": "candy can",
    "description": "canada candy canteen",
    "brand": "cannister candid",
    "manufacturer": "candle canvas"
}

我需要实现自动完成功能的基础上输入的搜索词匹配的领域除了 id . 例如,如果输入项是 can ,那么我应该返回文档中所有匹配的单词

{ hints: ["candy", "can", "canada", "canteen", ...]

我看了这个问题,但没用。我也试着搜索怎么做 regex 在多个字段中搜索并提取匹配的令牌,或者在mongodb中提取匹配的令牌 text search 但找不到任何帮助。

biswetbf

biswetbf1#

热释光;博士

对于您想要的内容没有简单的解决方案,因为普通查询无法修改它们返回的字段。有一个解决方案(使用下面的mapreduce内联,而不是对集合进行输出),但是除了非常小的数据库之外,不可能实时地这样做。

问题

如前所述,普通查询不能真正修改它返回的字段。但还有其他问题。如果您想在适当的时间内完成regex搜索,您必须索引所有字段,这将需要不成比例的ram来实现该功能。如果不为所有字段编制索引,regex搜索将导致集合扫描,这意味着每个文档都必须从磁盘加载,这将花费太多时间自动完成,不方便。此外,多个同时请求自动完成的用户将在后端造成相当大的负载。

解决方案

这个问题与我已经回答过的问题非常相似:我们需要从多个字段中提取每个单词,删除停止词,并将剩余的单词连同指向在集合中找到的相应文档的链接一起保存。现在,为了获得自动完成列表,我们只需查询索引单词列表。

步骤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步:查询自动完成

对于自动完成,我们只需要单词,而不需要指向文档的链接。因为单词被编入索引,所以我们使用一个覆盖查询——一个只从索引中回答的查询,它通常驻留在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.1:获取单词集合的文档

当用户选择自动补全时,我们必须查询单词的完整文档,以便找到选择自动补全的单词来源的文档。

db.words.find({_id:"canteen"})

会产生这样的文件:

{ "_id" : "canteen", "value" : { "documents" : [ ObjectId("553e435f20e6afc4b8aa0efb") ] } }

步骤3.2:获取实际文档

使用该文档,我们现在可以显示包含搜索结果的页面,也可以像本例一样重定向到您可以通过以下方式获得的实际文档:

db.yourCollection.find({_id:ObjectId("553e435f20e6afc4b8aa0efb")})

注意事项

虽然这种方法一开始看起来可能很复杂(好吧,mapreduce有点复杂),但实际上从概念上讲是相当简单的。基本上,你是在用实时结果来换取速度(除非你花费大量内存,否则你无论如何都不会得到实时结果)。伊莫,那是笔好买卖。为了使代价高昂的mapreduce阶段更加高效,实现增量mapreduce可能是一种方法——改进我公认的被黑客攻击的mapreduce可能是另一种方法。
最后但并非最不重要的是,这种方式是一个相当丑陋的黑客。你可能想深入研究elasticsearch或lucene。我的那些产品非常非常适合你的需要。

相关问题