按不同权重的相关性查询

Query by relevance with different weight

我提供了产品搜索功能,

用户可以通过多个标签进行搜索,

例如用户可以搜索 "iphone,128G, usa"

如果搜索字词与标题匹配,它将得分 3 分,

如果搜索词在标签中匹配,它将得分 1 分

如何重写当前查询以执行结果。

示例文档 1

"title": "iphone 6 128G",
"tag": [
  "usa",
  "golden",
]

示例文档 2

"title": "iphone 4 64G",
"tag": [
  "usa",
  "golden",
]

当前查询

  collection.aggregate(
     {
      "$match" => {
          "tag":{ "$in"=> q_params },
      }
     },
     { "$unwind" => "$tag" },
     { "$match" => { "tag"=> { "$in"=> q_params } } },
     { "$group" => { "_id"=> {"title":"$title"},
        "points"=> { "$sum"=>1 } } },
     { "$sort" => { "points"=> -1 } }
  )

我认为您处理这个问题的方式有点不对,并且从数据库中询问了太多 "fuzzy matching"。相反,考虑这个修改后的数据样本:

db.items.insert([
    {
        "title": "iphone 6 128G",
        "tags": [
            "iphone",
            "iphone6",
            "128G",
            "usa",
            "golden",
        ]
    },
    {
        "title": "iphone 4 64G",
        "tags": [
            "iphone",
            "iphone4",
            "64G",
            "usa",
            "golden",
        ]
    }
])

那么你考虑这样的"search phrase":

"iphone4 128G usa"

然后你需要实现你自己的应用程序逻辑(不是很难,只是引用主标签)扩展成这样:

var searchedTags = ["iphone", "iphone4", "128G", "usa"]

您可以像这样构建管道查询:

db.items.aggregate([
    { "$match": { "tags": { "$in": searchedTags } } },
    { "$project": {
        "title": 1,
        "tags": 1,
        "score": {
            "$let": {
                "vars": {
                    "matchSize":{ 
                       "$size": {
                           "$setIntersection": [
                               "$tags",
                               searchedTags
                           ]
                       }
                   }
                },
                "in": {
                    "$add": [
                       "$$matchSize",
                       { "$cond": [
                           { "$eq": [
                               "$$matchSize", 
                               { "$size": "$tags" }
                           ]},
                           "$$matchSize",
                           0
                       ]}
                    ]
                }
            }
        }
    }},
    { "$sort": { "score": -1 } }
])

其中 returns 这些结果:

{
    "_id" : ObjectId("55b3551164518e494632fa19"),
    "title" : "iphone 6 128G",
    "tags" : [
            "iphone",
            "iphone6",
            "128G",
            "usa",
            "golden"
    ],
    "score" : 3
}
{
    "_id" : ObjectId("55b3551164518e494632fa1a"),
    "title" : "iphone 4 64G",
    "tags" : [
            "iphone",
            "iphone4",
            "64G",
            "usa",
            "golden"
    ],
    "score" : 2
}

所以 "tags" 场比赛越多,获胜的次数越多。

但是如果把短语改成这样:

"iphone4 64G usa golden"

这导致解析标签如下:

var searchedTags = ["iphone", "iphone4", "64G", "usa", "golden"]

然后相同的查询管道产生这个:

{
    "_id" : ObjectId("55b3551164518e494632fa1a"),
    "title" : "iphone 4 64G",
    "tags" : [
            "iphone",
            "iphone4",
            "64G",
            "usa",
            "golden"
    ],
    "score" : 10
}
{
    "_id" : ObjectId("55b3551164518e494632fa19"),
    "title" : "iphone 6 128G",
    "tags" : [
            "iphone",
            "iphone6",
            "128G",
            "usa",
            "golden"
    ],
    "score" : 3
}

您不仅从一个文档中提供的标签上获得了比另一个文档更多匹配的好处,而且因为其中一个文档与 "all" 提供的标签匹配,所以得分得到了额外的提升,将它推向更高的排名,而不是仅匹配相同数量标签的东西。

为了分解它,首先考虑那里的 $let 表达式为管道中的元素声明了一个 "variable",因此我们不会 "repeat ourselves" 通过键入相同的表达式多个地方的结果 $$matchSize 值。

该变量本身是通过从该数组的 $setIntersection of the searchedTags array and the $tags array itself. The result of the "intersection" are just those items that match, which gives room to test the $size 计算出结果数组来确定的。

所以稍后在将那个匹配的 $size 归因于 "score" 时,另一个考虑是通过三元 $cond 来查看 $$matchSize 是否相等到 $tags 的原始长度。如果为真,则将 $$matchSize 添加到自身(得分是 "tags" 长度的两倍)作为所提供标签的 "exact match",否则该条件的返回结果是 0.

使用 $add 处理这两个数字结果会生成每个文档的最终总计 "score" 值。


这里的要点是聚合框架缺少运算符来对标题等字符串进行任何类型的 "Fuzzy match"。您可以 $regex match within a $match 阶段,因为这基本上是一个查询运算符,它只会 "filter" 结果。

您可以使用它 "mess around",但您真正想要的正则表达式是为匹配的术语获取数字 "score"。这种拆分(尽管在其他语言的正则表达式运算符中可能)实际上并不可用,因此简单地 "tokenize" 您的 "tags" 进行输入并将它们与文档 "tags".[= 匹配更有意义42=]

对于 "database"(MongoDB 主要是)这是一个更好的解决方案。或者您甚至可以将其与 $text 搜索运算符结合使用,以结合 "parsed tags" 逻辑在标题上投射它自己的 "score" 值,如此处所示。这使 "exact matches".

更加有效

它可以与聚合管道结合使用,但即使它本身也不会产生不好的结果:

db.items.createIndex({ "title": "text" })

db.items.find({ 
    "$text": { "$search": "iphone 4 64G" } },
    { "score": { "$meta": "textScore" }}
).sort({ "score": { "$meta": "textScore" } })

会产生:

{
    "_id" : ObjectId("55b3551164518e494632fa1a"),
    "title" : "iphone 4 64G",
    "tags" : [
            "iphone",
            "iphone4",
            "64G",
            "usa",
            "golden"
    ],
    "score" : 2
}
{
    "_id" : ObjectId("55b3551164518e494632fa19"),
    "title" : "iphone 6 128G",
    "tags" : [
            "iphone",
            "iphone6",
            "128G",
            "usa",
            "golden"
    ],
    "score" : 0.6666666666666666
}

但如果您只想发送字符串,不想被 "tokenizing" 逻辑打扰,并希望其他逻辑归因于您的 "score",那么请查看专用的文本搜索引擎这样做比 "text search" 甚至 MongoDB.

等主要功能数据库的基本搜索功能要好得多