基于MongoDB数据库,实现一个后端分页查询接口,输出指定坐标点附近的文档,并计算距离并返回。类似地图app基于自己的位置查询附近的位置,并按距离从进到原排序。
要最高效的实现这个功能,需要利用MongoDB自带的2dsphere
地理空间索引。
使用GeoJson对象存储坐标字段
使用该索引时,在数据的存储时,经纬度字段储存示例如下,需要在该字段上创建2dsphere索引,并且字段应为GeoJson对象,例如点对象字段的标识为:
location: {
type: "Point",
coordinates: [-73.856077, 40.848447]
}
关于线、多边行类型的存储可参考官方文档:https://docs.mongodb.com/manual/reference/geojson/
使用$geoNear聚合阶段查询排序附近的文档并返回计算距离
最终,要使用地理空间索引进行附近的文档查询并且返回距离字段,需要使用MongoDB的聚合aggregate查询,使用$geoNear作为第一阶段聚合所需文档。
使用如下:
db.places.aggregate([
{
$geoNear: {
near: { type: "Point", coordinates: [ -73.99279, 40.719296]},
distanceField: "dist.calculated",
maxDistance: 2,
query: { category: "Parks" },
includeLocs: "dist.location",
spherical: true
}
}
])
其中个字段含义:
near: 查找最近坐标的点,可理解为地图app中‘我的位置’;
distanceField: 指定计算后的距离字段名;
maxDistance: 最大距离限制;
query: 对查询文档的限制,类似$match阶段,使用mongodb的查询语句即可;
includeLocs: 指定返回当前文档的经纬度数据的字段名,
spherical: 当为True时将使用球形几何计算距离,不过2dsphere索引默认都使用球形几何。
更详细的使用方法可参考官方文档:https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#pipe._S_geoNear
项目代码示例
1. 插入文档示例
先在数据库插入几条模拟数据如下:
[
{
"_id": {
"$oid": "5f43b73e5a25bb14ec753a23"
},
"area_name": "测试区域1",
"company_name": "科技有限公司",
"geo_point": {
"type": "Point",
"coordinates": [106.704063, 29.867705]
}
},
{
"_id": {
"$oid": "5f43b815c515d315de2ae744"
},
"area_name": "测试区域1",
"company_name": "农业责任公司",
"geo_point": {
"type": "Point",
"coordinates": [106.704064, 29.867708]
}
},
{
"_id": {
"$oid": "5f43b824c515d315de2ae745"
},
"area_name": "测试区域2",
"company_name": "科技贸易有限公司",
"geo_point": {
"type": "Point",
"coordinates": [106.704055, 29.867711]
}
},
{
"_id": {
"$oid": "5f43b834c515d315de2ae746"
},
"area_name": "测试区域2",
"company_name": "金融有限公司",
"geo_point": {
"type": "Point",
"coordinates": [106.704052, 29.867718]
}
}
]
2. 索引创建
除地理坐标索引外,由于后端接口需要对多个字段进行筛选,这里模拟多字段的索引创建。
因该处仅做模拟,mongodb的连接使用了最简单的方式进行,只是方便操作。
可以手动或者使用代码创建索引,我这里使用了代码创建,代码如下:
import pymongo
from pymongo import MongoClient, IndexModel
uri = "mongodb://%s:%s@%s" % ("admin", "admin", "localhost:27017")
client = MongoClient(uri, connect=False)
db = client["test"]
coll = db["company_detail"]
def create_all_indexes():
indexes = [
IndexModel([("geo_point", "2dsphere")], background=True),
IndexModel([("area_name", pymongo.DESCENDING)], background=True),
IndexModel([("company_name", pymongo.DESCENDING)], unique=True, background=True)
]
create_result = coll.create_indexes(indexes)
return create_result
3. MongoDB附近公司查询
模拟mongodb的地理坐标查询及分页方法,具体实现参照代码吧,看代码比看文字好明白:
def company_query(current_geo, area_name=None, name_kw=None, page_no=1, page_size=10):
"""
聚合查询符合条件的附近公司
:param current_geo: list:用户当前经纬度数组,0:经度,1:纬度,example:["106.704063", "29.867705"]
:param area_name: 公司所属的区域名
:param name_kw:公司名模糊匹配关键字(长度大于1)
:param page_no: 页码
:param page_size: 页大小
:return:tuple: (匹配文档总数,分页后附近文档列表)
"""
# 匹配条件,默认为匹配所有
match = {}
if area_name:
match.update({"area_name": area_name})
if name_kw:
addr_query = {"company_name": {"$regex": name_kw}}
match.update(addr_query)
# 地理空间距离查询、距离值返回按距离升序排序
near_match = {
"$geoNear": {
"near": {"type": "Point", "coordinates": current_geo},
"distanceField": "distance",
"includeLocs": "location",
"query": match,
"spherical": True
}
}
# 过滤字段
project = {
"$project": {
"_id": 0,
"geo_point": 0
}
}
# 分页处理
skip = {"$skip": (int(page_no) - 1) * int(page_size)}
limit = {"$limit": int(page_size)}
# 聚合管道1: 匹配的总文档数量
total_pipeline = [{"$match": match}, {"$count": "total"}]
# 聚合管道2:匹配且分页后的附近公司文档(包含距离及坐标)
near_pipeline = [near_match, project, skip, limit]
# 先后执行两个聚合管道
total_match_result = list(coll.aggregate(total_pipeline))
if total_match_result:
total_match_count = total_match_result[0].get("total")
page_result = list(coll.aggregate(near_pipeline))
return total_match_count, page_result
由于需要获取分页前总文档的数量,这里使用了两次聚合,一次聚合只获取匹配结果集总数量,一次聚合才是分页后的附近企业文档列表。
4. 测试输出
在后面对查询函数做输出测试:
if __name__ == '__main__':
# 创建索引
create_all_indexes()
current_geo = [106.704223, 29.867201]
area_name = "测试区域1"
name_kw = None
total_count, page_res = company_query(current_geo, area_name, name_kw)
print(total_count, page_res)
输出如下:
匹配的结果总数是:4
附近企业分页结果集是:[
{'area_name': '测试区域1', 'company_name': '科技有限公司', 'distance': 58.19188972091601, 'location': {'type': 'Point', 'coordinates': [106.704063, 29.867705]}},
{'area_name': '测试区域1', 'company_name': '农业责任公司', 'distance': 58.48852829885492, 'location': {'type': 'Point', 'coordinates': [106.704064, 29.867708]}}
]
其中distance则是指定的距离计算结果字段,单位为米,该聚合已经自动按照距离从近到远对结果集进行了排序,我在集合数据量较大的情况下进行了测试,聚合速度依然非常快,在做好索引优化的情况下接口查询速度保持在100ms左右,满足正常使用。
总结
MongoDB的地理坐标索引的使用、查询、聚合知识点挺多,若有其他相关需求,建议查看官方文档,内容详细,可以较快且准确的解决问题。
且在使用的时候需要注意MongoDB的版本。