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

Geo-Abfragen

Mit Sammlungen den Überblick behalten Sie können Inhalte basierend auf Ihren Einstellungen speichern und kategorisieren.

Viele Apps verfügen über Dokumente, die nach physischen Standorten indiziert sind. Beispielsweise könnte Ihre App Benutzern ermöglichen, Geschäfte in der Nähe ihres aktuellen Standorts zu durchsuchen.

Cloud Firestore lässt nur eine einzige Bereichsklausel pro zusammengesetzter Abfrage zu , was bedeutet, dass wir keine geografischen Abfragen durchführen können, indem wir einfach Breiten- und Längengrad als separate Felder speichern und einen Begrenzungsrahmen abfragen.

Lösung: Geohashes

Geohash ist ein System zum Codieren eines Paares (latitude, longitude) in eine einzelne Base32-Zeichenfolge. Im Geohash-System ist die Welt in ein rechteckiges Raster unterteilt. Jedes Zeichen einer Geohash-Zeichenfolge gibt eine von 32 Unterteilungen des Präfix-Hash an. Zum Beispiel ist der Geohash abcd einer von 32 vierstelligen Hashes, die vollständig im größeren Geohash abc enthalten sind.

Je länger das gemeinsame Präfix zwischen zwei Hashes ist, desto näher sind sie beieinander. Zum Beispiel ist abcdef näher an abcdeg als an abcdff . Die Umkehrung gilt jedoch nicht! Zwei Bereiche können sehr nahe beieinander liegen und gleichzeitig sehr unterschiedliche Geohashes haben:

Geohashes weit auseinander

Wir können Geohashes verwenden, um Dokumente nach Position in Cloud Firestore mit angemessener Effizienz zu speichern und abzufragen, während nur ein einziges indiziertes Feld erforderlich ist.

Hilfsbibliothek installieren

Das Erstellen und Analysieren von Geohashes erfordert einige knifflige Mathematik, daher haben wir Hilfsbibliotheken erstellt, um die schwierigsten Teile auf Android, Apple und im Web zu abstrahieren:

Netz

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

Schnell

Hinweis: Dieses Produkt ist auf watchOS- und App Clip-Zielen nicht verfügbar.
// Fügen Sie dies zu Ihrem Podfile pod 'GeoFire/Utils' hinzu

Java

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

Speichern Sie Geohashes

Für jedes Dokument, das Sie nach Standort indizieren möchten, müssen Sie ein Geohash-Feld speichern:

Netz

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

Schnell

Hinweis: Dieses Produkt ist auf watchOS- und App Clip-Zielen nicht verfügbar.
// 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) {
                // ...
            }
        });

Geohashes abfragen

Geohashes ermöglichen es uns, Gebietsabfragen zu approximieren, indem wir eine Reihe von Abfragen im Geohash-Feld zusammenführen und dann einige falsch positive Ergebnisse herausfiltern:

Netz

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

Schnell

Hinweis: Dieses Produkt ist auf watchOS- und App Clip-Zielen nicht verfügbar.
// 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
                // ...
            }
        });

Einschränkungen

Die Verwendung von Geohashes zum Abfragen von Standorten gibt uns neue Möglichkeiten, ist jedoch mit eigenen Einschränkungen verbunden:

  • False Positives – Die Abfrage durch Geohash ist nicht exakt, und Sie müssen False-Positive-Ergebnisse auf der Client-Seite herausfiltern. Diese zusätzlichen Lesevorgänge erhöhen die Kosten und die Latenz Ihrer App.
  • Edge Cases – diese Abfragemethode beruht auf der Schätzung des Abstands zwischen Längen-/Breitengradlinien. Die Genauigkeit dieser Schätzung nimmt ab, wenn sich die Punkte dem Nord- oder Südpol nähern, was bedeutet, dass Geohash-Abfragen in extremen Breitengraden mehr Fehlalarme aufweisen.