Diese Seite wurde von der Cloud Translation API übersetzt.
Switch to English

Geografische Abfragen

Viele Anwendungen haben Dokumente, die nach physischen Standorten indexiert werden. Zum Beispiel könnten Nutzer in Ihrer Anwendung Geschäfte in der Nähe ihres aktuellen Standorts suchen.

Firestore lässt nur eine einzelne Bereichsklausel pro zusammengesetzter Abfrage zu. Das bedeutet, dass wir für Geoabfragen nicht einfach die Breiten- und Längengrade als separate Felder speichern und dann einen Begrenzungsrahmen abfragen können.

Lösung: Geohashes

Geohash ist ein System zum Codieren eines (latitude, longitude)-Paares in einen einzelnen Base32-String. Im Geohash-System ist die Welt in ein rechteckiges Raster unterteilt. Jedes Zeichen eines Geohash-Strings gibt eine von 32 Untergruppen des Präfix-Hashs an. Beispiel: Die Geohash abcd ist einer von 32 stelligen Hash-Werten, 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 die Verbindungen zueinander. Beispiel: abcdef ist näher an abcdeg als abcdff. Das Gegenteil trifft jedoch nicht zu! Zwei Bereiche können sehr nah beieinander sein, obwohl sie sehr unterschiedliche Geohashes haben:

Weit entfernte Geohashes

Wir können Geohashes verwenden, um Dokumente in Firestore mit angemessener Effizienz nach Position zu speichern und abzufragen, wobei nur ein einziges indexiertes Feld erforderlich ist.

Hilfsbibliothek installieren

Das Erstellen und Parsen von Geohashen erfordert einige knifflige Berechnungen. Daher haben wir Hilfsbibliotheken erstellt, um die schwierigsten Teile auf Android, auf iOS und im Web zusammenzufassen:

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

Swift

// Add this to your Podfile
pod 'GeoFire/Utils'

Java
Android

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

Geohash speichern

Für jedes Dokument, das nach Standort indexiert werden soll, müssen Sie ein Geohash-Feld speichern:

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

Swift

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

Geohashes ermöglichen es uns, die Gebietsabfragen mithilfe eines Satzes von Abfragen im Feld „Geohash“ ungefähr zu lösen und anschließend einige falsch-positive Ergebnisse herauszufiltern:

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

Swift

// Find cities within 50km of London
let center = CLLocationCoordinate2D(latitude: 51.5074, longitude: 0.1278)
let radiusInKilometers: Double = 50

// 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: radiusInKilometers)
let queries = queryBounds.compactMap { (any) -> Query? in
    guard let bound = any as? GFGeoQueryBounds else { return nil }
    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 <= radiusInKilometers {
            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
Android

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

Beschränkungen

Die Verwendung von Geohashes zum Abfragen von Standorten bietet neue Funktionen, hat jedoch eigene Einschränkungen:

  • False Positives – Abfragen nach Geohash sind nicht genau. Sie müssen falsch-positive Ergebnisse auf Clientseite herausfiltern. Diese zusätzlichen Lesevorgänge erhöhen die Kosten und die Latenz Ihrer Anwendung.
  • Edge-Fälle: Diese Abfragemethode basiert auf der Schätzung der Entfernung zwischen Längen- und Breitengradlinien. Die Genauigkeit dieser Schätzung nimmt ab, je näher die Punkte am Nord- oder Südpol liegen. Das bedeutet, dass Geohash-Abfragen bei extremen Gratwerten mehr falsch-positive Ergebnisse liefern.