在MongoDB上使用全文搜索与地理空间索引

15

假设我想开发一个安卓应用程序,允许用户搜索距离他们当前位置最近的酒店。这在现今的应用程序中非常常见,例如AirBnb。

这是我使用的数据集:

{
    "name" : "The Most Amazing Hotel",
    "city" : "India",
    "type": "Point"
    "coord": [
        -56.16082,
        61.15392
      ]
}

{
    "name" : "The Most Incredible Hotel",
    "city" : "India",
    "type": "Point"
    "coord": [
        -56.56285,
        61.34590
      ]
}

{
    "name" : "The Fantastic GuestHouse",
    "city" : "India",
    "type": "Point"
    "coord": [
        -56.47085,
        61.11357
      ]
}
现在,我想在“name”字段上创建一个文本索引,以便按名称搜索,然后根据基于坐标的地理空间索引进行排序。
因此,如果我搜索单词“最多”,它将通过名称搜索包含单词“最多”的酒店,并返回最接近这些单词的酒店。
MongoDB是否支持此类型的搜索?
我正在阅读这里的MongoDB指南:https://docs.mongodb.org/manual/core/index-text/ 引用如下:
复合文本索引不能包括任何其他特殊索引类型,例如多键或地理空间索引字段。
就我所知,我没有创建复合文本索引。这是一个简单的文本索引,这意味着我只对“name”字段中的文本进行索引,而不是对“city”和“name”字段进行索引。
1个回答

41
在这种情况下,存在一个非常难以证明使用场景的公正案例,因此我认为“搜索旅馆”不是真正适用于“文本”和“地理空间”搜索组合的应用。实际上,“大多数人”会寻找靠近某个位置的东西,甚至更有可能靠近他们想要访问的各个位置作为其主要标准,而其他的“获胜者”很可能更加注重“成本”、“评分”、“品牌”、“设施”,甚至可能是靠近餐馆等方面的接近程度。将“文本搜索”添加到该列表中是一件非常不同的事情,并且在这种特定应用程序中可能没有太多实际用途。尽管如此,这可能值得一些解释,对于为什么这两个概念在这个使用案例中并不真正“契合”的原因,有一些概念需要理解。

修正模式

首先,我想建议“微调”数据模式:
{
    "name" : "The Most Amazing Hotel",
    "city" : "India",
    "location": {
        "type": "Point",
        "coordinates": [
               72.867804,
               19.076033
        ]
    }
}

至少提供了“位置”作为有效的GeoJSON对象进行索引,通常您希望使用GeoJSON而不是传统的坐标对,因为它可以打开更多的查询和存储选项,并且距离以米为标准,而不是在全球范围内等效的“弧度”。

为什么它们不能共同工作

所以你的理解基本上是正确的,即您不能同时使用多个特殊索引。首先看一下复合索引的定义:
db.hotels.createIndex({ "name": "text", "location": "2dsphere" })

{ "ok" : 0, "errmsg" : "索引键模式{name:\"text\",location:\"2dsphere\"}不良:不能为单个索引使用多个索引插件。", "code" : 67 }

因此,这是无法完成的。即使分别考虑:

db.hotels.createIndex({ "name": "text" })
db.hotels.createIndex({ "location": "2dsphere" })

然后尝试进行查询:

db.hotels.find({
    "location": {
        "$nearSphere": {
            "$geometry": {
                "type": "Point",
                "coordinates": [
                   72.867804,
                   19.076033
                ]
            }
        }
    },
    "$text": { "$search": "Amazing" }
})

错误:命令失败:{ "waitedMS" : NumberLong(0), "ok" : 0, "errmsg" : "text and geoNear not allowed in same query", "code" : 2 } : undefined

这实际上支持了三种不能在复合索引中定义的原因:

  1. 正如最初的错误所示,MongoDB中处理这些“特殊”索引的方式需要基本上“分支”到所选索引类型的“特殊”处理程序,而两个处理程序不在同一个位置。

  2. 即使使用单独的索引,由于逻辑基本上是“and”条件,MongoDB也无法选择多个索引,因为两个查询子句都需要“特殊”处理,因此实际上必须这样做。而它不能。

  3. 即使这在逻辑上是一个$or条件,你基本上会回到点1,在那里即使应用“索引交集”,这些“特殊”索引的另一个属性也是它们必须在查询操作的“顶层”应用才能允许索引选择。将它们包装在$or中意味着MongoDB无法这样做,因此不允许。

但你可以“作弊”

因此,每个基本上都必须是独占的,你不能同时使用它们。但是,当然你可以“作弊”,这取决于哪种搜索顺序对你更重要。

先通过“位置”:

db.hotels.aggregate([
    { "$geoNear": {
        "near": {
            "type": "Point",
            "coordinates": [
               72.867804,
               19.076033
            ]
        },
        "spherical": true,
        "maxDistance": 5000,
        "distanceField": "distance",
        "query": {
           "name": /Amazing/
        }
    }}
])

或者甚至:

db.hotels.find({
    "location": {
        "$nearSphere": {
            "$geometry": {
                "type": "Point",
                "coordinates": [
                   72.867804,
                   19.076033
                ]
            },
            "$maxDistance": 5000
        }
    },
    "name": /Amazing/
})

或者首先通过文本搜索:

db.hotels.find({
    "$text": { "$search": "Amazing" },
    "location": {
        "$geoWithin": {
            "$centerSphere": [[
               72.867804,
               19.076033
            ], 5000 ]
        }
    }
})

现在您可以使用.explain()仔细查看每个方法中的选择选项,以了解发生了什么,但基本情况是各自只选择一个特殊索引来使用。
在第一种情况下,它将使用集合上的地理空间索引作为主要索引,并根据其与首先给定位置的接近程度找到结果,然后通过给定name字段的正则表达式参数进行过滤。
在第二种情况下,它将使用“text”索引进行主要选择(因此首先找到“Amazing”),并从这些结果应用地理空间过滤器(不使用索引)与$geoWithin,在这种情况下,它执行的基本上是等价于$near所做的事情,通过在提供的距离内搜索点周围的圆以过滤结果。
然而,关键要考虑的是,每种方法返回的结果可能不同。通过首先缩小位置范围,只能检查那些在指定距离内的位置数据,因此距离之外的任何“Amazing”都不会被附加过滤器考虑。
在第二种情况下,由于文本术语是主要搜索,因此将考虑“所有”“Amazing”的结果,并且从最初的文本过滤器允许返回的结果中只能返回那些被允许返回的项目。
这在整体考虑中非常重要,因为两个查询操作(“text”和“geoSpatial”)努力实现非常不同的目标。在“text”案例中,它正在寻找给定术语的“顶级结果”,并且自然地仅以排名顺序返回有限数量的匹配项。这意味着,在应用任何其他过滤条件时,很可能有许多符合第一个条件的项目不符合其他条件。
简而言之,'并非所有“Amazing”都必须靠近查询点',这意味着在实际限制(例如100个结果)和最佳匹配的情况下,这100个结果可能不包含所有“附近”的项目。
此外,$text 运算符本身实际上并不会以任何方式 "排序" 结果。它的主要目的不仅在于匹配短语,而且还在于"评分"结果,以将 "最佳" 匹配浮动到顶部。这通常是在查询本身之后完成的,投影值被 "排序" 并可能被 "限制",如上所述。在聚合管道中可能有这样的操作,然后应用第二个过滤器,但是如上所述,这可能会排除其他目的下 "接近" 的结果。
反过来也很可能是真实的('还有许多距离该点更远的 "惊人" 事物'),但是随着现实距离限制的加入,这种情况变得不太可能。但是给出的另一个考虑因素是这不是一个 真正 的文本搜索,而只是使用正则表达式来匹配给定术语。
最后一个备注,我总是在这里使用 "Amazing" 作为示例短语,而不是问题中建议使用 "Most"。这是因为在文本索引中的词干处理方式(以及大多数专用文本搜索产品中的方式)是,特定术语会被忽略,就像"and"、"or"、"the",甚至 "in" 一样,因为它们并不真正被认为是短语的有价值的部分,而这正是文本搜索所做的。
因此,如果确实需要这样做,那么正则表达式实际上比匹配此类术语更好。
总之,这实际上将我们带回到了原始观点,即 "文本" 查询实际上在这里并不适用。其他有用的过滤器通常与真正的 "地理空间" 搜索条件一起使用,并且真正的 "文本搜索" 真的不是最重要的事情之一。
更可能的是人们想要一个位于从他们希望访问的目的地到达的距离的 *"集合交集" 中的位置,或者至少足够接近某些或大多数。然后当然还有其他因素( *"价格"、"服务" 等)作为一般考虑的东西。
以这种方式查找结果并不是一个"好的解决方案"。如果您认为您确实必须这样做,那么请使用 "欺骗性" 方法之一,或者实际上使用不同的查询,然后使用其他逻辑来合并每组结果。但是,服务器单独执行这项任务真的没有意义,这就是为什么它不会尝试的原因。
因此,我建议首先正确地获取您的地理空间匹配,然后应用其他应该重要的标准进行匹配。但是,我真的不相信 "文本搜索" 真的有效,成为其中之一。如果确实需要,请使用 "欺骗性" 方法。

写得非常好,见解深刻。真的很感谢你花时间写这个答案——它给了我新的视角。昨晚,我一整晚都在思考如何做到这一点,但现在也许我应该改变我的策略。 - Simon
只是一个快速的问题 - 在您的第二个场景中,使用文本搜索,理论上您可以将半径设置为一个极大的数字(例如Integer.MAX_VALUE),这样它会返回按接近度排序的文本搜索? - Simon
4
这篇文章非常聪明,但最终只是为缺少一个功能找借口。对于一些需要根据位置进行全文搜索的应用程序来说,这是必要的,因此Solr或ElasticSearch(均基于Lucene)类型的东西真的需要作为MongoDB的补充,并且不幸的是我目前处在这种情况下。很想只使用MongoDB,但尚未达到其使用情况。 - King Friday
非常漂亮的答案,真的帮助我理解了需要澄清的内容,谢谢! - Lelouch
2
同意Jason的看法,对这个答案感到非常不满意。我们的用例是允许用户在特定区域内搜索给定的事物(例如,在“Brixton”中搜索“健身房”)。我们需要按照它们与用户的距离来排名结果,以便为我们的用户提供最相关的结果(即返回实际位于Brixton的健身房,然后再回退到周围地区的健身房)。但实际匹配健身房并根据关键词的匹配程度进行排名也很重要(例如,“武术健身房”)。因此,我们确实需要$nearSphere和$text两者,无法绕过它们。 - dan674
这是一份非常优质的解释。感谢您花时间描述如此全面的解决方案。 - Frédéric Fara Wat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接