在 Mongodb 上使用地理空间索引进行全文搜索
Using full text search with geospatial index on Mongodb
假设我想开发一个 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
]
}
现在,我想在 name
字段上创建一个 文本索引 ,以便它按名称搜索,然后按基于坐标的地理空间索引排序。
因此,如果我搜索单词 "The Most",它将按名称搜索单词 "The Most" 和 return 最近的酒店,其中包含单词“The Most in them。
mongodb是否支持这种类型的搜索?
我正在阅读 mongodb 的指南:https://docs.mongodb.org/manual/core/index-text/
A compound text index cannot include any other special index types,
such as multi-key or geospatial index fields.
据我所知,我并不是在创建复合文本索引。这是一个简单的文本索引,这意味着我只为 name
字段而不是 city
和 name
字段索引文本。
有一个公平的例子,你真的根本不需要这个,因为很难证明这种操作的用例是合理的,我认为 "Searching for a Hotel" 不是 "text" 和 "geoSpatial" 搜索组合真正适用的东西。
实际上 "most people" 会寻找靠近某个位置的东西,或者更有可能 靠近他们想要访问的各个位置,作为他们主要标准的一部分,然后其他 "winners" 可能会比 "cost"、"rating"、"brand"、"facilities",甚至可能靠近餐馆等。
将 "Text search" 添加到该列表是一件 非常不同的事情 并且在这个特定应用程序中可能没有多大实际用处.
不过,这可能值得一些解释,这里有一些概念需要理解,以了解为什么这两个概念并不真正 "mesh" 用于此用途至少案例。
修复架构
首先,我想对"tweak"您的数据模式提出一点建议:
{
"name" : "The Most Amazing Hotel",
"city" : "India",
"location": {
"type": "Point",
"coordinates": [
72.867804,
19.076033
]
}
}
这至少证明 "location"
是一个有效的 GeoJSON 对象用于索引,并且您通常需要 GeoJSON 而不是旧的坐标对,因为它确实为查询和存储打开了更多选项,另外距离标准化为米,而不是全球范围内等同的 "radians"。
为什么他们不一起工作
所以你的阅读基本上是正确的,你不能一次使用多个特殊索引。先看复合索引定义:
db.hotels.createIndex({ "name": "text", "location": "2dsphere" })
{
"ok" : 0,
"errmsg" : "bad index key pattern { name: \"text\", location: \"2dsphere\" }: Can't use more than one index plugin for a single index.",
"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" }
})
Error: command failed: {
"waitedMS" : NumberLong(0),
"ok" : 0,
"errmsg" : "text and geoNear not allowed in same query",
"code" : 2
} : undefined
这实际上支持了无法通过三种方式在复合索引中定义的原因:
如初始错误所示,这些 "special" 索引在 MongoDB 中的处理方式基本上需要 "branching off" 到 "special" 处理程序selected索引类型,两个handler不在同一个地方
即使有单独的索引,因为逻辑基本上是一个 "and" 条件,所以 MongoDB 无论如何都不能 select 多个索引,并且因为两个查询子句要求 "special" 处理它实际上需要这样做。它不能。
即使这在逻辑上是一个 $or
条件,您基本上还是回到点 1,即使应用 "index intersection" 也有另一个 属性 这样的 "special" 索引,它们 必须 应用于查询操作的 "top level" 以允许索引 selection。将它们包装在 $or
中意味着 MongoDB 不能这样做,因此它是不允许的。
但是你可以"Cheat"
所以基本上每一个都必须是独占的,不能一起使用。但当然您可以随时 "cheat",具体取决于哪个搜索顺序对您来说更重要。
或者先 "location":
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()
仔细查看每个方法中的 selection 选项,看看发生了什么,但基本情况是每个 selects 仅分别使用的特殊索引之一。
在第一种情况下,它将是用于主要集合的 geoSpatial 索引,并将根据它们与首先给定的位置的接近程度找到结果,然后通过为 name
字段.
在第二种情况下,它将使用 "text" 索引来执行主要 selection(因此首先找到东西 "Amazing"),然后从这些结果中应用地理空间过滤器(不是使用索引 ) 和 $geoWithin
, which in this case is performing what is basically the equivalent of of what a $near
is doing, by searching within a circle around a point 在提供的距离内过滤结果。
不"all" 查询相等
虽然要考虑的关键是每种方法很可能 return 不同的结果。通过首先缩小位置范围,唯一可以检查的数据是指定距离内的那些位置,因此任何 "Amazing" 在距离之外的东西都不会被附加过滤器考虑。
在第二种情况下,由于文本词是主要搜索,因此所有个结果都被考虑"Amazing",并且只有 可以被二级过滤器 return 编辑的项目是那些允许从初始文本过滤器 return 编辑的项目。
这在整体考虑中非常重要,因为两个查询操作("text" 和 "geoSpatial")努力实现截然不同的事情。在 "text" 的情况下,它正在寻找 "top results" 给定的术语,并且本质上只会 return 有限数量的结果与排名顺序匹配的术语。这意味着当应用任何其他过滤条件时,很可能许多满足第一个条件的项目不满足其他条件。
简而言之,'Not all things "Amazing" are necessarily anywhere near the queried point',这意味着像 100 results
这样的现实限制,并且通过最佳匹配,这 100 个可能不包含所有还有 "near" 项。
此外,$text
运算符本身并没有真正 "sort" 结果。事实上,它的主要目的不仅是 "match" 一个短语,而且 "score" 结果以便将 "best" 匹配浮动到顶部。这通常是 "after" 查询本身完成的,其预测值为 "sorted",很可能是 "limited",如上所述。可能在聚合管道中执行此操作然后应用第二个过滤器,但如前所述,这可能排除了其他目的的东西 "near"。
反之亦然('There are many "Amazing" things further away from the point'),但由于现实的距离限制,这种情况不太可能发生。但给出的另一个考虑是这不是 true 文本搜索,而只是使用正则表达式来匹配给定的术语。
最后一点,我总是在这里使用 "Amazing"
作为示例短语,而不是问题中建议的 "Most"
。这是因为 "stemming" 在此处的文本索引(以及大多数专用文本搜索产品)中的工作方式,因为特定术语将被 忽略 ,很像 "and"、"or"、"the",甚至 "in" 也可以,因为它们并不真正被认为 有价值 到短语,这就是文本搜索的作用。
所以事实上,如果确实需要的话,正则表达式实际上会更好地匹配这些术语。
总结
这真的把我们带回了原点,因为 "text" 查询确实不属于这里。其他有用的过滤器通常与 true "geoSpatial" 搜索条件配合使用效果更好,而 true "text search" 在重要列表中的位置确实很低。
更有可能的是,人们想要的位置与他们希望访问的所需目的地的距离在 *"Set Intersection" 以内,或者至少离某些或大多数目的地足够近。当然前面提到的其他因素(*"price"、"service"等)也是人们普遍考虑的因素。
这么找结果还真不是"good fit"。如果您认为确实必须这样做,请应用 "cheat" 方法之一,或者实际上使用不同的查询,然后使用一些其他逻辑来合并每组结果。但是服务器单独做这件事确实没有意义,这也是它不尝试的原因。
所以我会专注于首先让您的地理空间匹配正确,然后应用其他对结果很重要的标准。但我真的不相信 "text search" 无论如何都可以成为其中之一。 "Cheat" 相反,但前提是你真的必须这样做。
假设我想开发一个 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
]
}
现在,我想在 name
字段上创建一个 文本索引 ,以便它按名称搜索,然后按基于坐标的地理空间索引排序。
因此,如果我搜索单词 "The Most",它将按名称搜索单词 "The Most" 和 return 最近的酒店,其中包含单词“The Most in them。
mongodb是否支持这种类型的搜索?
我正在阅读 mongodb 的指南:https://docs.mongodb.org/manual/core/index-text/
A compound text index cannot include any other special index types, such as multi-key or geospatial index fields.
据我所知,我并不是在创建复合文本索引。这是一个简单的文本索引,这意味着我只为 name
字段而不是 city
和 name
字段索引文本。
有一个公平的例子,你真的根本不需要这个,因为很难证明这种操作的用例是合理的,我认为 "Searching for a Hotel" 不是 "text" 和 "geoSpatial" 搜索组合真正适用的东西。
实际上 "most people" 会寻找靠近某个位置的东西,或者更有可能 靠近他们想要访问的各个位置,作为他们主要标准的一部分,然后其他 "winners" 可能会比 "cost"、"rating"、"brand"、"facilities",甚至可能靠近餐馆等。
将 "Text search" 添加到该列表是一件 非常不同的事情 并且在这个特定应用程序中可能没有多大实际用处.
不过,这可能值得一些解释,这里有一些概念需要理解,以了解为什么这两个概念并不真正 "mesh" 用于此用途至少案例。
修复架构
首先,我想对"tweak"您的数据模式提出一点建议:
{
"name" : "The Most Amazing Hotel",
"city" : "India",
"location": {
"type": "Point",
"coordinates": [
72.867804,
19.076033
]
}
}
这至少证明 "location"
是一个有效的 GeoJSON 对象用于索引,并且您通常需要 GeoJSON 而不是旧的坐标对,因为它确实为查询和存储打开了更多选项,另外距离标准化为米,而不是全球范围内等同的 "radians"。
为什么他们不一起工作
所以你的阅读基本上是正确的,你不能一次使用多个特殊索引。先看复合索引定义:
db.hotels.createIndex({ "name": "text", "location": "2dsphere" })
{ "ok" : 0, "errmsg" : "bad index key pattern { name: \"text\", location: \"2dsphere\" }: Can't use more than one index plugin for a single index.", "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" }
})
Error: command failed: { "waitedMS" : NumberLong(0), "ok" : 0, "errmsg" : "text and geoNear not allowed in same query", "code" : 2 } : undefined
这实际上支持了无法通过三种方式在复合索引中定义的原因:
如初始错误所示,这些 "special" 索引在 MongoDB 中的处理方式基本上需要 "branching off" 到 "special" 处理程序selected索引类型,两个handler不在同一个地方
即使有单独的索引,因为逻辑基本上是一个 "and" 条件,所以 MongoDB 无论如何都不能 select 多个索引,并且因为两个查询子句要求 "special" 处理它实际上需要这样做。它不能。
即使这在逻辑上是一个
$or
条件,您基本上还是回到点 1,即使应用 "index intersection" 也有另一个 属性 这样的 "special" 索引,它们 必须 应用于查询操作的 "top level" 以允许索引 selection。将它们包装在$or
中意味着 MongoDB 不能这样做,因此它是不允许的。
但是你可以"Cheat"
所以基本上每一个都必须是独占的,不能一起使用。但当然您可以随时 "cheat",具体取决于哪个搜索顺序对您来说更重要。
或者先 "location":
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()
仔细查看每个方法中的 selection 选项,看看发生了什么,但基本情况是每个 selects 仅分别使用的特殊索引之一。
在第一种情况下,它将是用于主要集合的 geoSpatial 索引,并将根据它们与首先给定的位置的接近程度找到结果,然后通过为 name
字段.
在第二种情况下,它将使用 "text" 索引来执行主要 selection(因此首先找到东西 "Amazing"),然后从这些结果中应用地理空间过滤器(不是使用索引 ) 和 $geoWithin
, which in this case is performing what is basically the equivalent of of what a $near
is doing, by searching within a circle around a point 在提供的距离内过滤结果。
不"all" 查询相等
虽然要考虑的关键是每种方法很可能 return 不同的结果。通过首先缩小位置范围,唯一可以检查的数据是指定距离内的那些位置,因此任何 "Amazing" 在距离之外的东西都不会被附加过滤器考虑。
在第二种情况下,由于文本词是主要搜索,因此所有个结果都被考虑"Amazing",并且只有 可以被二级过滤器 return 编辑的项目是那些允许从初始文本过滤器 return 编辑的项目。
这在整体考虑中非常重要,因为两个查询操作("text" 和 "geoSpatial")努力实现截然不同的事情。在 "text" 的情况下,它正在寻找 "top results" 给定的术语,并且本质上只会 return 有限数量的结果与排名顺序匹配的术语。这意味着当应用任何其他过滤条件时,很可能许多满足第一个条件的项目不满足其他条件。
简而言之,'Not all things "Amazing" are necessarily anywhere near the queried point',这意味着像 100 results
这样的现实限制,并且通过最佳匹配,这 100 个可能不包含所有还有 "near" 项。
此外,$text
运算符本身并没有真正 "sort" 结果。事实上,它的主要目的不仅是 "match" 一个短语,而且 "score" 结果以便将 "best" 匹配浮动到顶部。这通常是 "after" 查询本身完成的,其预测值为 "sorted",很可能是 "limited",如上所述。可能在聚合管道中执行此操作然后应用第二个过滤器,但如前所述,这可能排除了其他目的的东西 "near"。
反之亦然('There are many "Amazing" things further away from the point'),但由于现实的距离限制,这种情况不太可能发生。但给出的另一个考虑是这不是 true 文本搜索,而只是使用正则表达式来匹配给定的术语。
最后一点,我总是在这里使用 "Amazing"
作为示例短语,而不是问题中建议的 "Most"
。这是因为 "stemming" 在此处的文本索引(以及大多数专用文本搜索产品)中的工作方式,因为特定术语将被 忽略 ,很像 "and"、"or"、"the",甚至 "in" 也可以,因为它们并不真正被认为 有价值 到短语,这就是文本搜索的作用。
所以事实上,如果确实需要的话,正则表达式实际上会更好地匹配这些术语。
总结
这真的把我们带回了原点,因为 "text" 查询确实不属于这里。其他有用的过滤器通常与 true "geoSpatial" 搜索条件配合使用效果更好,而 true "text search" 在重要列表中的位置确实很低。
更有可能的是,人们想要的位置与他们希望访问的所需目的地的距离在 *"Set Intersection" 以内,或者至少离某些或大多数目的地足够近。当然前面提到的其他因素(*"price"、"service"等)也是人们普遍考虑的因素。
这么找结果还真不是"good fit"。如果您认为确实必须这样做,请应用 "cheat" 方法之一,或者实际上使用不同的查询,然后使用一些其他逻辑来合并每组结果。但是服务器单独做这件事确实没有意义,这也是它不尝试的原因。
所以我会专注于首先让您的地理空间匹配正确,然后应用其他对结果很重要的标准。但我真的不相信 "text search" 无论如何都可以成为其中之一。 "Cheat" 相反,但前提是你真的必须这样做。