日本の住所は表記ゆれがよく見られます。例えば、以下は全て国会議事堂の住所であり、人間が見れば同一住所であることがわかりますが、コンピューターの扱う文字列としては全て別物です。
東京都千代田区永田町1丁目7−1
東京都千代田区永田町1-7−1
千代田区永田町1-7−1
東京都千代田区永田町一丁目7番1号
東京都千代田区永田町1の7の1
東京都千代田区永田町一-七−一
東京都千代田区永田町1-7−1
出典:
そのため、住所を使ったデータ処理をするために住所の正規化が必要になるケースもあります。
この記事ではGeoloniaさんのnormalize-japanese-addressesとBigQueryのRemote Functionsを組み合わせて大量の住所の正規化を行ってみます。
BigQuery Remote Functionsとは
BigQueryのRemote Functions機能はGoogle CloudのFaaSであるCloud FunctionsとDWHであるBigQueryを統合する機能です。この機能を使うことでBigQueryからCloud Functionsで作成された処理を呼び出して結果を得ることができます。
Cloud Functionsの用意
まずは以下のCloud Functionsを作成します。
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);
}
};
そして、以下のコマンドでデプロイをします。サービスアカウントは予め作成しておく必要があります。
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
BigQueryとCloud Functionsを繋ぎこむ
次にBigQueryとCloud Functionsを繋ぐために以下のリソースを作成します。ここで作成しているリソースの詳細は以下のブログで説明しているので、こちらも併せてご確認ください。
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}"
}
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さんに感謝いたします。