【问题标题】:Show only matching fields for MongoDB text search仅显示 MongoDB 文本搜索的匹配字段
【发布时间】:2015-06-13 10:54:47
【问题描述】:

我是 Mongo 的新手,想为 Web 前端实现文本搜索功能。我已在“文本”索引的集合中添加了所有文本字段,因此搜索会在所有字段中找到匹配项。文件可能很重。

问题是当我收到整个匹配文档而不仅仅是匹配字段时。我只想获取匹配的字段以及文档_id,因此我可以在Web 预先输入中显示一个提示,当用户选择匹配时,我可以通过_id 加载整个文档。

有一个$project 运算符,但问题是我不知道匹配会出现在哪个文本字段中。

【问题讨论】:

  • 据我了解,您有多个字段的文本索引。长话短说:你想要的都是不可能的。
  • @MarkusWMahlberg,感谢您的快速回复。索引实际上是“$**”。你知道其他可以实现我目标的技巧吗?让我解释一下我想要的:有一个包含 3 个文本字段的文档:名称、地址、注释。所有三个字段都在文本索引中。然后发出查询以搜索例如“Ken”。在 Web Typeahead 中,我需要展示:Kennedy (Name) Kendo (Note) Kenson (Address) 所有这些提示可能来自不同或相同的文档。
  • 也许 elasticsearch 或 solr 可以帮助你。仅使用 MongoDB,这将变得相当复杂,并且至少需要 huge 索引。您可以运行聚合作业,使用提到的字段(减去停用词)的每个单词作为_id 创建新文档,并将每个原始文档的“_id”保存在数组中作为(弱)引用。由于单词将在索引中,因此自动完成应该相当快,但您的索引可能会很大。如果明确禁止使用 elasticsearch 或 solr,我只会使用这种方法。
  • @MarkusWMahlberg,再次感谢。问题是我使用后端即服务,它使用 mongoDB。如果我要使用 elasticsearch,我需要从头开始编写整个后端。无论如何,我会调查 solr 并看看它是如何进行的。您能否在问题中添加“不可能”的答案,以便我关闭它。非常感谢。

标签: mongodb full-text-search


【解决方案1】:

在考虑了很久之后,我认为可以实现您想要的。但是,它不适合非常大的数据库,而且我还没有制定出增量方法。它没有词干,必须手动定义停用词。

这个想法是使用 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 和出现的字段一起保存在中间表中.

注释代码:

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 的复合索引中的 wordfield,这应该会使即将到来的查询变得非常快。但是,这也意味着索引将非常大,并且 - 与所有索引一样 - 往往会占用 RAM。

聚合阶段

所以我们减少了单词列表。现在我们查询一个(子)字符串。 我们需要做的是找到所有以用户输入的字符串开头的单词,返回与该字符串匹配的单词列表。为了能够做到这一点并以适合我们的形式获得结果,我们使用聚合。

这种聚合应该很快,因为所有需要查询的字段都是复合索引的一部分。

这是用户输入字母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 集合,以节省磁盘空间和更重要的宝贵内存。

【讨论】:

  • 感谢您的宝贵回答。我的数据库不大,也不重载,所以我肯定会使用这个解决方案。在搜索全文搜索引擎时,我遇到了 Bleve (blevesearch.com)。还没写完,但不需要带JVM。你可能会觉得很有趣。
  • 让我知道这是怎么回事。还没有时间为此做负载测试。
猜你喜欢
  • 2021-05-19
  • 1970-01-01
  • 2015-10-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多