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

Zapytania geograficzne

Zadbaj o dobrą organizację dzięki kolekcji Zapisuj i kategoryzuj treści zgodnie ze swoimi preferencjami.

Wiele aplikacji zawiera dokumenty indeksowane według lokalizacji fizycznych. Na przykład Twoja aplikacja może umożliwiać użytkownikom przeglądanie sklepów w pobliżu ich bieżącej lokalizacji.

Cloud Firestore dopuszcza tylko jedną klauzulę zakresu na zapytanie złożone , co oznacza, że ​​nie możemy wykonywać zapytań geograficznych, po prostu przechowując szerokość i długość geograficzną jako osobne pola i wysyłając zapytanie do prostokąta ograniczającego.

Rozwiązanie: Geohasze

Geohash to system kodowania pary (latitude, longitude) w pojedynczy ciąg Base32. W systemie Geohash świat jest podzielony na prostokątną siatkę. Każdy znak łańcucha Geohash określa jeden z 32 podpodziałów przedrostka skrótu. Na przykład Geohash abcd jest jednym z 32 czteroznakowych skrótów w pełni zawartych w większym Geohash abc .

Im dłuższy wspólny prefiks między dwoma skrótami, tym bliżej siebie są. Na przykład abcdef jest bliżej abcdeg niż abcdff . Jednak sytuacja odwrotna nie jest prawdziwa! Dwa obszary mogą być bardzo blisko siebie, mając jednocześnie bardzo różne Geohash:

Geohasze są daleko od siebie

Możemy używać Geohash do przechowywania i wyszukiwania dokumentów według pozycji w Cloud Firestore z rozsądną wydajnością, wymagając tylko jednego indeksowanego pola.

Zainstaluj bibliotekę pomocniczą

Tworzenie i analizowanie Geohashów wymaga trochę skomplikowanej matematyki, dlatego stworzyliśmy biblioteki pomocnicze, aby wyodrębnić najtrudniejsze części na Androida, Apple i w Internecie:

Sieć

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

Szybki

Uwaga: ten produkt nie jest dostępny w docelowych systemach watchOS i App Clip.
// Dodaj to do swojego Podfile pod 'GeoFire/Utils'

Kotlin+KTX

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

Java

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

Przechowuj Geohasze

Dla każdego dokumentu, który chcesz zindeksować według lokalizacji, musisz zapisać pole Geohash:

Sieć

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

Szybki

Uwaga: ten produkt nie jest dostępny w docelowych systemach watchOS i 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+KTX

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

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

Geohasze zapytań

Geohash pozwalają nam przybliżyć zapytania o obszar, łącząc zestaw zapytań w polu Geohash, a następnie odfiltrowując niektóre fałszywe alarmy:

Sieć

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

Szybki

Uwaga: ten produkt nie jest dostępny w docelowych systemach watchOS i 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)
}

Kotlin+KTX

// Find cities within 50km of London
val center = GeoLocation(51.5074, 0.1278)
val radiusInM = 50.0 * 1000.0

// 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.
val bounds = GeoFireUtils.getGeoHashQueryBounds(center, radiusInM)
val tasks: MutableList<Task<QuerySnapshot>> = ArrayList()
for (b in bounds) {
    val 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 {
            val matchingDocs: MutableList<DocumentSnapshot> = ArrayList()
            for (task in tasks) {
                val snap = task.result
                for (doc in snap!!.documents) {
                    val lat = doc.getDouble("lat")!!
                    val lng = doc.getDouble("lng")!!

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

            // matchingDocs contains the results
            // ...
        }

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

Ograniczenia

Używanie Geohashów do wyszukiwania lokalizacji daje nam nowe możliwości, ale wiąże się z własnym zestawem ograniczeń:

  • Fałszywe pozytywne wyniki - zapytania Geohash nie są dokładne i musisz odfiltrować fałszywie pozytywne wyniki po stronie klienta. Te dodatkowe odczyty zwiększają koszty i opóźnienia w Twojej aplikacji.
  • Edge Cases - ta metoda zapytania polega na oszacowaniu odległości między liniami długości/szerokości geograficznej. Dokładność tego oszacowania maleje, gdy punkty zbliżają się do bieguna północnego lub południowego, co oznacza, że ​​zapytania Geohash mają więcej fałszywych trafień na skrajnych szerokościach geograficznych.