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

แบบสอบถามทางภูมิศาสตร์

จัดทุกอย่างให้เป็นระเบียบอยู่เสมอด้วยคอลเล็กชัน บันทึกและจัดหมวดหมู่เนื้อหาตามค่ากำหนดของคุณ

แอพจำนวนมากมีเอกสารที่จัดทำดัชนีตามสถานที่ตั้งจริง ตัวอย่างเช่น แอพของคุณอาจอนุญาตให้ผู้ใช้เรียกดูร้านค้าใกล้ตำแหน่งปัจจุบันของพวกเขา

Cloud Firestore อนุญาตให้ใช้ ประโยคช่วงเดียวต่อการสืบค้นแบบผสม ซึ่งหมายความว่าเราไม่สามารถดำเนินการสืบค้นข้อมูลทางภูมิศาสตร์โดยเพียงแค่จัดเก็บละติจูดและลองจิจูดเป็นฟิลด์แยกกันและการสืบค้นในกล่องที่มีขอบเขต

วิธีแก้ปัญหา: Geohashes

Geohash เป็นระบบสำหรับเข้ารหัสคู่ (latitude, longitude) เป็นสตริง Base32 เดียว ในระบบ Geohash โลกถูกแบ่งออกเป็นตารางสี่เหลี่ยม อักขระแต่ละตัวของสตริง Geohash จะระบุหนึ่งใน 32 ส่วนย่อยของแฮชคำนำหน้า ตัวอย่างเช่น Geohash abcd เป็นหนึ่งใน 32 แฮชสี่อักขระที่อยู่ภายใน Geohash abc ที่ใหญ่กว่า

ยิ่งใช้คำนำหน้าร่วมกันระหว่างสองแฮชนานเท่าใด แฮชก็จะยิ่งใกล้กันมากขึ้นเท่านั้น ตัวอย่างเช่น abcdef ใกล้เคียงกับ abcdeg มากกว่า abcdff อย่างไรก็ตามการสนทนาไม่เป็นความจริง! พื้นที่สองแห่งอาจอยู่ใกล้กันมากในขณะที่มี Geohash ต่างกันมาก:

Geohashes ห่างกัน

เราสามารถใช้ Geohash เพื่อจัดเก็บและสืบค้นเอกสารตามตำแหน่งใน Cloud Firestore ได้อย่างมีประสิทธิภาพที่เหมาะสม ในขณะที่ต้องการเพียงฟิลด์ที่จัดทำดัชนีเพียงฟิลด์เดียวเท่านั้น

ติดตั้งไลบรารีตัวช่วย

การสร้างและการแยกวิเคราะห์ Geohash เกี่ยวข้องกับคณิตศาสตร์ที่ยุ่งยาก ดังนั้นเราจึงสร้างไลบรารีตัวช่วยเพื่อสรุปส่วนที่ยากที่สุดใน Android, Apple และเว็บ:

เว็บ

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

หมายเหตุ: ผลิตภัณฑ์นี้ไม่สามารถใช้ได้กับเป้าหมาย watchOS และ App Clip
// เพิ่มสิ่งนี้ในพ็อด Podfile ของคุณ 'GeoFire/Utils'

Java

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

ร้านค้า Geohashes

สำหรับแต่ละเอกสารที่คุณต้องการสร้างดัชนีตามตำแหน่ง คุณจะต้องจัดเก็บฟิลด์ Geohash:

เว็บ

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

หมายเหตุ: ผลิตภัณฑ์นี้ไม่สามารถใช้ได้กับเป้าหมาย watchOS และ 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
    // ...
}

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

Geohash ช่วยให้เราสามารถประมาณการสืบค้นพื้นที่โดยเข้าร่วมชุดของข้อความค้นหาในฟิลด์ Geohash แล้วกรองผลบวกที่ผิดพลาดออก:

เว็บ

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

หมายเหตุ: ผลิตภัณฑ์นี้ไม่สามารถใช้ได้กับเป้าหมาย watchOS และ 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)
}

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

ข้อจำกัด

การใช้ Geohashes สำหรับการสืบค้นตำแหน่งทำให้เรามีความสามารถใหม่ แต่มาพร้อมกับข้อจำกัดของตัวเอง:

  • ผล บวกเท็จ - การสืบค้นโดย Geohash นั้นไม่ถูกต้อง และคุณต้องกรองผลลัพธ์ที่เป็นเท็จในฝั่งไคลเอ็นต์ออก การอ่านเพิ่มเติมเหล่านี้จะเพิ่มค่าใช้จ่ายและเวลาแฝงให้กับแอปของคุณ
  • กรณีขอบ - วิธีการสืบค้นนี้อาศัยการประมาณระยะห่างระหว่างเส้นลองจิจูด/ละติจูด ความแม่นยำของการประมาณนี้ลดลงเมื่อจุดเข้าใกล้ขั้วโลกเหนือหรือขั้วโลกใต้มากขึ้น ซึ่งหมายความว่าข้อความค้นหา Geohash มีผลบวกที่ผิดพลาดมากกว่าที่ละติจูดสุดขั้ว