如何使用firestore运行地理"附近"查询?

Mon*_*key 53 firebase google-cloud-firestore

来自firebase的新firestore数据库本身是否支持基于位置的地理查询?即查找10英里内的帖子,或找到最近的50个帖子?

我看到实时firebase数据库有一些现有的项目,像geofire这样的项目 - 那些也可以适应firestore吗?

Rya*_*Lee 39

更新:Firestore目前不支持实际的GeoPoint查询,因此当下面的查询成功执行时,它只按纬度而不是经度进行过滤,因此会返回许多不在附近的结果.最好的解决方案是使用geohashes.要了解如何自己做类似的事情,请看一下这个视频.

这可以通过创建小于大于查询的边界框来完成.至于效率,我不能说话.

注意,应该检查~1英里的纬度/经度偏移的精度,但是这里有一个快速的方法:

SWIFT 3.0版

func getDocumentNearBy(latitude: Double, longitude: Double, distance: Double) {

    // ~1 mile of lat and lon in degrees
    let lat = 0.0144927536231884
    let lon = 0.0181818181818182

    let lowerLat = latitude - (lat * distance)
    let lowerLon = longitude - (lon * distance)

    let greaterLat = latitude + (lat * distance)
    let greaterLon = longitude + (lon * distance)

    let lesserGeopoint = GeoPoint(latitude: lowerLat, longitude: lowerLon)
    let greaterGeopoint = GeoPoint(latitude: greaterLat, longitude: greaterLon)

    let docRef = Firestore.firestore().collection("locations")
    let query = docRef.whereField("location", isGreaterThan: lesserGeopoint).whereField("location", isLessThan: greaterGeopoint)

    query.getDocuments { snapshot, error in
        if let error = error {
            print("Error getting documents: \(error)")
        } else {
            for document in snapshot!.documents {
                print("\(document.documentID) => \(document.data())")
            }
        }
    }

}

func run() {
    // Get all locations within 10 miles of Google Headquarters
    getDocumentNearBy(latitude: 37.422000, longitude: -122.084057, distance: 10)
}
Run Code Online (Sandbox Code Playgroud)

  • 我们正在运行与您相同的查询,但我们得到的结果是忽略了经度.所以我们得到的所有文件都在纬度范围内,但不在lon范围内. (10认同)
  • 谢谢你的回答!正如其他人提到的那样,但有一个很大的警告.你愿意将这样的东西添加到顶部吗?更新:Firestore目前不支持实际的GeoPoint查询,因此当下面的查询成功执行时,它只按纬度而不是经度进行过滤,因此会返回许多不在附近的结果.最好的解决方案是使用[geohashes](https://en.wikipedia.org/wiki/Geohash).要了解如何自己做类似的事情,请看一下[视频](https://www.youtube.com/watch?v=mx1mMdHBi5Q). (4认同)
  • @zirinisp对于经度问题,我在集合中设置了一个额外的字段,其中是虚拟经度图块。我只执行Math.floor(longitude),然后在查询时可以执行.where(“ longitude_tile”,“ ==”,Math.floor(yourCurrentLongitude))。由于这是一个相等的运算符,因此可以将其与位置范围一起使用,以在服务器而非客户端上极大地过滤结果。每个经度约为69英里,足以满足我们的目的。 (2认同)
  • 如果实际上(不是)解决问题,为什么将此答案标记为“正确”? (2认同)

stp*_*ham 22

更新:Firestore目前不支持实际的GeoPoint查询,因此当下面的查询成功执行时,它只按纬度而不是经度进行过滤,因此会返回许多不在附近的结果.最好的解决方案是使用geohashes.要了解如何自己做类似的事情,请看一下这个视频.

(首先让我为这篇文章中的所有代码道歉,我只是希望任何阅读此答案的人都可以轻松地重现功能.)

为了解决OP所面临的同样问题,我首先调整了GeoFire库以与Firestore一起工作(通过查看该库可以了解很多关于地理信息的内容).然后我意识到我并不介意如果位置以精确的圆圈返回.我只是想通过某种方式获得"附近"的位置.

我无法相信我花了多长时间才意识到这一点,但您可以使用SW角和NE角在GeoPoint场上执行双重不等式查询,以获取围绕中心点的边界框内的位置.

所以我创建了一个类似下面的JavaScript函数(这基本上是Ryan Lee的答案的JS版本).

/**
 * Get locations within a bounding box defined by a center point and distance from from the center point to the side of the box;
 *
 * @param {Object} area an object that represents the bounding box
 *    around a point in which locations should be retrieved
 * @param {Object} area.center an object containing the latitude and
 *    longitude of the center point of the bounding box
 * @param {number} area.center.latitude the latitude of the center point
 * @param {number} area.center.longitude the longitude of the center point
 * @param {number} area.radius (in kilometers) the radius of a circle
 *    that is inscribed in the bounding box;
 *    This could also be described as half of the bounding box's side length.
 * @return {Promise} a Promise that fulfills with an array of all the
 *    retrieved locations
 */
function getLocations(area) {
  // calculate the SW and NE corners of the bounding box to query for
  const box = utils.boundingBoxCoordinates(area.center, area.radius);

  // construct the GeoPoints
  const lesserGeopoint = new GeoPoint(box.swCorner.latitude, box.swCorner.longitude);
  const greaterGeopoint = new GeoPoint(box.neCorner.latitude, box.neCorner.longitude);

  // construct the Firestore query
  let query = firebase.firestore().collection('myCollection').where('location', '>', lesserGeopoint).where('location', '<', greaterGeopoint);

  // return a Promise that fulfills with the locations
  return query.get()
    .then((snapshot) => {
      const allLocs = []; // used to hold all the loc data
      snapshot.forEach((loc) => {
        // get the data
        const data = loc.data();
        // calculate a distance from the center
        data.distanceFromCenter = utils.distance(area.center, data.location);
        // add to the array
        allLocs.push(data);
      });
      return allLocs;
    })
    .catch((err) => {
      return new Error('Error while retrieving events');
    });
}
Run Code Online (Sandbox Code Playgroud)

上面的函数还为返回的每个位置数据添加.distanceFromCenter属性,这样您只需检查该距离是否在您想要的范围内,就可以获得类似圆的行为.

我在上面的函数中使用了两个util函数,所以这里也是代码.(以下所有的util函数实际上都是从GeoFire库中改编而来的.)

距离():

/**
 * Calculates the distance, in kilometers, between two locations, via the
 * Haversine formula. Note that this is approximate due to the fact that
 * the Earth's radius varies between 6356.752 km and 6378.137 km.
 *
 * @param {Object} location1 The first location given as .latitude and .longitude
 * @param {Object} location2 The second location given as .latitude and .longitude
 * @return {number} The distance, in kilometers, between the inputted locations.
 */
distance(location1, location2) {
  const radius = 6371; // Earth's radius in kilometers
  const latDelta = degreesToRadians(location2.latitude - location1.latitude);
  const lonDelta = degreesToRadians(location2.longitude - location1.longitude);

  const a = (Math.sin(latDelta / 2) * Math.sin(latDelta / 2)) +
          (Math.cos(degreesToRadians(location1.latitude)) * Math.cos(degreesToRadians(location2.latitude)) *
          Math.sin(lonDelta / 2) * Math.sin(lonDelta / 2));

  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return radius * c;
}
Run Code Online (Sandbox Code Playgroud)

boundingBoxCoordinates():(此处还有更多的utils,我在下面粘贴过.)

/**
 * Calculates the SW and NE corners of a bounding box around a center point for a given radius;
 *
 * @param {Object} center The center given as .latitude and .longitude
 * @param {number} radius The radius of the box (in kilometers)
 * @return {Object} The SW and NE corners given as .swCorner and .neCorner
 */
boundingBoxCoordinates(center, radius) {
  const KM_PER_DEGREE_LATITUDE = 110.574;
  const latDegrees = radius / KM_PER_DEGREE_LATITUDE;
  const latitudeNorth = Math.min(90, center.latitude + latDegrees);
  const latitudeSouth = Math.max(-90, center.latitude - latDegrees);
  // calculate longitude based on current latitude
  const longDegsNorth = metersToLongitudeDegrees(radius, latitudeNorth);
  const longDegsSouth = metersToLongitudeDegrees(radius, latitudeSouth);
  const longDegs = Math.max(longDegsNorth, longDegsSouth);
  return {
    swCorner: { // bottom-left (SW corner)
      latitude: latitudeSouth,
      longitude: wrapLongitude(center.longitude - longDegs),
    },
    neCorner: { // top-right (NE corner)
      latitude: latitudeNorth,
      longitude: wrapLongitude(center.longitude + longDegs),
    },
  };
}
Run Code Online (Sandbox Code Playgroud)

metersToLongitudeDegrees():

/**
 * Calculates the number of degrees a given distance is at a given latitude.
 *
 * @param {number} distance The distance to convert.
 * @param {number} latitude The latitude at which to calculate.
 * @return {number} The number of degrees the distance corresponds to.
 */
function metersToLongitudeDegrees(distance, latitude) {
  const EARTH_EQ_RADIUS = 6378137.0;
  // this is a super, fancy magic number that the GeoFire lib can explain (maybe)
  const E2 = 0.00669447819799;
  const EPSILON = 1e-12;
  const radians = degreesToRadians(latitude);
  const num = Math.cos(radians) * EARTH_EQ_RADIUS * Math.PI / 180;
  const denom = 1 / Math.sqrt(1 - E2 * Math.sin(radians) * Math.sin(radians));
  const deltaDeg = num * denom;
  if (deltaDeg < EPSILON) {
    return distance > 0 ? 360 : 0;
  }
  // else
  return Math.min(360, distance / deltaDeg);
}
Run Code Online (Sandbox Code Playgroud)

wrapLongitude():

/**
 * Wraps the longitude to [-180,180].
 *
 * @param {number} longitude The longitude to wrap.
 * @return {number} longitude The resulting longitude.
 */
function wrapLongitude(longitude) {
  if (longitude <= 180 && longitude >= -180) {
    return longitude;
  }
  const adjusted = longitude + 180;
  if (adjusted > 0) {
    return (adjusted % 360) - 180;
  }
  // else
  return 180 - (-adjusted % 360);
}
Run Code Online (Sandbox Code Playgroud)

  • `function degreesToRadians(degrees){return(degrees*Math.PI)/ 180;}` (3认同)
  • @ b-fg那是真的.但至少在我的应用程序中,从数据库中检索附近的位置后,我的结果集通常很小,因此客户端过滤/排序是实用的.我知道这不是一个完美的解决方案,但在Firestore直接支持地理查询之前,我怀疑会有一个完美的解决方案. (2认同)

小智 11

截至今天,没有办法做这样的查询.SO中还有其他与之相关的问题:

有没有办法将GeoFire与Firestore一起使用?

如何在Firebase Cloud Firestore中查询集合中最近的GeoPoints?

有没有办法将GeoFire与Firestore一起使用?

在我目前的Android项目中,我可以使用https://github.com/drfonfon/android-geohash添加geohash字段,而Firebase团队正在开发本机支持.

使用其他问题中建议的Firebase实时数据库意味着您无法同时按位置和其他字段过滤结果集,这是我想要首先切换到Firestore的主要原因.


ra9*_*a9r 8

自@monkeybonkey首先提出这个问题以来,已经引入了一个新项目.该项目名为GEOFirestore.

使用此库,您可以执行查询,例如圆圈内的查询文档:

  const geoQuery = geoFirestore.query({
    center: new firebase.firestore.GeoPoint(10.38, 2.41),
    radius: 10.5
  });
Run Code Online (Sandbox Code Playgroud)

您可以通过npm安装GeoFirestore.您必须单独安装Firebase(因为它是GeoFirestore的对等依赖项):

$ npm install geofirestore firebase --save
Run Code Online (Sandbox Code Playgroud)

  • @nikhilSridhar尤其有用!您想让这类查询远离最终用户,仅在Cloud Functions中执行它们!1.当您希望移动设备尽可能少地查询时,查询本身可能会触发多个请求。2.将此类数据直接暴露给移动应用程序可能会成为严重的安全威胁!假设您存储了所有用户的位置...如果您赋予应用程序运行这种查询的权力,那么您也将这种权力授予希望监视所有用户位置的任何开发人员。 (3认同)
  • 这实际上没用,因为它只适用于 Javascript (2认同)
  • @NikhilSridhar JavaScript 是地球上最常用的语言,怎么可能没用呢?(当然是打字稿)。如果您正在编写任何其他语言(服务器端或客户端),我认为您是在浪费时间。任何人共享客户端/服务器代码?React-native 等 (2认同)

小智 6

劫持此线程希望能帮助任何仍在寻找的人。Firestore 仍然不支持基于地理的查询,并且使用 GeoFirestore 库也不理想,因为它只能让您按位置搜索,没有别的。

我把它放在一起:https : //github.com/mbramwell1/GeoFire-Android

它基本上允许您使用位置和距离进行附近搜索:

QueryLocation queryLocation = QueryLocation.fromDegrees(latitude, longitude);
Distance searchDistance = new Distance(1.0, DistanceUnit.KILOMETERS);
geoFire.query()
    .whereNearTo(queryLocation, distance)
    .build()
    .get();
Run Code Online (Sandbox Code Playgroud)

repo 上有更多文档。它对我有用,所以试一试,希望它能满足你的需求。


Fra*_*len 5

截至 2020 年底,现在还有关于如何使用 Firestore 进行地理查询的文档。

这些适用于 iOS、Android 和 Web 的解决方案构建在 Firebase 创建的 GeoFire 库的精简版之上,然后展示了如何:

  • 生成 geohash 值并将它们存储在 Firestore 中
  • 确定某个点和半径的边界框的 geohash 范围
  • 在这些 geohash 范围内执行查询

这比此处介绍的大多数其他库要低级一些,因此它可能更适合某些用例,而不太适合其他用例。