Ikuti semua informasi yang diumumkan di Firebase Summit, dan pelajari bagaimana Firebase dapat membantu Anda mempercepat pengembangan aplikasi dan menjalankan aplikasi dengan percaya diri. Pelajari Lebih Lanjut

Kueri geografis

Tetap teratur dengan koleksi Simpan dan kategorikan konten berdasarkan preferensi Anda.

Banyak aplikasi memiliki dokumen yang diindeks oleh lokasi fisik. Misalnya, aplikasi Anda mungkin mengizinkan pengguna menjelajahi toko di dekat lokasi mereka saat ini.

Cloud Firestore hanya mengizinkan satu klausa rentang per kueri gabungan , yang berarti kami tidak dapat melakukan kueri geografis hanya dengan menyimpan garis lintang dan garis bujur sebagai bidang terpisah dan membuat kueri kotak pembatas.

Solusi: Geohash

Geohash adalah sistem untuk mengkodekan pasangan (latitude, longitude) menjadi string Base32 tunggal. Dalam sistem Geohash dunia dibagi menjadi kotak persegi panjang. Setiap karakter string Geohash menentukan salah satu dari 32 subdivisi dari hash awalan. Misalnya Geohash abcd adalah salah satu dari 32 hash empat karakter yang sepenuhnya terkandung dalam Geohash abc yang lebih besar.

Semakin lama awalan bersama antara dua hash, semakin dekat mereka satu sama lain. Misalnya abcdef lebih dekat ke abcdeg daripada abcdff . Namun sebaliknya tidak benar! Dua area mungkin sangat dekat satu sama lain sementara memiliki Geohash yang sangat berbeda:

Geohash berjauhan

Kita dapat menggunakan Geohashes untuk menyimpan dan mengkueri dokumen berdasarkan posisi di Cloud Firestore dengan efisiensi yang wajar sementara hanya membutuhkan satu bidang yang diindeks.

Instal pustaka pembantu

Membuat dan menguraikan Geohashes melibatkan beberapa matematika rumit, jadi kami membuat perpustakaan pembantu untuk mengabstraksi bagian tersulit di Android, Apple, dan 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

Cepat

Catatan: Produk ini tidak tersedia di target watchOS dan App Clip.
// Tambahkan ini ke pod Podfile Anda 'GeoFire/Utils'

Java

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

Simpan Geohash

Untuk setiap dokumen yang ingin Anda indeks berdasarkan lokasi, Anda perlu menyimpan bidang 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(() => {
  // ...
});

Cepat

Catatan: Produk ini tidak tersedia di target watchOS dan 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) {
                // ...
            }
        });

Kueri Geohash

Geohash memungkinkan kita untuk memperkirakan kueri area dengan menggabungkan serangkaian kueri di bidang Geohash dan kemudian memfilter beberapa kesalahan positif:

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

Cepat

Catatan: Produk ini tidak tersedia di target watchOS dan 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
                // ...
            }
        });

Keterbatasan

Menggunakan Geohashes untuk menanyakan lokasi memberi kami kemampuan baru, tetapi dilengkapi dengan batasannya sendiri:

  • Positif Palsu - kueri oleh Geohash tidak tepat, dan Anda harus memfilter hasil positif palsu di sisi klien. Bacaan ekstra ini menambah biaya dan latensi ke aplikasi Anda.
  • Kasus Tepi - metode kueri ini bergantung pada perkiraan jarak antara garis bujur/lintang. Keakuratan perkiraan ini menurun saat titik semakin dekat ke Kutub Utara atau Selatan yang berarti kueri Geohash memiliki lebih banyak kesalahan positif pada garis lintang ekstrem.