Mongoose GeoJSON 实战指南:在 MongoDB 中存储与查询地理位置数据
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
GeoJSON 是存储地理点(Point)与多边形(Polygon)等地理对象的开放格式,MongoDB 原生支持基于 GeoJSON 的地理空间查询。本文以 docs/geojson.md 为主线,结合 mongoose 仓库中的 lib/query.js、lib/aggregate.js 源码与 test/geojson.test.js 测试用例,系统讲解如何在 Mongoose Schema 中定义 GeoJSON 字段、复用点/多边形结构,以及如何使用$geoWithin、within()、$near、$geoNear和 2dsphere 索引完成高效的地理查询。读完本文,你将掌握一套可直接复制运行的地理数据建模与查询方案。
一、GeoJSON 基础:Point 是最简单的结构
GeoJSON 中最基础的结构是点(Point)。下面的示例表示旧金山的大致位置,注意GeoJSON 坐标数组中经度在前、纬度在后,这与人们习惯的"纬度, 经度"书写顺序恰好相反:
{ "type" : "Point", "coordinates" : [ -122.5, 37.7 ] }其中coordinates是一个长度为 2 的数字数组,[-122.5, 37.7]依次为经度(longitude)与纬度(latitude)。GeoJSON 规范要求点坐标必须写成[经度, 纬度]的顺序,Schema 定义与业务代码中都应严格遵循这一点,否则查询结果会南辕北辙。
二、定义 Point Schema
在 Mongoose 中定义一个location字段为 GeoJSON 点的 Schema:
const citySchema = new mongoose.Schema({ name: String, location: { type: { type: String, // 不要写成 `{ location: { type: String } }` enum: ['Point'], // 'location.type' 必须是 'Point' required: true }, coordinates: { type: [Number], required: true } } });这里的要点是内层字段名也叫type,因此外层必须写成type: { type: String, enum: ['Point'], required: true }的形式。若写成{ location: { type: String } },Mongoose 会把它解析成普通的字符串字段,而不是嵌套的{ type, coordinates }结构,这是定义 GeoJSON 字段时最容易踩的坑。
从 Schema 校验的角度看:
enum: ['Point']限定了location.type只允许取值'Point',从数据层面保证存入的是合法 GeoJSON 点;coordinates: { type: [Number], required: true }约束坐标为数字数组,且必填。
三、用子文档复用 pointSchema
在实际项目中,往往有多个集合都需要存"点"(城市、餐厅、景点……)。借助 子文档(subdocuments) 机制,可以把pointSchema抽出来一次性定义、随处复用:
const pointSchema = new mongoose.Schema({ type: { type: String, enum: ['Point'], required: true }, coordinates: { type: [Number], required: true } }); const citySchema = new mongoose.Schema({ name: String, location: { type: pointSchema, required: true } });这样的好处是:点结构的约束(枚举、必填、坐标类型)集中在一处维护,location字段在多处引用时行为完全一致。在仓库的 test/geojson.test.js 中,正是以这种复用的pointSchema来构造City模型的。
四、定义 Polygon Schema:三重嵌套数组
多边形(Polygon)用来在地图上表示任意形状的区域。下面这个 GeoJSON 矩形近似了美国科罗拉多州的边界:
{ "type": "Polygon", "coordinates": [[ [-109, 41], [-102, 41], [-102, 37], [-109, 37], [-109, 41] ]] }多边形之所以"棘手",是因为它的坐标是三重嵌套数组:最外层是环(ring)的数组,每个环由多个点组成,每个点又是一个[经度, 纬度]数组。注意第一个点与最后一个点相同,以闭合多边形。
对应的 Mongoose Schema 定义:
const polygonSchema = new mongoose.Schema({ type: { type: String, enum: ['Polygon'], required: true }, coordinates: { type: [[[Number]]], // 数字的数组的数组的数组 required: true } }); const citySchema = new mongoose.Schema({ name: String, location: polygonSchema });核心就是type: [[[Number]]]这个三重嵌套数组类型,它精确匹配 Polygon 的坐标结构。Mongoose 会按此结构对坐标数据做类型校验与序列化,保证写入数据库的是规范的多边形坐标。
五、地理空间查询:$geoWithin 与 within() 辅助方法
Mongoose 查询支持与 MongoDB 驱动完全一致的地理空间查询操作符。例如下面的脚本:先存入一个location为丹佛市 GeoJSON 点的city文档,再用 MongoDB 的$geoWithin操作符查询科罗拉多州多边形内的所有文档:
const City = db.model('City', new Schema({ name: String, location: pointSchema })); const colorado = { type: 'Polygon', coordinates: [[ [-109, 41], [-102, 41], [-102, 37], [-109, 37], [-109, 41] ]] }; const denver = { type: 'Point', coordinates: [-104.9903, 39.7392] }; return City.create({ name: 'Denver', location: denver }). then(() => City.findOne({ location: { $geoWithin: { $geometry: colorado } } })). then(doc => assert.equal(doc.name, 'Denver'));$geoWithin判断"查询点是否位于给定几何图形内部",配合$geometry传入 Polygon 即可实现区域检索。该用例在 test/geojson.test.js 中有完整验证。
Mongoose 还提供了within()辅助方法,它是$geoWithin的便捷写法:
const denver = { type: 'Point', coordinates: [-104.9903, 39.7392] }; return City.create({ name: 'Denver', location: denver }). then(() => City.findOne().where('location').within(colorado)). then(doc => assert.equal(doc.name, 'Denver'));从 lib/query.js 的源码注释可以看到,within()定义了$within/$geoWithin参数,并且从 Mongoose 3.7 起查询一律使用$geoWithin(它与旧的$within100% 向后兼容)。within()必须在where()之后调用,并支持多种几何形式:
// 矩形框(box):左下角 + 右上角 query.where('loc').within().box(lowerLeft, upperRight); // 圆形(circle):圆心 + 半径 query.where('loc').within().circle(area); // 多边形(polygon) query.where('loc').within().polygon([10, 20], [13, 25], [7, 15]); // 球面圆形区域 query.where('loc').within().centerSphere(area); // 直接传入 GeoJSON 几何对象 query.where('loc').within({ type: 'LineString', coordinates: [...] }); // 综合形式 query.where('loc').within({ center: [50, 50], radius: 10, unique: true, spherical: true }); query.where('loc').within({ box: [[40.73, -73.9], [40.7, -73.988]] }); query.where('loc').within({ polygon: [[], [], [], []] });此外还有配套的intersects()(几何相交判断)与geometry()方法,例如query.where('loc').intersects().geometry({ type: 'Polygon', coordinates: polyA }),可用于判断两个几何图形是否重叠。若你的 MongoDB 版本过旧(MongoDB 2.4 之前),可以通过mongoose.Query.use$geoWithin = false回退到旧的$within语法,这一开关同样定义在 lib/query.js。
六、距离查询:$near 与 $geoNear
若要做"离我最近"这类按距离排序的查询,MongoDB 提供了$near查询操作符。Mongoose 的near()辅助方法支持多种调用形式(源码见 lib/query.js):
// 形式一:传入 { center, maxDistance, spherical } query.where('loc').near({ center: [10, 10], maxDistance: 5, spherical: true }); // 形式二:路径 + 参数对象 query.near('loc', { center: [10, 10], maxDistance: 5 }); // 形式三:兼容旧版的坐标/经纬度拆分写法 query.near([1, 1]); // 直接传坐标数组 query.near(1, 1); // 传两个数字 query.near('loc', [1, 2]); // 路径 + 坐标数组在 test/geojson.test.js 中可以看到$near与 2dsphere 索引的强绑定关系:若 Schema 上没有 2dsphere 索引,$near查询会直接报错unable to find index for $geoNear query,因此测试里必须先City.init()确保索引建好再查询。
在聚合管道(Aggregation Pipeline)场景下,Mongoose 通过 lib/aggregate.js 中的Aggregate#near()封装$geoNear阶段。注意$geoNear必须是管道的第一阶段:
const docs = await City.aggregate().near({ near: { type: 'Point', coordinates: [40.724, -73.997] }, distanceField: 'dist.calculated', // 必填:距离写入的字段名 maxDistance: 0.008, query: { type: 'public' }, includeLocs: 'dist.location', spherical: true });从源码可见,near()内部会校验参数非空、必须包含near属性,且 GeoJSON 点的coordinates必须是长度不小于 2 的纯数字数组,然后生成{ $geoNear: arg }追加到管道。对应测试见 test/aggregate.test.js:传入near: { type: 'Point', coordinates: [1, 2] }后,管道被构造成[{ $geoNear: { near: { type: 'Point', coordinates: [1, 2] } } }]。
必须牢记:$near查询操作符和$geoNear聚合阶段都强制要求 2dsphere 索引,否则 MongoDB 会拒绝执行查询。
七、2dsphere 地理空间索引
2dsphere 索引用于加速球面上的地理空间查询。在 Mongoose 中定义 GeoJSON 字段的 2dsphere 索引有两种方式。
方式一:字段级index选项
const denver = { type: 'Point', coordinates: [-104.9903, 39.7392] }; const City = db.model('City', new Schema({ name: String, location: { type: pointSchema, index: '2dsphere' // 在 `City.location` 上创建 2dsphere 索引 } })); return City.create({ name: 'Denver', location: denver }). then(() => City.findOne().where('location').within(colorado)). then(doc => assert.equal(doc.name, 'Denver'));index: '2dsphere'是声明式写法,City.init()(或模型首次使用触发自动建索引)时会在location字段上创建球面地理索引。
方式二:Schema#index()方法
citySchema.index({ location: '2dsphere' });两种方式等价,后者更便于把全部索引集中管理。凡是需要$geoWithin、$near、$geoNear高性能执行的字段,都应提前创建 2dsphere 索引;数据量增长后,缺少索引的地理查询会产生全表扫描,性能急剧下降。
八、完整流程与测试验证
将以上要点串成一条完整链路:定义pointSchema→ 嵌入业务 Schema → 写入 GeoJSON 点 → 建 2dsphere 索引 → 区域/距离查询。仓库中的 test/geojson.test.js 是官方对这一整套流程的自动化验证,包含四个用例:
| 测试用例 | 验证内容 | 对应源码位置 |
|---|---|---|
driver query | 用$geoWithin+$geometry查询多边形内的点 | test/geojson.test.js |
within helper | 用within()辅助方法等价实现 | test/geojson.test.js |
index | 字段级index: '2dsphere'下查询可正常执行 | test/geojson.test.js |
near | $near依赖 2dsphere 索引,无索引即报错 | test/geojson.test.js |
写业务代码时可以直接把文档中的示例搬进自己的项目,配合City.init()确认索引就绪后再执行地理查询,即可复现全部行为。
九、实践要点小结
- 经度在前,纬度在后:GeoJSON 坐标恒为
[经度, 纬度],切勿写反。 - 内层
type命名冲突:GeoJSON 字段的内层属性名为type,必须用type: { type: String, enum: [...] }的写法。 - Polygon 用三重嵌套数组:
coordinates声明为[[[Number]]]。 - 复用优先:把
pointSchema/polygonSchema抽成独立 Schema,通过子文档嵌入各处复用(见 docs/subdocs.md)。 - 区域查询用
$geoWithin/within(),距离查询用$near/$geoNear,二者都要求 2dsphere 索引。 $geoNear必须是聚合管道第一阶段,且distanceField必填。- 旧版 MongoDB 兼容:可通过
Query.use$geoWithin = false回退到$within(lib/query.js)。
按照上述方案,你可以在 Mongoose 中完整落地"存储地理对象 + 区域筛选 + 距离排序"的地理能力,实现附近的店铺、围栏内的车辆、行政区划统计等典型业务场景。
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考