BigQueryのRemote Functionで大量の日本語住所の正規化を行う

日本の住所は表記ゆれがよく見られます。例えば、以下は全て国会議事堂の住所であり、人間が見れば同一住所であることがわかりますが、コンピューターの扱う文字列としては全て別物です。

> 東京都千代田区永田町１丁目７−１
> 東京都千代田区永田町１-７−１
> 千代田区永田町１-７−１
> 東京都千代田区永田町一丁目7番1号
> 東京都千代田区永田町１の7の１
> 東京都千代田区永田町一-七−一
> 東京都千代田区永田町１-７−１

出典:

https://www.geolonia.com/address-normalization/

そのため、住所を使ったデータ処理をするために住所の正規化が必要になるケースもあります。
この記事ではGeoloniaさんのnormalize-japanese-addressesとBigQueryのRemote Functionsを組み合わせて大量の住所の正規化を行ってみます。

# BigQuery Remote Functionsとは
BigQueryのRemote Functions機能はGoogle CloudのFaaSであるCloud　FunctionsとDWHであるBigQueryを統合する機能です。この機能を使うことでBigQueryからCloud Functionsで作成された処理を呼び出して結果を得ることができます。

https://cloud.google.com/bigquery/docs/remote-functions?hl=ja

# Cloud Functionsの用意

まずは以下のCloud Functionsを作成します。

```ts:index.ts
import type { HttpFunction } from '@google-cloud/functions-framework/build/src/functions';
const { config, normalize } = require('@geolonia/normalize-japanese-addresses')
const fs = require('fs');

export const normalizeJapaneseAddress: HttpFunction = async (req, res) => {
  // 住所データは予めダウンロードしておく
  const addressDataPath = __dirname + "/../../static/japanese-addresses-master/api/ja";
  console.log(addressDataPath);
  if(fs.existsSync(addressDataPath)) {
    console.log(`using address data in ${addressDataPath}`);
    config.japaneseAddressesApi = `file://${addressDataPath}`;
  }

  // 複数の住所データが配列で渡される
  // https://cloud.google.com/bigquery/docs/remote-functions#input_format
  const calls: string[][] = req.body.calls
  console.log(`processing ${calls.length} rows`)
  try {
    const normalizeResults = await Promise.allSettled(calls.map(async (call: string[]) => {
      const address = call[0];
      return JSON.stringify(await normalize(address));
    }));

    const replies = normalizeResults.map((normalizeResult) => {
      if(normalizeResult.status === "fulfilled") {
        return normalizeResult.value;
      }else {
        return JSON.stringify({
          pref: "",
          city: "",
          town: "",
          addr: "",
          lat: null,
          lng: null,
          level: 0,
        });
      }
    });

    res.send({replies: replies});
  } catch(e) {
    console.error(e);
    res.send({errorMessage: `unexpected error occured: ${e}`});
    res.sendStatus(500);
  }
};
```

そして、以下のコマンドでデプロイをします。サービスアカウントは予め作成しておく必要があります。

```bash
PROJECT_ID=<プロジェクトID>

gcloud functions deploy normalize_japanese_address \
  --project=$PROJECT_ID \
  --region=us-central1 \
  --gen2 \
  --runtime node18 \
  --entry-point normalize_japanese_address \
  --trigger-http \
  --no-allow-unauthenticated \
  --run-service-account normalize_japanese_address@$PROJECT_ID.iam.gserviceaccount.com
```

https://cloud.google.com/sdk/gcloud/reference/functions/deploy

# BigQueryとCloud Functionsを繋ぎこむ
次にBigQueryとCloud Functionsを繋ぐために以下のリソースを作成します。ここで作成しているリソースの詳細は以下のブログで説明しているので、こちらも併せてご確認ください。

https://techblog.zozo.com/entry/bq-kintone

```terraform
resource "google_bigquery_connection" "cloud_resource" {
  connection_id = "cloud_resource"
  location      = "US"
  description   = "Connection for Cloud Resource"
  cloud_resource {}
}

data "google_cloud_run_service" "normalize_japanese_address" {
  name     = "normalize_japanese_address"
  location = "us-central1"
}

resource "google_cloud_run_service_iam_member" "normalize_japanese_address" {
  location = data.google_cloud_run_service.normalize_japanese_address.location
  service  = data.google_cloud_run_service.normalize_japanese_address.name
  role     = "roles/run.invoker"
  member   = "serviceAccount:${google_bigquery_connection.cloud_resource.cloud_resource[0].service_account_id}"
}
```

```sql
CREATE FUNCTION `<プロジェクトID>.<データセットID>.`.normalize_japanese_address(address STRING) RETURNS STRING
REMOTE WITH CONNECTION `<プロジェクトID>.US.<コネクション名>`
OPTIONS (
  endpoint = '<Cloud FunctionsのエンドポイントURL>',
  max_batching_rows = 100 -- 大きくするほどスループットは上がるが、やり過ぎるとFunctionsがタイムアウトするので注意
)
```

# 性能検証
ZOZOTOWNの配送先住所から100万件を抽出した住所データを用いて性能検証を行います。

どこまでを判別ができたのか(都道府県レベル・市区町村レベルなど)を表すlevelの内訳も以下に示します。
ほとんどのデータが町丁目まで判別できていることがわかります。

| level | 件数 | 割合(%) |
|-------|------|------|
|0 - 都道府県も判別できなかった。| 518 |  0.05   |
|1 - 都道府県まで判別できた。| 10 |  0.00    |
|2 - 市区町村まで判別できた。|  9706 |  0.97  |
|3 - 町丁目まで判別できた。|  989766 | 98.98 |

# 謝辞
このような非常に便利なライブラリを作成し、MITライセンスという扱いやすい形式で公開してくださっているGeoloniaさんに感謝いたします。

https://www.geolonia.com/