在Mongodb上使用地理空间索引的全文搜索

Sim*_*mon 9 full-text-search geospatial mongodb mongodb-query

假设我想开发一个Android应用程序,允许用户搜索距离您最近的酒店.这在当今的应用程序中非常常见,例如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
      ]
}
Run Code Online (Sandbox Code Playgroud)

现在,我想在字段上创建一个文本索引,name以便按名称搜索,然后根据坐标按地理空间索引排序.

因此,如果我搜索"The Most"这个词,它将按名称搜索"The Most"并返回最近的酒店,其中包含"The Most in them".

mongodb甚至支持这种类型的搜索吗?

我在这里阅读mongodb的指南:https://docs.mongodb.org/manual/core/index-text/

复合文本索引不能包含任何其他特殊索引类型,例如多键或地理空间索引字段.

据我了解,我不是在创建复合文本索引.这是一个简单的文本索引,这意味着我只是索引name字段的文本而不是cityAND name字段.

Bla*_*ven 28

有一个公平的案例,你真的根本不需要这个,因为很难证明这种操作的用例是合理的,我认为"搜索酒店"不是"文本搜索"的组合. "和"geoSpatial"搜索确实适用.

实际上,"大多数人"会寻找靠近某个位置的东西,或者甚至更可能接近他们想要访问的各个位置,作为其主要标准的一部分,然后其他"赢家"可能会更加权重"成本" ","评级","品牌","设施",甚至可能靠近餐馆等.

"文本搜索"添加到该列表是一个非常不同的事情,在这个特定的应用程序中可能没有太多实际用途.

尽管如此,这可能值得一些解释,并且这里有一些概念可以理解为什么这两个概念至少对这个用例没有真正"网格化".

修复架构

首先,我想建议稍微"调整"您的数据模式:

{
    "name" : "The Most Amazing Hotel",
    "city" : "India",
    "location": {
        "type": "Point",
        "coordinates": [
               72.867804,
               19.076033
        ]
    }
}
Run Code Online (Sandbox Code Playgroud)

这至少证明"location"是一个有效的GeoJSON对象用于索引,你通常需要GeoJSON而不是传统的坐标对,因为它确实为查询和存储开辟了更多的选项,加上距离被标准化为米而不是等于"弧度"在全球范围内.

为什么他们不一起工作

因此,您的阅读基本上是正确的,因为您不能同时使用多个特殊索引.首先看一下复合索引定义:

db.hotels.createIndex({ "name": "text", "location": "2dsphere" })
Run Code Online (Sandbox Code Playgroud)

{"ok":0,"errmsg":"错误的索引键模式{name:\"text \",location:\"2dsphere \"}:不能为单个索引使用多个索引插件.", "代码":67}

所以这是无法做到的.即使单独考虑:

db.hotels.createIndex({ "name": "text" })
db.hotels.createIndex({ "location": "2dsphere" })
Run Code Online (Sandbox Code Playgroud)

然后尝试进行查询:

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

错误:命令失败:{"waitedMS":NumberLong(0),"ok":0,"errmsg":"text和geoNear在同一查询中不允许","code":2}:undefined

这实际上支持了以下三种方式无法在复合索引中定义的原因:

  1. 正如初始错误所示,MongoDB中处理这些"特殊"索引的方式实际上需要"分支"到所选索引类型的"特殊"处理程序,并且两个处理程序不在同一个位置.

  2. 即使使用单独的索引,由于逻辑基本上是一个"和"条件,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/
        }
    }}
])
Run Code Online (Sandbox Code Playgroud)

甚至:

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

或者先通过文字搜索:

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

现在,您可以仔细查看每种方法中的选择选项,.explain()以查看发生的情况,但基本情况是每种方法只选择一个要分别使用的特殊索引.

在第一种情况下,它将是用于主要集合的集合上的geoSpatial索引,并将根据它们与首先给定的位置的接近度找到结果,然后通过为该name字段给出的正则表达式参数进行过滤.

在第二种情况下,它将使用"文本"索引进行主要选择(因此首先找到"惊人"的东西)并从这些结果应用geoSpatial过滤器(不使用索引)$geoWithin,在这种情况下执行的是$near通过在所提供距离内的点周围的圆内搜索来过滤结果,基本上相当于正在做的事情.

不是"所有"查询都是相等的

但要考虑的关键是每种方法都有可能返回不同的结果.通过首先缩小位置,可以检查的唯一数据是指定距离内的那些位置,因此除了距离之外的任何"惊人"都不会被附加过滤器考虑.

在第二种情况下,由于文本术语是主要搜索,因此将考虑"惊人"的所有结果,并且辅助过滤器可以返回的唯一项目是允许从初始文本返回的项目.过滤.

这在整体考虑中非常重要,因为两个查询操作("text"和"geoSpatial")都力求实现非常不同的事物.在"文本"案例中,它正在寻找给定术语的"最佳结果",并且本质上仅返回与排名顺序中的术语匹配的限制数量的结果.这意味着当应用任何其他过滤条件时,很可能满足第一条件的许多项目不符合附加标准.

简而言之,"并非所有事情"惊人"必须接近查询点",这意味着具有现实限制100 results,并且通过最佳匹配,这些100可能不包含所有"近"项目.

此外,$text操作员实际上并没有真正以任何方式对结果进行"排序".它的主要目的实际上不仅是为了"匹配"一个短语而是为了"得分"结果,以便将"最佳"匹配浮动到顶部.这通常在查询本身"之后"完成,其中投影值被"排序"并且最可能如上所述"受限制".可能在聚合管道中执行此操作然后应用第二个过滤器,但如上所述,这可能排除了在另一个目的中"接近"的事物.

相反的情况也可能是正确的('有很多"惊人的"东西远离这一点'),但是由于实际的距离限制,这种情况变得不太可能.但是给出的另一个考虑因素是这不是真正的文本搜索,而只是使用正则表达式来匹配给定的术语.

作为最后一点,我总是"Amazing"在这里使用作为示例短语而不是"Most"问题中的建议.这是因为"词干"在这里(以及在大多数专用文本搜索产品中)的文本索引中的工作方式,因为特定术语将被忽略,就像"和","或","the",甚至"in" "也是如此,因为它们并不真正被认为对短语有价值,这就是文本搜索的作用.

因此,事实上,正则表达式实际上更适合匹配这些术语,如果确实需要的话.

结论

这真的让我们回到原点,因为"文本"查询真的不属于这里.其他有用的过滤器通常与真正的"地理空间"搜索标准协同工作越好,真正的"文本搜索"在重要的列表中真的很低.

更有可能的是,人们想要一个位于距离他们希望访问的目的地的距离"设置交叉点"内的位置,或者至少接近一些或大多数距离.当然,如前所述的其他因素(*"价格","服务"等)是人们普遍需要的东西.

以这种方式寻找结果并不是一个"合适的".如果你认为你真的必须,那么应用其中一种"作弊"方法,或者实际上使用不同的查询,然后使用其他一些逻辑来合并每组结果.但是服务器单独执行此操作确实没有意义,这就是它不尝试的原因.

因此,我将专注于首先使您的geoSpatial匹配正确,然后应用对结果重要的其他批评.但我真的不相信"文本搜索"无论如何都是有效的.相反,"作弊",但只有你真的必须.

  • 这是非常聪明的文章,但最后只是缺乏功能的借口。对于某些应用程序而言,考虑到位置进行全文搜索是必要的,因此,确实需要Solr或ElasticSearch(基于Lucene的所有类型)类型的东西作为mongodb的附件,而不幸的是,这种情况。很想只使用mongodb而不是它的用例。 (3认同)
  • 同意杰森的观点,对这个答案很不满意。我们的用例是允许用户搜索特定区域中的给定事物(例如“Brixton”中的“健身房”)。我们需要按距离对结果进行排名,以便为用户提供最相关的结果(即返回实际位于布里克斯顿的健身房,然后再返回到周边地区的健身房)。但实际匹配健身房并对其与给定关键字匹配的程度进行排名(例如“武术健身房”)也很重要。所以我们确实需要 $nearSphere 和 $text 并且找不到解决方法。 (2认同)