地理位置查询

许多应用都有按实际地理位置编入索引的文档。例如,您的应用可能允许用户浏览他们当前所在位置附近的商店。

解决方案:Geohash

Geohash 是用于将 (latitude, longitude) 对编码为单个 Base32 字符串的体系。在 Geohash 体系中,世界被划分为一个矩形网格。Geohash 字符串的每个字符都指定了前缀哈希值 32 个细分中的其中一个。例如,Geohash abcd 是完全涵盖在更大的 Geohash abc 范围之内的 32 个四字符哈希值的其中之一。

两个哈希值之间的共同前缀越长,彼此就越近。例如,abcdefabcdegabcdff 更近。但是,反之却不成立!两个地理区域可能会非常近,却拥有截然不同的 Geohash:

较远的 Geohash

我们可以使用 Geohash 在 Firestore 中的位置存储和查询文档,同时获得合理的效率,但只需要一个已编入索引的字段。

安装帮助程序库

创建和解析 Geohash 涉及一些复杂的数学概念,因此我们构建了帮助程序库,以抽象化 Android、Apple 和 Web 上最棘手的部分:

Web 版本 9

// Install from NPM. If you prefer to use a static .js file visit
// https://github.com/firebase/geofire-js/releases and download
// geofire-common.min.js from the latest version
npm install --save geofire-common

Web 版本 8

// Install from NPM. If you prefer to use a static .js file visit
// https://github.com/firebase/geofire-js/releases and download
// geofire-common.min.js from the latest version
npm install --save geofire-common

Swift

注意:此产品不适用于 watchOS 和 App Clip 目标。
// 将其添加到您的 Podfile pod“GeoFire/Utils”

Kotlin
Android

// Add this to your app/build.gradle
implementation 'com.firebase:geofire-android-common:3.2.0'

Java
Android

// Add this to your app/build.gradle
implementation 'com.firebase:geofire-android-common:3.1.0'

存储 Geohash

对于您要按地理位置编制索引的每个文档,您需要存储一个 Geohash 字段:

Web 版本 9

import { doc, updateDoc } from 'firebase/firestore';

// Compute the GeoHash for a lat/lng point
const lat = 51.5074;
const lng = 0.1278;
const hash = geofire.geohashForLocation([lat, lng]);

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
const londonRef = doc(db, 'cities', 'LON');
await updateDoc(londonRef, {
  geohash: hash,
  lat: lat,
  lng: lng
});

Web 版本 8

// Compute the GeoHash for a lat/lng point
const lat = 51.5074;
const lng = 0.1278;
const hash = geofire.geohashForLocation([lat, lng]);

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
const londonRef = db.collection('cities').doc('LON');
londonRef.update({
  geohash: hash,
  lat: lat,
  lng: lng
}).then(() => {
  // ...
});

Swift

注意:此产品不适用于 watchOS 和 App Clip 目标。
// Compute the GeoHash for a lat/lng point
let latitude = 51.5074
let longitude = 0.12780
let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)

let hash = GFUtils.geoHash(forLocation: location)

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
let documentData: [String: Any] = [
  "geohash": hash,
  "lat": latitude,
  "lng": longitude
]

let londonRef = db.collection("cities").document("LON")
londonRef.updateData(documentData) { error in
  // ...
}

Kotlin
Android

// Compute the GeoHash for a lat/lng point
val lat = 51.5074
val lng = 0.1278
val hash = GeoFireUtils.getGeoHashForLocation(GeoLocation(lat, lng))

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
val updates: MutableMap<String, Any> = mutableMapOf(
    "geohash" to hash,
    "lat" to lat,
    "lng" to lng,
)
val londonRef = db.collection("cities").document("LON")
londonRef.update(updates)
    .addOnCompleteListener {
        // ...
    }

Java
Android

// Compute the GeoHash for a lat/lng point
double lat = 51.5074;
double lng = 0.1278;
String hash = GeoFireUtils.getGeoHashForLocation(new GeoLocation(lat, lng));

// Add the hash and the lat/lng to the document. We will use the hash
// for queries and the lat/lng for distance comparisons.
Map<String, Object> updates = new HashMap<>();
updates.put("geohash", hash);
updates.put("lat", lat);
updates.put("lng", lng);

DocumentReference londonRef = db.collection("cities").document("LON");
londonRef.update(updates)
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // ...
            }
        });

查询 Geohash

Geohash 可让我们针对 Geohash 字段加入一组查询来进行大致的区域查询,然后再过滤掉一些假正例:

Web 版本 9

import { collection, query, orderBy, startAt, endAt, getDocs } from 'firebase/firestore';

// Find cities within 50km of London
const center = [51.5074, 0.1278];
const radiusInM = 50 * 1000;

// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
// @ts-ignore
const bounds = geofire.geohashQueryBounds(center, radiusInM);
const promises = [];
for (const b of bounds) {
  const q = query(
    collection(db, 'cities'), 
    orderBy