LoginSignup
4
1

More than 1 year has passed since last update.

firestore で地理的な近傍を検索する

Posted at

TL;DR

  • firestore でサービスを開発しており、ある場所の地理的な近傍を検索したい。
  • 近傍検索において GeoHash は「十分に効率的に」処理できる手法である。
  • select の選択肢として使用する場合のカスタムフックを作成した。

前提

クライアント

  • react-admin を使用する。
  • DataProvider として react-admin-firebase を使用する。

データ

  • firestore を使用する。

  • レコードには、以下が入っていることとする

    フィールド名 内容
    geo [number, number] [緯度, 経度]
    geohash string GeoHash
  • サンプル データ登録例

import { collection, addDoc } from "firebase/firestore";
import * as geofire from "geofire-common";

const cities = [
  ['秋葉原', 35.698715, 139.774021],
  ['上野',  35.714171, 139.775220],
];

for(const city of cities) {
  const [name, lat, lng] = city;
  await addDoc(collection(db, "cities"), {
    name,
    geohash: geofire.geohashForLocation([lat, lng]),
    geo: [lat, lng],
  });
}

カスタムフック

import * as geofire from "geofire-common";
import { useEffect, useState } from "react";
import firebase from "firebase/compat/app";
import { useDataProvider } from "react-admin";

type CollectionReference = firebase.firestore.CollectionReference;

export const useNearBy = (
  resource: string,
  center: [number, number],
  radius: number // Km
) => {
  const dp = useDataProvider();
  const [choices, setChoices] = useState<unknown[]>([]);

  useEffect(() => {
    if (!resource) return;
    if (!center) return;
    if (!radius) return;

    const bounds = geofire.geohashQueryBounds(center, radius * 1000);

    const params = bounds.map(([s, e]) => ({
      pagination: { page: 1, perPage: 1000 },
      sort: { field: "geohash", order: "asc" },
      filter: {
        collectionQuery: (cr: CollectionReference) =>
          cr.orderBy("geohash").startAt(s).endAt(e),
      },
    }));

    const jobs = params.map((p) => dp.getList(resource, p));

    Promise.all(jobs).then((all) => {
      const result = all
        .map(({ data }) => data)
        .flat()
        .filter(({ geo }) => geo)
        .map((record) => ({
          record,
          distance: geofire.distanceBetween(center, record.geo),
        }))
        .filter(({ distance }) => distance < radius)
        .sort((a, b) => a.distance - b.distance)
        .map(({ record }) => record);
      setChoices(result);
    });
  }, [dp, resource, center, radius]);

  return choices;
};

使い方

const NearByInput = (props) => {
  const center = [35.6812362, 139.7649361]; // 東京駅
  const choices = useNearBy("cities", center, 10); // 10 Km 範囲
  return <AutocompleteInput {...props} choices={choices} optionText="name" />;
};

おまけ

  • geofire-common ライブラリがとても有用。今回使用した関数は以下。
    関数名 入力 出力
    geohashForLocation 緯度経度 GeoHash
    geohashQueryBounds 中心(緯度経度)
    半径(Km)
    円を覆う適度な GeoHash (9 個以内)
    distanceBetween 地点A
    地点B
    二地点間の距離(Km)

参考

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1