Catch up on everything announced at Firebase Summit, and learn how Firebase can help you accelerate app development and run your app with confidence. Learn More

Truy vấn địa lý

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

Nhiều ứng dụng có tài liệu được lập chỉ mục theo vị trí thực tế. Ví dụ: ứng dụng của bạn có thể cho phép người dùng duyệt qua các cửa hàng gần vị trí hiện tại của họ.

Cloud Firestore chỉ cho phép một mệnh đề phạm vi duy nhất cho mỗi truy vấn ghép , có nghĩa là chúng tôi không thể thực hiện truy vấn địa lý bằng cách chỉ lưu trữ vĩ độ và kinh độ dưới dạng các trường riêng biệt và truy vấn hộp giới hạn.

Giải pháp: Geohashes

Geohash là một hệ thống để mã hóa một cặp (latitude, longitude) thành một chuỗi Base32 duy nhất. Trong hệ thống Geohash, thế giới được chia thành một lưới hình chữ nhật. Mỗi ký tự của chuỗi Geohash chỉ định một trong 32 phần nhỏ của mã băm tiền tố. Ví dụ: Geohash abcd là một trong 32 hàm băm bốn ký tự được chứa đầy đủ trong Geohash abc lớn hơn.

Tiền tố được chia sẻ giữa hai băm càng dài thì chúng càng gần nhau. Ví dụ abcdef gần với abcdeg hơn abcdff . Tuy nhiên điều ngược lại là không đúng! Hai khu vực có thể rất gần nhau trong khi có các Geohashes rất khác nhau:

Geohashes xa nhau

Chúng tôi có thể sử dụng Geohashes để lưu trữ và truy vấn tài liệu theo vị trí trong Cloud Firestore với hiệu quả hợp lý trong khi chỉ yêu cầu một trường được lập chỉ mục duy nhất.

Cài đặt thư viện trợ giúp

Tạo và phân tích cú pháp Geohashes liên quan đến một số phép toán phức tạp, vì vậy chúng tôi đã tạo các thư viện trợ giúp để tóm tắt những phần khó nhất trên Android, Apple và Web:

Web

// 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

Nhanh

Lưu ý: Sản phẩm này không khả dụng trên các mục tiêu watchOS và App Clip.
// Thêm cái này vào nhóm Podfile 'GeoFire / Utils' của bạn

Java

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

Cửa hàng Geohashes

Đối với mỗi tài liệu bạn muốn lập chỉ mục theo vị trí, bạn sẽ cần lưu trữ trường Geohash:

Web

// 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(() => {
  // ...
});

Nhanh

Lưu ý: Sản phẩm này không khả dụng trên các mục tiêu watchOS và 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
    // ...
}

Java

// 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) {
                // ...
            }
        });

Truy vấn Geohashes

Geohashes cho phép chúng tôi ước tính các truy vấn khu vực bằng cách kết hợp một tập hợp các truy vấn trên trường Geohash và sau đó lọc ra một số kết quả xác thực sai:

Web

// 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.
const bounds = geofire.geohashQueryBounds(center, radiusInM);
const promises = [];
for (const b of bounds) {
  const q = db.collection('cities')
    .orderBy('geohash')
    .startAt(b[0])
    .endAt(b[1]);

  promises.push(q.get());
}

// Collect all the query results together into a single list
Promise.all(promises).then((snapshots) => {
  const matchingDocs = [];

  for (const snap of snapshots) {
    for (const doc of snap.docs) {
      const lat = doc.get('lat');
      const lng = doc.get('lng');

      // We have to filter out a few false positives due to GeoHash
      // accuracy, but most will match
      const distanceInKm = geofire.distanceBetween([lat, lng], center);
      const distanceInM = distanceInKm * 1000;
      if (distanceInM <= radiusInM) {
        matchingDocs.push(doc);
      }
    }
  }

  return matchingDocs;
}).then((matchingDocs) => {
  // Process the matching documents
  // ...
});

Nhanh

Lưu ý: Sản phẩm này không khả dụng trên các mục tiêu watchOS và App Clip.
// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInM: Double = 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.
let queryBounds = GFUtils.queryBounds(forLocation: center,
                                      withRadius: radiusInM)
let queries = queryBounds.map { bound -> Query in
    return db.collection("cities")
        .order(by: "geohash")
        .start(at: [bound.startValue])
        .end(at: [bound.endValue])
}

var matchingDocs = [QueryDocumentSnapshot]()
// Collect all the query results together into a single list
func getDocumentsCompletion(snapshot: QuerySnapshot?, error: Error?) -> () {
    guard let documents = snapshot?.documents else {
        print("Unable to fetch snapshot data. \(String(describing: error))")
        return
    }

    for document in documents {
        let lat = document.data()["lat"] as? Double ?? 0
        let lng = document.data()["lng"] as? Double ?? 0
        let coordinates = CLLocation(latitude: lat, longitude: lng)
        let centerPoint = CLLocation(latitude: center.latitude, longitude: center.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 <= radiusInM {
            matchingDocs.append(document)
        }
    }
}

// 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 {
    query.getDocuments(completion: getDocumentsCompletion)
}

Java

// Find cities within 50km of London
final GeoLocation center = new GeoLocation(51.5074, 0.1278);
final double 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.
List<GeoQueryBounds> bounds = GeoFireUtils.getGeoHashQueryBounds(center, radiusInM);
final List<Task<QuerySnapshot>> tasks = new ArrayList<>();
for (GeoQueryBounds b : bounds) {
    Query q = db.collection("cities")
            .orderBy("geohash")
            .startAt(b.startHash)
            .endAt(b.endHash);

    tasks.add(q.get());
}

// Collect all the query results together into a single list
Tasks.whenAllComplete(tasks)
        .addOnCompleteListener(new OnCompleteListener<List<Task<?>>>() {
            @Override
            public void onComplete(@NonNull Task<List<Task<?>>> t) {
                List<DocumentSnapshot> matchingDocs = new ArrayList<>();

                for (Task<QuerySnapshot> task : tasks) {
                    QuerySnapshot snap = task.getResult();
                    for (DocumentSnapshot doc : snap.getDocuments()) {
                        double lat = doc.getDouble("lat");
                        double lng = doc.getDouble("lng");

                        // We have to filter out a few false positives due to GeoHash
                        // accuracy, but most will match
                        GeoLocation docLocation = new GeoLocation(lat, lng);
                        double distanceInM = GeoFireUtils.getDistanceBetween(docLocation, center);
                        if (distanceInM <= radiusInM) {
                            matchingDocs.add(doc);
                        }
                    }
                }

                // matchingDocs contains the results
                // ...
            }
        });

Hạn chế

Sử dụng Geohashes để truy vấn vị trí mang lại cho chúng tôi những khả năng mới, nhưng đi kèm với một số hạn chế riêng của nó:

  • Kết quả khẳng định sai - truy vấn của Geohash không chính xác và bạn phải lọc ra các kết quả dương tính giả ở phía khách hàng. Những lần đọc bổ sung này thêm chi phí và độ trễ cho ứng dụng của bạn.
  • Trường hợp cạnh - phương pháp truy vấn này dựa vào việc ước tính khoảng cách giữa các đường kinh độ / vĩ độ. Độ chính xác của ước tính này giảm khi các điểm càng gần Bắc Cực hoặc Nam Cực, có nghĩa là các truy vấn Geohash có nhiều dương tính giả hơn ở các vĩ độ cực đại.