mongodb - 在MongoDB中对最近的位置进行分组
问题描述
位置点另存为
{
"location_point" : {
"coordinates" : [
-95.712891,
37.09024
],
"type" : "Point"
},
"location_point" : {
"coordinates" : [
-95.712893,
37.09024
],
"type" : "Point"
},
"location_point" : {
"coordinates" : [
-85.712883,
37.09024
],
"type" : "Point"
},
.......
.......
}
有几个文件。我需要到group
最近的位置。分组后,第一个第二个位置将在一个文档中,第三个在第二个文档中。请注意,第一个和第二个的位置点不相等。两者都是最近的地方。
有什么办法吗?提前致谢。
解决方案
快速而懒惰的解释是同时使用$geoNear
和$bucket
聚合管道阶段来获得结果:
.aggregate([
{
"$geoNear": {
"near": {
"type": "Point",
"coordinates": [
-95.712891,
37.09024
]
},
"spherical": true,
"distanceField": "distance",
"distanceMultiplier": 0.001
}
},
{
"$bucket": {
"groupBy": "$distance",
"boundaries": [
0, 5, 10, 20, 50, 100, 500
],
"default": "greater than 500km",
"output": {
"count": {
"$sum": 1
},
"docs": {
"$push": "$$ROOT"
}
}
}
}
])
更长的形式是您可能应该理解“为什么?” 这是如何解决问题的一部分,甚至可以选择理解即使这确实应用了至少一个仅在最近的 MongoDB 版本中引入的聚合运算符,但实际上这一切都可以追溯到 MongoDB 2.4。
使用 $geoNear
在任何“分组”中要查找的主要内容基本上是"distance"
添加到“近”查询的结果中的字段,指示该结果与搜索中使用的坐标的距离。幸运的是,这正是$geoNear
聚合管道阶段所做的。
基本阶段将是这样的:
{
"$geoNear": {
"near": {
"type": "Point",
"coordinates": [
-95.712891,
37.09024
]
},
"spherical": true,
"distanceField": "distance",
"distanceMultiplier": 0.001
}
},
此阶段具有必须提供的三个强制参数:
near - 是用于查询的位置。这可以是传统坐标对形式或 GeoJSON 数据。任何作为 GeoJSON 的东西基本上都以米为单位来考虑结果,因为那是 GeoJSON 标准。
球形-强制,但仅当索引类型为
2dsphere
. 它默认为false
,但您可能确实需要2dsphere
地球表面上任何真实地理位置数据的索引。distanceField - 这也始终是必需的,它是要添加到文档中的字段的名称,其中将包含与查询位置的距离
near
。此结果将以弧度或米为单位,具体取决于near
参数中使用的数据格式的类型。结果也受可选参数的影响,如下所述。
可选参数是:
distanceMultiplier - 这会将命名字段路径中的结果更改为
distanceField
. 乘数应用于返回值,可用于将单位“转换”为所需格式。注意:不适
distanceMultiplier
用于其他可选参数,例如maxDistance
orminDistance
。应用于这些可选参数的约束必须采用原始返回的单位格式。因此,对于 GeoJSON,任何为“最小”或“最大”距离设置的界限都需要以米为单位计算,无论您是否distanceMultiplier
使用类似km
或的值转换了值miles
。
这要做的主要事情是简单地返回“最近的”文档(默认情况下最多 100 个),并按从最近到最远的顺序distanceField
返回,并在现有文档内容中包含名为 the 的字段,这就是前面提到的允许您“分组”的实际输出。
这里distanceMultiplier
只是将GeoJSON的默认米转换为公里以进行输出。如果您想要输出中的英里数,那么您将更改乘数。IE:
"distanceMultiplier": 0.000621371
它完全是可选的,但您需要了解在下一个“分组”阶段要应用哪些单位(已转换或未转换):
实际的“分组”归结为三个不同的选项,具体取决于您可用的 MongoDB 和您的实际需求:
选项 1 - $bucket
$bucket
管道阶段是在 MongoDB 3.4 中添加的。它实际上是该版本中添加的几个“管道阶段”之一,它们实际上更像是宏函数或用于编写流水线阶段和实际运算符组合的基本简写形式。稍后再谈。
主要的基本参数是groupBy
表达式,boundaries
它指定“分组”范围的下限,以及一个选项,只要与表达式匹配的数据不落在定义的条目之间,该default
选项基本上用作_id
输出中的 *“分组键”或字段groupBy
与boundaries
.
{
"$bucket": {
"groupBy": "$distance",
"boundaries": [
0, 5, 10, 20, 50, 100, 500
],
"default": "greater than 500km",
"output": {
"count": {
"$sum": 1
},
"docs": {
"$push": "$$ROOT"
}
}
}
}
另一部分是output
,它基本上包含与 a 一起使用的相同的累加器表达式$group
,并且它确实应该为您提供$bucket
实际扩展到哪个聚合管道阶段的指示。那些根据“分组键”进行实际的“数据收集”。
虽然有用,但有一个小错误,$bucket
即_id
输出只会是在数据超出约束boundaries
的选项内或选项内定义的值。如果您想要“更好”的东西,通常会在结果的客户端后处理中完成,例如:default
boundaries
result = result
.map(({ _id, ...e }) =>
({
_id: (!isNaN(parseFloat(_id)) && isFinite(_id))
? `less than ${bounds[bounds.indexOf(_id)+1]}km`
: _id,
...e
})
);
这将用更有意义的“字符串”替换返回字段中的任何普通数值_id
,描述实际分组的内容。
请注意,虽然 adefault
是“可选的”,但如果任何数据超出边界范围,您将收到一个硬错误。事实上,返回的非常具体的错误导致我们进入下一个案例。
选项 2 - $group 和 $switch
从上面所说的,您可能已经意识到,来自管道阶段的“宏转换”$bucket
实际上变成了一个$group
阶段,并且专门将运算符用作分组字段$switch
的参数。_id
MongoDB 3.4再次$switch
引入了操作符。
本质上,这实际上是使用 对上面显示的内容进行的手动构造,$bucket
对字段的输出进行了一些微调,并且对前者产生的表达式_id
不太简洁。事实上,您可以使用聚合管道的“解释”输出来查看与以下清单“相似”的内容,但使用上面定义的管道阶段:
{
"$group": {
"_id": {
"$switch": {
"branches": [
{
"case": {
"$and": [
{
"$lt": [
"$distance",
5
]
},
{
"$gte": [
"$distance",
0
]
}
]
},
"then": "less than 5km"
},
{
"case": {
"$and": [
{
"$lt": [
"$distance",
10
]
}
]
},
"then": "less than 10km"
},
{
"case": {
"$and": [
{
"$lt": [
"$distance",
20
]
}
]
},
"then": "less than 20km"
},
{
"case": {
"$and": [
{
"$lt": [
"$distance",
50
]
}
]
},
"then": "less than 50km"
},
{
"case": {
"$and": [
{
"$lt": [
"$distance",
100
]
}
]
},
"then": "less than 100km"
},
{
"case": {
"$and": [
{
"$lt": [
"$distance",
500
]
}
]
},
"then": "less than 500km"
}
],
"default": "greater than 500km"
}
},
"count": {
"$sum": 1
},
"docs": {
"$push": "$$ROOT"
}
}
}
事实上,除了更清晰的“标签”$bucket
之外,唯一的实际区别是$gte
在$lte
每个case
. 这不是必需的,因为$switch
实际工作方式以及逻辑条件如何“失败”,就像它们在switch
逻辑块的通用语言对应用法中一样。
这实际上更多地取决于个人偏好问题,即您是否更乐意为语句中定义输出“字符串”,_id
或者case
您是否可以对后处理值进行重新格式化以重新格式化此类内容。
无论哪种方式,这些基本上返回与我们的第三个选项相同的输出(除了对结果有定义的顺序)。$bucket
选项 3 - $group 和 $cond
如前所述,上述所有内容基本上都是基于$switch
运算符的,但就像它在各种编程语言实现中的对应物一样,“switch 语句”实际上只是一种更清洁、更方便的编写方式if .. then .. else if ...
等等。if .. then .. else
MongoDB在 MongoDB 2.2 中也有一个表达式$cond
:
{
"$group": {
"_id": {
"$cond": [
{
"$and": [
{
"$lt": [
"$distance",
5
]
},
{
"$gte": [
"$distance",
0
]
}
]
},
"less then 5km",
{
"$cond": [
{
"$and": [
{
"$lt": [
"$distance",
10
]
}
]
},
"less then 10km",
{
"$cond": [
{
"$and": [
{
"$lt": [
"$distance",
20
]
}
]
},
"less then 20km",
{
"$cond": [
{
"$and": [
{
"$lt": [
"$distance",
50
]
}
]
},
"less then 50km",
{
"$cond": [
{
"$and": [
{
"$lt": [
"$distance",
100
]
}
]
},
"less then 100km",
"greater than 500km"
]
}
]
}
]
}
]
}
]
},
"count": {
"$sum": 1
},
"docs": {
"$push": {
"_id": "$_id",
"location_point": "$location_point",
"distance": "$distance"
}
}
}
}
同样,它实际上都是一样的,主要区别在于,不是一个“干净的数组”选项来处理为“案例”,而是你拥有的是一组嵌套的条件,其中else
只包含另一个$cond
,直到找到“边界”的末尾,然后else
只包含该default
值。
由于我们至少还“假装”我们将回到 MongoDB 2.4(这是实际运行的约束条件)$geoNear
,因此其他类似的东西$$ROOT
在该版本中不可用,因此您只需命名所有字段表达式的文档,以便使用$push
.
代码生成
所有这一切真的应该归结为“分组”实际上是$bucket
用在撰写本文时在 3.4 下运行任何 MongoDB)。
当然,所需语法中的任何其他形式都更长,但实际上可以应用相同的参数数组来基本上生成和运行上面显示的任何一种形式。
下面是一个示例清单(针对 NodeJS),它表明它实际上只是一个简单的过程,从一个简单bounds
的分组数组生成这里的所有内容,甚至只是几个定义的选项,它们都可以在管道操作中重复使用作为用于生成流水线指令或将返回的结果操作为“更漂亮”的输出格式的任何客户端预处理或后处理。
const { Schema } = mongoose = require('mongoose');
const uri = 'mongodb://localhost:27017/test',
options = { useNewUrlParser: true };
mongoose.set('useFindAndModify', false);
mongoose.set('useCreateIndex', true);
mongoose.set('debug', true);
const geoSchema = new Schema({
location_point: {
type: { type: String, enum: ["Point"], default: "Point" },
coordinates: [Number, Number]
}
});
geoSchema.index({ "location_point": "2dsphere" },{ background: false });
const GeoModel = mongoose.model('GeoModel', geoSchema, 'geojunk');
const [{ location_point: near }] = data = [
[ -95.712891, 37.09024 ],
[ -95.712893, 37.09024 ],
[ -85.712883, 37.09024 ]
].map(coordinates => ({ location_point: { type: 'Point', coordinates } }));
const log = data => console.log(JSON.stringify(data, undefined, 2));
(async function() {
try {
const conn = await mongoose.connect(uri, options);
// Clean data
await Promise.all(
Object.entries(conn.models).map(([k,m]) => m.deleteMany())
);
// Insert data
await GeoModel.insertMany(data);
const bounds = [ 5, 10, 20, 50, 100, 500 ];
const distanceField = "distance";
// Run three sample cases
for ( let test of [0,1,2] ) {
let pipeline = [
{ "$geoNear": {
near,
"spherical": true,
distanceField,
"distanceMultiplier": 0.001
}},
(() => {
// Standard accumulators
const output = {
"count": { "$sum": 1 },
"docs": { "$push": "$$ROOT" }
};
switch (test) {
case 0:
log("Using $bucket");
return (
{ "$bucket": {
"groupBy": `$${distanceField}`,
"boundaries": [ 0, ...bounds ],
"default": `greater than ${[...bounds].pop()}km`,
output
}}
);
case 1:
log("Manually using $switch");
let branches = bounds.map((bound,i) =>
({
'case': {
'$and': [
{ '$lt': [ `$${distanceField}`, bound ] },
...((i === 0) ? [{ '$gte': [ `$${distanceField}`, 0 ] }]: [])
]
},
'then': `less than ${bound}km`
})
);
return (
{ "$group": {
"_id": {
"$switch": {
branches,
"default": `greater than ${[...bounds].pop()}km`
}
},
...output
}}
);
case 2:
log("Legacy using $cond");
let _id = null;
for (let i = bounds.length -1; i > 0; i--) {
let rec = {
'$cond': [
{ '$and': [
{ '$lt': [ `$${distanceField}`, bounds[i-1] ] },
...((i == 1) ? [{ '$gte': [ `$${distanceField}`, 0 ] }] : [])
]},
`less then ${bounds[i-1]}km`
]
};
if ( _id == null ) {
rec['$cond'].push(`greater than ${bounds[i]}km`);
} else {
rec['$cond'].push( _id );
}
_id = rec;
}
// Older MongoDB may require each field instead of $$ROOT
output.docs.$push =
["_id", "location_point", distanceField]
.reduce((o,e) => ({ ...o, [e]: `$${e}` }),{});
return ({ "$group": { _id, ...output } });
}
})()
];
let result = await GeoModel.aggregate(pipeline);
// Text based _id for test: 0 with $bucket
if ( test === 0 )
result = result
.map(({ _id, ...e }) =>
({
_id: (!isNaN(parseFloat(_id)) && isFinite(_id))
? `less than ${bounds[bounds.indexOf(_id)+1]}km`
: _id,
...e
})
);
log({ pipeline, result });
}
} catch (e) {
console.error(e)
} finally {
mongoose.disconnect();
}
})()
和示例输出(当然,上面的所有列表都是从此代码生成的):
Mongoose: geojunk.createIndex({ location_point: '2dsphere' }, { background: false })
"Using $bucket"
{
"result": [
{
"_id": "less than 5km",
"count": 2,
"docs": [
{
"_id": "5ca897dd2efdc41b79d5fe94",
"location_point": {
"type": "Point",
"coordinates": [
-95.712891,
37.09024
]
},
"__v": 0,
"distance": 0
},
{
"_id": "5ca897dd2efdc41b79d5fe95",
"location_point": {
"type": "Point",
"coordinates": [
-95.712893,
37.09024
]
},
"__v": 0,
"distance": 0.00017759511720976155
}
]
},
{
"_id": "greater than 500km",
"count": 1,
"docs": [
{
"_id": "5ca897dd2efdc41b79d5fe96",
"location_point": {
"type": "Point",
"coordinates": [
-85.712883,
37.09024
]
},
"__v": 0,
"distance": 887.5656539981669
}
]
}
]
}
"Manually using $switch"
{
"result": [
{
"_id": "greater than 500km",
"count": 1,
"docs": [
{
"_id": "5ca897dd2efdc41b79d5fe96",
"location_point": {
"type": "Point",
"coordinates": [
-85.712883,
37.09024
]
},
"__v": 0,
"distance": 887.5656539981669
}
]
},
{
"_id": "less than 5km",
"count": 2,
"docs": [
{
"_id": "5ca897dd2efdc41b79d5fe94",
"location_point": {
"type": "Point",
"coordinates": [
-95.712891,
37.09024
]
},
"__v": 0,
"distance": 0
},
{
"_id": "5ca897dd2efdc41b79d5fe95",
"location_point": {
"type": "Point",
"coordinates": [
-95.712893,
37.09024
]
},
"__v": 0,
"distance": 0.00017759511720976155
}
]
}
]
}
"Legacy using $cond"
{
"result": [
{
"_id": "greater than 500km",
"count": 1,
"docs": [
{
"_id": "5ca897dd2efdc41b79d5fe96",
"location_point": {
"type": "Point",
"coordinates": [
-85.712883,
37.09024
]
},
"distance": 887.5656539981669
}
]
},
{
"_id": "less then 5km",
"count": 2,
"docs": [
{
"_id": "5ca897dd2efdc41b79d5fe94",
"location_point": {
"type": "Point",
"coordinates": [
-95.712891,
37.09024
]
},
"distance": 0
},
{
"_id": "5ca897dd2efdc41b79d5fe95",
"location_point": {
"type": "Point",
"coordinates": [
-95.712893,
37.09024
]
},
"distance": 0.00017759511720976155
}
]
}
]
}
推荐阅读
- css - 带有 transform rotateX 的 CSS 错误有时会旋转太多
- javascript - 在 Angular 8 /9 中,我可以在 app.module.ts 中确定我的 URL 并在声明模块时使用它吗?
- xml - xsl 中的元素:模板被忽略、XML、XSL、XHTML
- python - Append 不会在 Python 中“循环”我的搜索代码
- javascript - 使用重置按钮重置表格上的数据
- getstream-io - 为什么所有活动都显示 Unknown for the User/Actor .NET Server API w/React-JS 客户端组件
- python - 变异二维列表
- r - 如何在 R 中打印 x 轴上的所有标签?
- node.js - 在 html 中使用 NodeJS 函数
- python - 使用 matplotlib 和 y twinx 打印图例时出错