Coğrafi sorgular

Birçok uygulamada, fiziksel konumlara göre dizine eklenen belgeler bulunur. Örneğin, uygulamanız kullanıcıların mevcut konumlarının yakınındaki mağazalara göz atmalarına izin verebilir.

Cloud Firestore, bileşik sorgu başına yalnızca tek bir aralık yan tümcesine izin verir; bu, yalnızca enlem ve boylamı ayrı alanlar olarak depolayarak ve bir sınırlayıcı kutuyu sorgulayarak coğrafi sorgular gerçekleştiremeyeceğimiz anlamına gelir.

Çözüm: Geohash'ler

Geohash, bir (latitude, longitude) çiftini tek bir Base32 dizisine kodlamak için bir sistemdir. Geohash sisteminde dünya dikdörtgen bir ızgaraya bölünmüştür. Geohash dizesinin her karakteri, önek karmasının 32 alt bölümünden birini belirtir. Örneğin Geohash abcd , daha büyük Geohash abc içinde tamamen bulunan 32 dört karakterli karmadan biridir.

İki karma arasındaki paylaşılan önek ne kadar uzun olursa, birbirlerine o kadar yakın olurlar. Örneğin abcdef , abcdeg daha abcdff . Ancak bunun tersi doğru değil! Çok farklı Geohash'lere sahipken iki alan birbirine çok yakın olabilir:

Geohash çok uzak

Yalnızca tek bir dizine alınmış alan gerektirirken, belgeleri Cloud Firestore'da konumlarına göre makul bir verimlilikle depolamak ve sorgulamak için Geohashes'ı kullanabiliriz.

Yardımcı kitaplığı yükleyin

Geohashes oluşturmak ve ayrıştırmak biraz zor matematik gerektirir, bu nedenle Android, Apple ve Web'deki en zor kısımları soyutlamak için yardımcı kitaplıklar oluşturduk:

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

Süratli

Not: Bu ürün, watchOS ve App Clip hedeflerinde mevcut değildir.
// Bunu 'GeoFire/Utils' Podfile bölmenize ekleyin

Java

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

Geohash'leri saklayın

Konuma göre indekslemek istediğiniz her belge için bir Geohash alanı saklamanız gerekir:

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

Süratli

Not: Bu ürün, watchOS ve App Clip hedeflerinde mevcut değildir.
// 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) {
                // ...
            }
        });

Geohash'leri Sorgula

Geohash'ler, Geohash alanında bir dizi sorguyu birleştirerek ve ardından bazı yanlış pozitifleri filtreleyerek alan sorgularını yaklaşık olarak belirlememizi sağlar:

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

Süratli

Not: Bu ürün, watchOS ve App Clip hedeflerinde mevcut değildir.
// 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
                // ...
            }
        });

sınırlamalar

Konumları sorgulamak için Geohashes kullanmak bize yeni yetenekler sağlar, ancak kendi sınırlamaları ile birlikte gelir:

  • Yanlış Pozitifler - Geohash tarafından yapılan sorgulama kesin değildir ve istemci tarafında yanlış pozitif sonuçları filtrelemeniz gerekir. Bu ekstra okumalar, uygulamanıza maliyet ve gecikme süresi ekler.
  • Uç Durumlar - bu sorgulama yöntemi, boylam/enlem çizgileri arasındaki mesafeyi tahmin etmeye dayanır. Bu tahminin doğruluğu, noktalar Kuzey veya Güney Kutbuna yaklaştıkça azalır, bu da Geohash sorgularının aşırı enlemlerde daha fazla yanlış pozitif olduğu anlamına gelir.