SwiftUI中Firestore GeoQuery匹配GeoHash却返回空文档问题
排查Firestore GeoHash区域查询无结果问题
问题背景
将场地(Venue)数据保存到Firebase Firestore时,已通过以下代码生成并存储GeoHash(示例值:gcpvhfh0cv),且确认存储正常:
static func generateGeoHash(latitude: Double, longitude: Double) -> String { let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) return GFUtils.geoHash(forLocation: location) }
参考官方文档及相关方案实现queryVenuesForRegion函数,使用DispatchGroup处理异步查询:
func queryVenuesForRegion(userRegion: MKCoordinateRegion, completion: @escaping ([Venue]) -> Void) { //Get a reference to the database let db = Firestore.firestore() let centerCoordinate = userRegion.center let radiusInMeters: Double = 50 * 1000 //max(userRegion.span.latitudeDelta, userRegion.span.longitudeDelta) * 5000 // Approximate conversion, adjust as needed // Calculate the query bounds using GeoFire utility function let queryBounds = GFUtils.queryBounds(forLocation: centerCoordinate, withRadius: radiusInMeters) // Create a query to retrieve venues within the region let queries = queryBounds.map { bound -> Query in return db.collection("venues") .order(by: "geoHash") .start(at: [bound.startValue]) .end(at: [bound.endValue]) } // Create a dispatch group outside of the query loop since each iteration of the loop // performs an asynchronous task. let dispatch = DispatchGroup() var matchingDocs = [QueryDocumentSnapshot]() // Collect all the query results together in a sinlge list func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) { guard let documents = snapshot?.documents else { print("Unable to fetch snapshot data. \(String(describing: error))") completion([]) dispatch.leave() // leave the dispatch group when we exit this completion return } for document in documents { let lat = document.data()["latitute"] as? Double ?? 0 let lng = document.data()["longitude"] as? Double ?? 0 let coordinates = CLLocation(latitude: lat, longitude: lng) let centerPoint = CLLocation(latitude: centerCoordinate.latitude, longitude: centerCoordinate.longitude) // We have to filter out a few false positives due to GeoHash accuracy, but // most will match let distance = GFUtils.distance(from: centerPoint, to: coordinates) if distance <= radiusInMeters { matchingDocs.append(document) } } dispatch.leave() // leave the dispatch group when we exit this completion } // After all callbacks have executed, matchingDocs contains the result. Note that this // sample does not demonstrate how to wait on all callbacks to complete. for query in queries { dispatch.enter() // enter the dispatch group on each iteration query.getDocuments(completion: getDocumentsCompletion) } // [END fs_geo_query_hashes] var venues = [Venue]() dispatch.notify(queue: .main) { for document in matchingDocs { if let venue = try? document.data(as: Venue.self) { venues.append(venue) } } } // Call the completion handler with the parsed venues completion(venues) }
调用该函数时,已确认查询区域(如伦敦市中心)与场地坐标匹配,queryBounds和queries均有数据,但documents和matchingDocs始终为空;直接获取所有文档可成功并正常显示地图标注。
排查思路
- 检查字段名一致性:确认Firestore文档中GeoHash的存储字段名是
geoHash(注意大小写);同时注意代码中获取纬度的字段拼写错误——代码里写的是latitute,正确应为latitude,这会导致获取的纬度值默认是0,进而距离计算错误过滤掉所有结果。 - 验证GeoHash边界范围:手动计算目标区域中心坐标的GeoHash范围,对比
queryBounds返回的startValue和endValue是否包含已存储的示例GeoHash(gcpvhfh0cv),若范围不匹配,说明GFUtils.queryBounds的计算逻辑存在问题。 - 确认Firestore索引配置:Firestore对
order(by: "geoHash")结合start(at:)/end(at:)的查询需要创建复合索引。进入Firestore控制台,查看是否有未创建的索引提示(查询失败时控制台会给出创建链接),确保索引已正确配置。 - 调试查询原始结果:在
getDocumentsCompletion中,先打印所有返回文档的geoHash值,确认是否有文档被查询到但被距离过滤掉。若有文档返回,重点检查距离计算逻辑是否正确。 - 修复DispatchGroup执行顺序:当前代码中
completion(venues)在dispatch.notify之前执行,此时venues还是空数组。需要将completion(venues)移到dispatch.notify的闭包内部,确保所有查询完成后再返回结果:dispatch.notify(queue: .main) { var venues = [Venue]() for document in matchingDocs { if let venue = try? document.data(as: Venue.self) { venues.append(venue) } } completion(venues) // 移到此处 } // 移除外部的completion(venues)调用 - 测试单个查询:从
queries中取出单个Query单独执行getDocuments,查看是否有结果,缩小问题范围是某一个查询无结果还是所有查询都无结果。 - 验证距离计算逻辑:用已知的坐标对(比如伦敦市中心坐标和场地坐标)手动计算实际距离,对比
GFUtils.distance(from:to:)的返回值是否正确,若计算错误会导致合法文档被错误过滤。
内容的提问来源于stack exchange,提问作者Phil
相关产品推荐
相关产品推荐

