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

Consultas geográficas

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

Muitos aplicativos têm documentos indexados por localizações físicas. Por exemplo, seu aplicativo pode permitir que os usuários procurem lojas perto de sua localização atual.

O Cloud Firestore permite apenas uma única cláusula de intervalo por consulta composta , o que significa que não podemos realizar consultas geográficas simplesmente armazenando latitude e longitude como campos separados e consultando uma caixa delimitadora.

Solução: Geohashes

Geohash é um sistema para codificar um par (latitude, longitude) em uma única string Base32. No sistema Geohash, o mundo é dividido em uma grade retangular. Cada caractere de uma string Geohash especifica uma das 32 subdivisões do prefixo hash. Por exemplo, o Geohash abcd é um dos 32 hashes de quatro caracteres totalmente contidos no Geohash abc maior.

Quanto maior o prefixo compartilhado entre dois hashes, mais próximos eles estão um do outro. Por exemplo, abcdef está mais próximo de abcdeg do que abcdff . No entanto, o inverso não é verdadeiro! Duas áreas podem estar muito próximas uma da outra, embora tenham Geohashes muito diferentes:

Geohashes distantes

Podemos usar Geohashes para armazenar e consultar documentos por posição no Cloud Firestore com eficiência razoável, exigindo apenas um único campo indexado.

Instalar biblioteca auxiliar

Criar e analisar Geohashes envolve alguns cálculos complicados, por isso criamos bibliotecas auxiliares para abstrair as partes mais difíceis no Android, Apple e Web:

Rede

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

Rápido

Observação: este produto não está disponível em destinos de watchOS e App Clip.
// Adicione isso ao seu pod de Podfile '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'

Armazenar Geohashes

Para cada documento que você deseja indexar por localização, você precisará armazenar um campo Geohash:

Rede

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

Rápido

Observação: este produto não está disponível em destinos de watchOS e 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) {
                // ...
            }
        });

Consultar Geohashes

Os Geohashes nos permitem aproximar as consultas de área juntando um conjunto de consultas no campo Geohash e, em seguida, filtrando alguns falsos positivos:

Rede

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

Rápido

Observação: este produto não está disponível em destinos de watchOS e 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
                // ...
            }
        });

Limitações

O uso de Geohashes para consultar locais nos dá novos recursos, mas vem com seu próprio conjunto de limitações:

  • Falsos positivos - a consulta por Geohash não é exata e você deve filtrar os resultados falsos positivos no lado do cliente. Essas leituras extras adicionam custo e latência ao seu aplicativo.
  • Casos extremos - este método de consulta baseia-se na estimativa da distância entre as linhas de longitude/latitude. A precisão dessa estimativa diminui à medida que os pontos se aproximam do Pólo Norte ou Sul, o que significa que as consultas Geohash têm mais falsos positivos em latitudes extremas.