GeoFire查询结果未按距离排序问题求助
解决GeoFire返回地标列表未按距离排序的问题
首先说明当前使用的依赖版本:
geofire#4.1.2 bower_components\geofire └── firebase#3.9.0
我之前写了一个根据用户位置返回地标列表的函数,原本能正常按距离排序,但最近做了以下操作后排序功能失效了:
- 添加Swagger
- 调整项目结构以符合OpenAPI规范
- 添加bower
- 重新安装geofire和firebase
我的函数代码如下:
GetAllLandmarksByUserLocation(ref, ref_locations, properties, user_latitude, user_longitude, user_radius) { const landmarkGeoFire = new GeoFire(ref_locations); var geoQuery = landmarkGeoFire.query({ center: [user_latitude, user_longitude], radius: user_radius }); return new Promise(function(resolve,reject) { var locations = []; var onKeyEnteredRegistration = geoQuery.on("key_entered", function (key, coordinates, distance) { var location = {}; location.key = key; location.latitude = coordinates[0]; location.longitude = coordinates[1]; location.distance = distance; locations.push(location); }); var attributes = []; var onReadyRegistration = geoQuery.on("ready", function() { ref.on('value', function (refsSnap) { refsSnap.forEach((refSnap) => { var list = refSnap; locations.forEach(function(locationSnap) { //console.log(refSnap.key, '==', locationSnap.key); // brute force approach, rework this later if (refSnap.key == locationSnap.key) { var attribute = {}; for(var i=0; i<=properties.length-1; i++) { if(properties[i] == 'key') { attribute[properties[i]] = refSnap.key; continue; } attribute[properties[i]] = list.child(properties[i]).val(); } attribute['latitude'] = locationSnap.latitude; attribute['longitude'] = locationSnap.longitude; attribute['distance'] = locationSnap.distance; attributes.push(attribute); } // refSnap.key == locationSnap.key }); // locations.forEach }); // refsSnap.forEach resolve(attributes); }); // ref.on }); // onreadyregistration }); // Promise }
Firebase实时数据库的结构如下:
测试用的输入参数:
- user_latitude = 40.769841
- user_longitude = -73.964306
- user_radius = 225
当前返回的结果未按距离排序,示例如下:
[ { "key": "-L8AURBnFy01EZVekPnR", "building": "753", "street": "York Avenue", "category": 0, "closing": "6 PM", "email": "cornell@somewhere.com", "name": "Weill Cornell", "opening": "8 AM", "phone": "212-555-1212", "postal": 10021, "timestamp": "Wed Mar 21 2018 22:15:55 GMT-0400 (Eastern Daylight Time)", "type": 4, "web": "http://www.weillcornell.com", "latitude": 40.765982, "longitude": -73.956045, "distance": 0.817387865219758 }, { "key": "-L8AUdrIysiTYr0RUDV_", "building": "753", "street": null, "category": 0, "closing": "6 PM", "email": "cornell@somewhere.com", "name": "Weill Cornell", "opening": "8 AM", "phone": "212-555-1212", "postal": 10021, "timestamp": "Wed Mar 21 2018 22:16:51 GMT-0400 (Eastern Daylight Time)", "type": 4, "web": "http://www.weillcornell.com", "latitude": 40.765982, "longitude": -73.956045, "distance": 0.817387865219758 }, { "key": "-L8AUeEayBJKU3y6Hd4e", "building": "753", "street": null, "category": 0, "closing": "6 PM", "email": "cornell@somewhere.com", "name": "Weill Cornell", "opening": "8 AM", "phone": "212-555-1212", "postal": 10021, "timestamp": "Wed Mar 21 2018 22:16:53 GMT-0400 (Eastern Daylight Time)", "type": 4, "web": "http://www.weillcornell.com", "latitude": 40.765982, "longitude": -73.956045, "distance": 0.817387865219758 }, { "key": "-L8AVW50fx3cTwn-DrIQ", "building": "753", "street": "Minetta Lane", "category": 0, "closing": "6 PM", "email": "support@southebys.com", "name": "Southebys", "opening": "8 AM", "phone": "212-555-1212", "postal": 10021, "timestamp": "Wed Mar 21 2018 22:20:38 GMT-0400 (Eastern Daylight Time)", "type": 4, "web": "http://www.southebys.com", "latitude": 40.766372, "longitude": -73.953803, "distance": 0.9649556738877696 }, { "key": "-L8AV_nVSAzMxdAXJb-L", "building": "753", "street": null, "category": 0, "closing": "6 PM", "email": "support@southebys.com", "name": "Southebys", "opening": "8 AM", "phone": "212-555-1212", "postal": 10021, "timestamp": "Wed Mar 21 2018 22:20:57 GMT-0400 (Eastern Daylight Time)", "type": 4, "web": "http://www.southebys.com", "latitude": 40.766372, "longitude": -73.953803, "distance": 0.9649556738877696 }, { "key": "-L8AWqfH3q4dZcrFMc6F", "building": "753", "street": null, "category": 0, "closing": "6 PM", "email": "support@helmsley.com", "name": "helmsley", "opening": "8 AM", "phone": "212-555-1212", "postal": 10021, "timestamp": "Wed Mar 21 2018 22:26:28 GMT-0400 (Eastern Daylight Time)", "type": 4, "web": "http://www.helmsl..."} ]
问题根源
其实GeoFire的key_entered事件触发顺序从来都不保证是按距离排序的,之前能正常排序可能只是巧合或者旧版本的偶然行为。加上后续匹配Firebase数据和地理坐标时,是按遍历顺序直接添加到结果数组,没有做任何排序处理,所以现在出现了排序混乱的情况。
修复方案
我们需要在最终返回结果前,手动对attributes数组按照distance字段做升序排序,同时还可以优化数据匹配逻辑提升性能:
1. 添加排序逻辑
在resolve(attributes);这行代码之前,插入排序代码:
attributes.sort((a, b) => a.distance - b.distance);
2. 优化数据匹配逻辑
之前用双重循环做匹配效率较低,我们可以把locations数组转成以key为键的对象,匹配时直接通过key查找,时间复杂度从O(n²)降到O(n):
// 在onReadyRegistration回调里,先把locations转成映射对象 const locationMap = {}; locations.forEach(location => { locationMap[location.key] = location; }); // 遍历Firebase数据时直接通过key匹配 refsSnap.forEach((refSnap) => { const location = locationMap[refSnap.key]; if (location) { // 保留原本构建attribute的逻辑 var attribute = {}; for(var i=0; i<=properties.length-1; i++) { if(properties[i] == 'key') { attribute[properties[i]] = refSnap.key; continue; } attribute[properties[i]] = refSnap.child(properties[i]).val(); } attribute['latitude'] = location.latitude; attribute['longitude'] = location.longitude; attribute['distance'] = location.distance; attributes.push(attribute); } });
3. 完整修改后的函数
整合以上修改后的完整函数代码:
GetAllLandmarksByUserLocation(ref, ref_locations, properties, user_latitude, user_longitude, user_radius) { const landmarkGeoFire = new GeoFire(ref_locations); var geoQuery = landmarkGeoFire.query({ center: [user_latitude, user_longitude], radius: user_radius }); return new Promise(function(resolve,reject) { var locations = []; var onKeyEnteredRegistration = geoQuery.on("key_entered", function (key, coordinates, distance) { var location = {}; location.key = key; location.latitude = coordinates[0]; location.longitude = coordinates[1]; location.distance = distance; locations.push(location); }); var attributes = []; var onReadyRegistration = geoQuery.on("ready", function() { // 优化:将locations转为key映射的对象,提升匹配效率 const locationMap = {}; locations.forEach(location => { locationMap[location.key] = location; }); ref.on('value', function (refsSnap) { refsSnap.forEach((refSnap) => { const location = locationMap[refSnap.key]; if (location) { var attribute = {}; for(var i=0; i<=properties.length-1; i++) { if(properties[i] == 'key') { attribute[properties[i]] = refSnap.key; continue; } attribute[properties[i]] = refSnap.child(properties[i]).val(); } attribute['latitude'] = location.latitude; attribute['longitude'] = location.longitude; attribute['distance'] = location.distance; attributes.push(attribute); } }); // 对结果按距离升序排序 attributes.sort((a, b) => a.distance - b.distance); resolve(attributes); // 取消监听,避免内存泄漏 onKeyEnteredRegistration.cancel(); }); }); }); }
额外提醒
- 记得在
ready事件处理完成后取消key_entered的监听,避免内存泄漏(上面的代码已经添加了onKeyEnteredRegistration.cancel();) - 如果地标数据量很大,建议考虑在Firebase端做数据过滤,减少客户端需要处理的数据量,提升整体性能
内容的提问来源于stack exchange,提问作者Peter
相关产品推荐
相关产品推荐

