对 Mongodb 中最近的位置进行分组

Ash*_*hav 1 aggregate geolocation mongodb aggregation-framework geonear

位置点另存为

{
  "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"
  },
  .......
  .......
}
Run Code Online (Sandbox Code Playgroud)

有几个文件。我需要到group最近的地点。分组后第一个第二个位置将在一个文档中,第三个在第二个文档中。请注意,第一和第二的位置点不相等。两个都是最近的地方。

有什么办法吗?提前致谢。

Nei*_*unn 6

快速而懒惰的解释是同时使用$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"
          }
        }
      }
    }
])
Run Code Online (Sandbox Code Playgroud)

较长的形式是您可能应该理解“为什么?” 这是如何解决问题的一部分,甚至可以选择了解即使这确实应用了至少一个仅在最近的 MongoDB 版本中引入的聚合运算符,这实际上在 MongoDB 2.4 中都是可能的。

使用 $geoNear

在任何“分组”中寻找的主要内容基本上是"distance"添加到“接近”查询结果的字段,指示该结果与搜索中使用的坐标有多远。幸运的是,这正是$geoNear聚合管道阶段所做的。

基本阶段将是这样的:

{
  "$geoNear": {
    "near": {
      "type": "Point",
      "coordinates": [
        -95.712891,
        37.09024
      ]
    },
    "spherical": true,
    "distanceField": "distance",
    "distanceMultiplier": 0.001
  }
},
Run Code Online (Sandbox Code Playgroud)

此阶段具有必须提供的三个强制性参数:

  • Near - 是用于查询的位置。这可以是传统坐标对形式,也可以是 GeoJSON 数据。任何作为 GeoJSON 的东西基本上都是以为单位考虑的,因为这是 GeoJSON 标准。

  • 球形-强制,但实际上仅当索引类型为2dsphere. 它默认为false,但您可能确实需要2dsphere地球表面上任何真实地理定位数据的索引。

  • distanceField - 这也始终是必需的,它是要添加到文档的字段的名称,该文档将包含通过near. 此结果将以弧度或米为单位,具体取决于near参数中使用的数据格式的类型。结果也受可选参数的影响,如下所述。

可选参数是:

  • distanceMultiplier - 这会将命名字段路径中的结果更改为distanceField。该乘法器被施加到返回的值,并且可以被用于“转化率”为单位所需的格式。

    注:distanceMultiplier不会适用于像其他可选参数maxDistanceminDistance。应用于这些可选参数的约束必须采用原始返回单位格式。因此,使用以GeoJSON为“最小”或“最大”的距离设定任何界限需要被计算平方米无论您转换一个distanceMultiplier值的东西,如kmmiles

这将要做的主要事情是简单地从最近到最远的顺序返回“最近”的文档(默认最多 100 个),并distanceField在现有文档内容中包含名为 the 的字段,这就是前面提到的允许您“分组”的实际输出。

distanceMultiplier这里简单地转换默认GeoJSON的以公里为输出。如果您想要输出中的英里数,那么您可以更改乘数。IE:

"distanceMultiplier": 0.000621371
Run Code Online (Sandbox Code Playgroud)

这完全是可选的,但您需要知道在下一个“分组”阶段要应用哪些单位(已转换或未转换):


实际的“分组”归结为三个不同的选项,具体取决于您可用的 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"
          }
        }
      }
    }
Run Code Online (Sandbox Code Playgroud)

另一部分是output,它基本上包含与 a 一起使用的相同累加器表达式,$group这确实应该指示您$bucket实际扩展到哪个聚合管道阶段。那些根据“分组键”进行实际的“数据收集”。

虽然很有用,但有一个小错误,$bucket_id输出将永远是数据落在约束之外boundariesdefault选项内或选项内定义的值boundaries。如果您想要“更好”的东西,通常会在客户端对结果进行后期处理时完成,例如:

result = result
  .map(({ _id, ...e }) =>
    ({
      _id: (!isNaN(parseFloat(_id)) && isFinite(_id))
        ? `less than ${bounds[bounds.indexOf(_id)+1]}km`
        : _id,
      ...e
    })
  );
Run Code Online (Sandbox Code Playgroud)

这将用更有意义的“字符串”替换返回字段中的任何普通数值,_id描述实际分组的内容。

请注意,虽然 adefault"optional",但如果任何数据超出边界范围,您将收到硬错误。事实上,返回的非常具体的错误将我们引向了下一个案例。

选项 2 - $group 和 $switch

从上面所说的内容中,您可能已经意识到来自管道阶段的“宏转换”$bucket实际上变成了一个$group阶段,并且专门将$switch运算符用作_id分组字段的参数。$switchMongoDB 3.4再次引入了运算符。

本质上,这实际上是使用对上面显示的内容进行手动构建,$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"
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

实际上一边从清“标签”的唯一的实际区别是$bucket使用$gte表达与沿$lte上的每一个case。由于$switch实际如何工作以及逻辑条件如何“落入”,就像它们在switch逻辑块的公共语言对应用法中一样,这不是必需的。

这实际上更多是关于个人偏好的问题,即您是否更愿意为语句_id内的定义输出“字符串”,case或者您是否同意后处理值以重新格式化类似的东西。

无论哪种方式,这些基本返回相同的输出(除了有一个定义的顺序,以$bucket结果)一样我们的第三个选择。

选项 3 - $group 和 $cond

如上所述,上述所有内容基本上都是基于$switch运算符,但就像它在各种编程语言实现中的对应物一样,“switch 语句”实际上只是一种更简洁、更方便的编写方式,if .. then .. else if ...等等。MongoDB 也有一个if .. then .. else回到 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"
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

同样,它真的完全一样,主要区别在于,不是将选项的“干净数组”作为“案例”进行处理,而是您拥有的是一组嵌套的条件,其中else只包含另一个$cond,直到找到“边界”的末尾,然后else只包含default值。

由于我们也至少“假装”我们要回到 MongoDB 2.4(这是实际运行 with 的约束$geoNear,然后其他类似的东西$$ROOT在该版本中不可用,因此您只需命名所有字段表达式的文档,以便使用$push.


代码生成

所有这一切都应该归结为“分组”实际上是用 完成的$bucket,除非您想要对输出进行一些自定义或者您的 MongoDB 版本不支持它(尽管您可能不应该这样做)在撰写本文时在 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();
  }

})()
Run Code Online (Sandbox Code Playgroud)

和示例输出(当然上面的所有列表都是从此代码生成的):

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",