【问题标题】:Grouping nearest locations in Mongodb在MongoDB中对最近的位置进行分组
【发布时间】:2019-04-03 07:49:35
【问题描述】:

位置点另存为

{
  "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 它。 分组后,第一个第二个位置将在一个文档中,第三个在第二个文档中。 请注意,第一个和第二个的位置点不相等。两者都是最近的地方。

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

【问题讨论】:

  • 您认为“最近分组”实际上是什么意思?显示一些文档的示例以及您期望作为查询输出的内容。一份文件真的没有告诉我们,除了它可能是最近的,因此是结果。同时显示任何尝试过的代码,因为至少它可能会提供更多关于您实际要求的指示。
  • 更新了问题。
  • 仍然不仅仅是有点模糊。当我说要演示您的实际意思时,所提供文件的预期结果至少可以让您了解您的期望。也许您的意思是“5公里以内”和“5到10公里之间”的“组”,等等直到“超过100公里”。但是,如果没有显示实际期望,“最近分组”实际上并不能告诉我们任何事情。你可以而且真的“应该”更具描述性。
  • 是的。你说的。像“5公里以内”的“群”;
  • 您认为提供的答案中是否有某些内容无法解决您的问题?如果是这样,那么请对答案发表评论,以澄清究竟需要解决哪些尚未解决的问题。如果它确实回答了您提出的问题,请注意Accept your Answers您提出的问题

标签: mongodb geolocation aggregation-framework aggregate geonear


【解决方案1】:

又快又懒的解释是同时使用$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 参数中使用的数据格式的类型。结果也受 optional 参数的影响,如下所述。

可选参数是:

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

    注意:distanceMultiplier 确实适用于其他可选参数,例如 maxDistanceminDistance。应用于这些可选参数的约束必须采用原始返回单位格式。因此,对于 GeoJSON,任何为“最小”或“最大”距离设置的界限都需要计算为 ,无论您是否将 distanceMultiplier 值转换为 kmmiles 之类的值。

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

这里的distanceMultiplier只是简单地将GeoJSON的默认转换为公里输出。如果您想要输出中的 miles,那么您将更改乘数。即:

"distanceMultiplier": 0.000621371

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


实际的“分组”根据您可用的 MongoDB 和您的实际需求归结为三个不同的选项:

选项 1 - $bucket

$bucket 管道阶段是随 MongoDB 3.4 添加的。它实际上是该版本中添加的几个“管道阶段”之一,它们实际上更像是一个宏函数简写的基本形式> 用于编写管道阶段和实际操作符的组合。稍后会详细介绍。

主要的基本参数是groupBy 表达式,boundaries 指定“分组”范围的 界限,以及default 选项只要与groupBy 表达式匹配的数据不在boundaries 定义的条目之间,基本上就会在输出中用作*“分组键”或_id 字段。

    {
      "$bucket": {
        "groupBy": "$distance",
        "boundaries": [
          0, 5, 10, 20,  50,  100,  500
        ],
        "default": "greater than 500km",
        "output": {
          "count": {
            "$sum": 1
          },
          "docs": {
            "$push": "$$ROOT"
          }
        }
      }
    }

另一部分是output,它基本上包含与$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
    })
  );

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

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

选项 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 在每个case 上使用$gte 表达式和$lte。这不是必需的,因为 $switch 的实际工作方式以及逻辑条件如何“通过”,就像它们在 switch 逻辑块的通用语言对应用法中一样。

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

无论哪种方式,这些基本上都返回相同的输出(除了定义 order$bucket results ),就像我们的第三个选项一样。

选项 3 - $group 和 $cond

如上所述,上述所有内容基本上都是基于$switch 运算符,但就像它在各种编程语言实现中的对应物一样,“switch 语句”实际上只是一种更简洁、更方便的编写if .. then .. else if ... 的方式等等. MongoDB 也有一个 if .. then .. else 表达式,它可以通过 $cond 回到 MongoDB 2.2:

{
  "$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 完成的,除非您想要对输出进行一些自定义或者您的 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();
  }

})()

以及示例输出(当然,上面的所有列表都是由此代码生成的):

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

【讨论】:

  • 这是一个很好的答案! ?
猜你喜欢
  • 2011-11-07
  • 1970-01-01
  • 2020-08-08
  • 2013-06-13
  • 1970-01-01
  • 1970-01-01
  • 2021-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多