将Mongodb中最近的位置分组

将Mongodb中最近的位置分组,mongodb,geolocation,aggregation-framework,aggregate,geonear,Mongodb,Geolocation,Aggregation Framework,Aggregate,Geonear,位置点另存为 { "location_point" : { "coordinates" : [ -95.712891, 37.09024 ], "type" : "Point" }, "location_point" : { "coordinates" : [ -95.712893, 37.09024 ], "type" : "Point" }, "location_point" : { "c

位置点另存为

{
  "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"
  },
  .......
  .......
}
有几个文件。我需要按最近的位置进行分组。 分组后,第二个位置将位于一个文档中,第三个位置位于第二个文档中。 请不要认为第一个和第二个的位置点不相等。两者都是最近的地方


有办法吗?提前感谢。

快速而懒惰的解释是同时使用和聚合管道阶段来获得结果:

.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 在任何分组中要查找的主要内容基本上是一个距离字段,该字段添加到一个near查询的结果中,指示该结果与搜索中使用的坐标之间的距离。幸运的是,这正是聚合管道阶段所做的

基本阶段如下:

{
  "$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参数中使用的数据格式类型。结果还受可选参数的影响,如下所述

可选参数为:

distanceMultiplier-这会将命名字段路径中的结果更改为distanceField。乘数应用于返回值,并可用于将单位转换为所需格式

注意:距离乘数不适用于其他可选参数,如maxDistance或minDistance。应用于这些可选参数的约束必须采用原始返回单位格式。因此,使用GeoJSON时,最小或最大距离的任何边界都需要以米为单位进行计算,而不管您是将距离乘数值转换为公里还是英里

这将要做的主要事情是按照从最近到最远的顺序返回最接近的文档,默认情况下最多返回100个,并在现有文档内容中包含名为distanceField的字段,这就是前面提到的实际输出,允许您分组

这里的距离乘数只是将GeoJSON的默认米数转换为公里数进行输出。如果你想在输出中输入英里数,那么你需要改变乘数。i、 e:

"distanceMultiplier": 0.000621371
这是完全可选的,但您需要知道在下一个分组阶段中转换或不转换的单位:

根据可用MongoDB和您的实际需要,实际分组可归结为三种不同的选项:

选项1-$bucket 管道阶段添加了MongoDB 3.4。它实际上是该版本中添加的几个管道阶段中的一个,更像是一个宏函数,或者是编写管道阶段和实际操作符组合的一种基本形式的速记。稍后再谈

主要的基本参数是groupBy表达式、指定分组范围下限的边界以及默认选项,只要与groupBy表达式匹配的数据不在用边界定义的条目之间,该选项基本上作为*分组键或_id字段应用于输出

另一部分是输出,它基本上包含了与将用于的相同的累加器表达式,这确实应该为您提供一个指示,说明它实际上扩展到了哪个聚合管道阶段。它们对每个分组键进行实际数据收集

虽然有用,但存在一个小错误,即_id输出将仅为边界内定义的值,或在数据超出边界约束的默认选项内定义的值。如果您想要更好的结果,通常会在客户端对结果进行后处理时进行,例如:

result = result
  .map(({ _id, ...e }) =>
    ({
      _id: (!isNaN(parseFloat(_id)) && isFinite(_id))
        ? `less than ${bounds[bounds.indexOf(_id)+1]}km`
        : _id,
      ...e
    })
  );
这将用一个更有意义的字符串替换返回的_id字段中的任何普通数值,该字符串描述实际分组的内容

请注意,虽然默认值是可选的,但如果任何数据超出边界范围,您将收到硬错误。事实上,非常具体的错误返回 埃德把我们带到下一个案子

选项2-$组和$开关 从上面所说的,您可能已经意识到,来自管道阶段的宏转换实际上变成了一个阶段,并且专门将操作符作为其参数应用于_id字段以进行分组。MongoDB 3.4再次引入了该操作符

本质上,这实际上是上面所示的手动构造,只需对_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"
    }
  }
}
事实上,除了更清晰的标签外,唯一的实际区别是在每一个案例中使用表达式和。这是不必要的,因为实际上是如何工作的,以及逻辑条件是如何通过的,就像在开关逻辑块的公共语言对应使用中一样

这实际上更多的是关于个人偏好的问题,即您是否更乐意在case语句中定义_id的输出字符串,或者您是否愿意使用后处理值来重新格式化类似的内容

无论哪种方式,它们基本上都会返回相同的输出,除了与我们的第三个选项一样,对$bucket results有一个已定义的订单

选项3-$group和$cond 如上所述,所有这些基本上都是基于运算符的,但就像它在各种编程语言实现中的对应项一样,switch语句实际上只是一种更干净、更方便的编写方法,如果。。然后否则如果。。。等等MongoDB还有一个if。。然后else表达式返回到MongoDB 2.2,带有:

同样,这一切都是一样的,主要的区别在于没有一个干净的选项数组作为案例处理,而是一组嵌套的条件,其中else只包含另一个,直到找到边界的末端,然后else只包含默认值

由于我们至少还假装要追溯到MongoDB 2.4,这是实际运行时的约束条件,因此$$ROOT之类的其他内容在该版本中不可用,因此您只需命名文档的所有字段表达式,以便使用

代码生成 所有这一切都应该归结为分组实际上是用完成的,并且它可能是您将使用的,除非您想要对输出进行一些定制,或者如果您的MongoDB版本不支持它,尽管在撰写本文时您可能不应该在3.4下运行任何MongoDB

当然,在所需的语法中,任何其他形式都更长,但实际上,可以应用相同的参数数组来生成和运行上面显示的任何一种形式

下面是NodeJS的一个示例列表,它演示了从一个简单的分组边界数组生成所有内容实际上只是一个简单的过程,甚至只有几个已定义的选项,它们既可以在管道操作中重复使用,也可以在任何客户端预处理或后处理中重复使用,以生成管道指令,或将返回的结果处理为更漂亮的输出格式

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
        }
      ]
    }
  ]
}

快速而懒惰的解释是使用和聚合管道阶段来获得结果:

.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 在任何分组中要查找的主要内容基本上是一个距离字段,该字段添加到一个near查询的结果中,指示该结果与搜索中使用的坐标之间的距离。幸运的是,这正是聚合管道阶段所做的

基本阶段如下:

{
  "$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参数中使用的数据格式类型。结果还受可选参数的影响,如下所述

可选参数为:

distanceMultiplier-这会将命名字段路径中的结果更改为distanceField。乘数应用于返回值,并可用于将单位转换为所需格式

注意:距离乘数不适用于其他可选参数,如maxDistance或minDistance。应用于这些可选参数的约束必须采用原始返回单位格式。因此,使用GeoJSON时,最小或最大距离的任何边界都需要以米为单位进行计算,而不管您是将距离乘数值转换为公里还是英里

这将要做的主要事情是按照从最近到最远的顺序返回最接近的文档,默认情况下最多返回100个,并在现有文档内容中包含名为distanceField的字段,这就是前面提到的实际输出,允许您分组

这里的距离乘数只是将GeoJSON的默认米数转换为公里数进行输出。如果你想在输出中输入英里数,那么你需要改变乘数。i、 e:

"distanceMultiplier": 0.000621371
这是完全可选的,但您需要知道在下一个分组阶段中转换或不转换的单位:

根据可用MongoDB和您的实际需要,实际分组可归结为三种不同的选项:

选项1-$bucket 管道阶段添加了MongoDB 3.4。它实际上是该版本中添加的几个管道阶段中的一个,更像是一个宏函数,或者是编写管道阶段和实际操作符组合的一种基本形式的速记。稍后再谈

主要的基本参数是groupBy表达式、指定分组范围下限的边界以及默认选项,只要与groupBy表达式匹配的数据不在用边界定义的条目之间,该选项基本上作为*分组键或_id字段应用于输出

另一部分是输出,它基本上包含了与将用于的相同的累加器表达式,这确实应该为您提供一个指示,说明它实际上扩展到了哪个聚合管道阶段。它们对每个分组键进行实际数据收集

虽然有用,但存在一个小错误,即_id输出将仅为边界内定义的值,或在数据超出边界约束的默认选项内定义的值。如果您想要更好的结果,通常会在客户端对结果进行后处理时进行,例如:

result = result
  .map(({ _id, ...e }) =>
    ({
      _id: (!isNaN(parseFloat(_id)) && isFinite(_id))
        ? `less than ${bounds[bounds.indexOf(_id)+1]}km`
        : _id,
      ...e
    })
  );
这将用一个更有意义的字符串替换返回的_id字段中的任何普通数值,该字符串描述实际分组的内容

请注意,虽然默认值是可选的,但如果任何数据超出边界范围,您将收到硬错误。事实上,返回的非常具体的错误导致我们进入下一个案例

选项2-$组和$开关 从上面所说的,您可能已经意识到,来自管道阶段的宏转换实际上变成了一个阶段,并且专门将操作符作为其参数应用于_id字段以进行分组。MongoDB 3.4再次引入了该操作符

本质上,这实际上是上面所示的手动构造,只需对_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"
    }
  }
}
事实上,除了更清晰的标签外,唯一的实际区别是在每一个案例中使用表达式和。这是不必要的,因为实际上是如何工作的,以及逻辑条件是如何通过的,就像在开关逻辑块的公共语言对应使用中一样

这实际上更多的是关于个人偏好的问题,即您是否更乐意在case语句中定义_id的输出字符串,或者您是否愿意使用后处理值来重新格式化类似的内容

无论哪种方式,它们基本上都会返回相同的输出,除了与我们的第三个选项一样,对$bucket results有一个已定义的订单

选项3-$group和$cond 如上所述,所有这些基本上都是基于运算符的,但就像它在各种编程语言实现中的对应项一样,switch语句实际上只是一种更干净、更方便的编写方法,如果。。然后否则如果。。。等等MongoDB还有一个if。。然后else表达式返回到MongoDB 2.2,带有:

A 实际上,它们都是一样的,主要区别在于,没有一个干净的选项数组作为案例处理,而是一组嵌套的条件,其中else只包含另一个,直到找到边界的末端,然后else只包含默认值

由于我们至少还假装要追溯到MongoDB 2.4,这是实际运行时的约束条件,因此$$ROOT之类的其他内容在该版本中不可用,因此您只需命名文档的所有字段表达式,以便使用

代码生成 所有这一切都应该归结为分组实际上是用完成的,并且它可能是您将使用的,除非您想要对输出进行一些定制,或者如果您的MongoDB版本不支持它,尽管在撰写本文时您可能不应该在3.4下运行任何MongoDB

当然,在所需的语法中,任何其他形式都更长,但实际上,可以应用相同的参数数组来生成和运行上面显示的任何一种形式

下面是NodeJS的一个示例列表,它演示了从一个简单的分组边界数组生成所有内容实际上只是一个简单的过程,甚至只有几个已定义的选项,它们既可以在管道操作中重复使用,也可以在任何客户端预处理或后处理中重复使用,以生成管道指令,或将返回的结果处理为更漂亮的输出格式

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
        }
      ]
    }
  ]
}

你认为最近距离分组实际上是什么意思?显示一些文档的示例以及您希望作为查询输出的内容。有一份文件确实没有告诉我们,除了它可能是最近的,因此是结果。同时显示任何尝试的代码,至少它可能会给出一些关于您实际询问的更多指示。更新了问题。仍然不仅仅是一点模糊。当我说要证明你的实际意思时,那么从提供的文档中得到的预期结果至少会让你对你的预期有所了解。也许你指的是5公里以内,5到10公里之间,以及100公里以上的人群。但是,如果没有显示最近的实际期望组,这并不能告诉我们任何事情。你可以而且应该更具描述性。是的。你说对了。5公里以内的类似群体;提供的答案中是否有您认为无法解决您问题的内容?如果是,请对答案进行评论,以澄清哪些问题需要解决,哪些问题尚未解决。如果它确实回答了您提出的问题,那么请注意您提出的问题。您认为最接近的方式是什么?显示一些文档的示例以及您希望作为查询输出的内容。有一份文件确实没有告诉我们,除了它可能是最近的,因此是结果。同时显示任何尝试的代码,至少它可能会给出一些关于您实际询问的更多指示。更新了问题。仍然不仅仅是一点模糊。当我说要证明你的实际意思时,那么从提供的文档中得到的预期结果至少会让你对你的预期有所了解。也许你指的是5公里以内,5到10公里之间,以及100公里以上的人群。但是,如果没有显示最近的实际期望组,这并不能告诉我们任何事情。你可以而且应该更具描述性。是的。你说对了。5公里以内的类似群体;提供的答案中是否有您认为无法解决您问题的内容?如果是,请对答案进行评论,以澄清哪些问题需要解决,哪些问题尚未解决。如果它确实回答了您提出的问题,那么请注意您提出的问题这是一个很好的答案!这是一个很好的答案!