使用地图瓦片索引实现地理聚合

Posted on Sep 3, 2022

在处理大规模的散点数据时,有时候我们需要提供一个只读的查询 API 在地图上做可视化。 当数据量过大,比如百万这个量级,将数据一口气全部返回给前端在浏览器上处理是不太合适的。 应当在后端服务内完成一定聚合,将聚合后的搜索返回给前端。 这里介绍下在 Go 中如何使用 MongoDB + Tile 索引实现这件事。

我们从最基础的数据开始,假设地图上的的一个点记录可以用下面的方式定义:

type GeoPoint struct {
	Type        string    `bson:"type" json:"type"`
	Coordinates []float64 `bson:"coordinates" json:"coordinates"`
}

type Record struct {
	ID       primitive.ObjectID `bson:"_id"`                      // ObjectID
	Location GeoPoint           `bson:"location" json:"location"` // Raw point
}

那因为要存储不同缩放层级的数据,扩展下 Record 的字段

type Tile struct {
	X, Y, Z    uint32
	Key        string
}

type Record struct {
	ID       primitive.ObjectID `bson:"_id"`                      // ObjectID
	Location GeoPoint           `bson:"location" json:"location"` // Raw point
	Levels   []Tile             `bson:"levels" json:"-"`          // Not export to outside in JSON
}

将 zoom 在 [min, max] 内的瓦片索引 x,y,z 一并存储。其中 Key{x}-{y}-{z} 的拼接格式。

当需要查找的时候,用 Mongo 的 Aggregate 功能,就能得到每个 Key 的出现次数:

l := 12
pipes := bson.A{
    bson.M{
        "$match": bson.M{"levels.z": l},  // 过滤出 zoom 等级为需要的数据
    },
    bson.M{
        "$unwind": "$levels",  // unwind
    },
    bson.M{
        "$match": bson.M{"levels.z": l},  // 去掉 unwind 后不想要的 zoom
    },
    bson.M{
        "$group": bson.M{
            "_id":   "$levels.key",
            "count": bson.M{"$sum": 1},  // 统计
        },
    },
}
myCollecion.Aggregate(ctx, pipes)

单个记录样例:

{
  "_id": "2413-3079-13",
  "count": 12
}

然后从 Key 反解出瓦片的中心点坐标,加上统计结果一并给前端,用相关 SDK 在地图上展现即可。

本文代码在 https://github.com/ringsaturn/geo-agg-tile-index-example