1. はじめに:500万社データベースにおける日本語検索の壁
日本全国の登記法人約500万社を網羅するB2Bプラットフォームや企業検索エンジンを構築する際、最大の技術的難所となるのが**「日本語テキスト検索のレイテンシと精度の両立」**です。
レコード数が数百件〜数万件程度であれば、単純な WHERE name LIKE '%メルカリ%' で十分動作します。しかし、データ量が500万件を超えると、部分一致検索はテーブルフルスキャン(Seq Scan)を引き起こし、1回のクエリに5秒〜15秒以上の膨大なレスポンス時間がかかってしまいます。これではオートコンプリート(インクリメンタルサーチ)はもちろん、実用的なSaaSプロダクトとして成り立ちません。
さらに、日本語の企業名検索には以下のような特有の揺らぎが存在します:
-
文字種の混在: 全角・半角カタカナ(
メルカリvsメルカリ)、アルファベット(DMMvsDMM) -
法人格の前後表記: 前株・後株・略称(
株式会社ヤフーvsヤフー(株)) -
長音記号(音引き)や表記揺れ:
テクノロジーvsテクノロジ、コンピューターvsコンピュータ - 漢字・カナの入力揺れ: ひらがなやカタカナ、漢字のいずれで検索されても正しい企業をヒットさせる必要性
本記事では、全国500万社の法人データ基盤(Kigyou-list)で実運用している、Pythonによる入力正規化とPostgreSQLのインデックスチューニング(pg_trgm & B-Tree)を組み合わせ、500万件の検索を10ミリ秒未満(sub-10ms)で高速実行するアーキテクチャを解説します。
2. システム全体アーキテクチャ
単一の検索アルゴリズムに頼るのではなく、**「クエリ正規化エンジン」と「デュアルインデックス(B-Tree前方一致 + pg_trgm GINトライグラム)」**を組み合わせたハイブリッドパイプラインを採用しています。
処理パイプラインの3ステップ
-
ステップ1:クエリの正規化 (Pythonレイヤー):
ユーザーの入力文字列から法人格を除去し、NFKC正規化およびカタカナ音韻変換を実施。 -
ステップ2:PostgreSQL デュアルインデックス検索 (DBレイヤー):
- 入力文字数が短い場合やプレフィックス一致:
B-Tree (varchar_pattern_ops)で瞬時に候補取得(1〜3ms) - 部分一致・タイポ・曖昧検索:
pg_trgm GINインデックスで類似度スコアリング抽出(5〜10ms)
- 入力文字数が短い場合やプレフィックス一致:
-
ステップ3:スコアリングと優先順位付け (Ranking):
完全一致 ➔ 前方一致 ➔ カナ一致 ➔ 類似度スコア順にソートしてレスポンス。
3. 実装:Pythonによる検索クエリ正規化エンジン
ユーザーが「(株)ソフトバンク」と入力しても「そふとばんく」「SOFTBANK」と入力しても同一のマスタを引けるよう、データベースに問い合わせる前にクエリの前処理を行います。
import re
import unicodedata
import pykakasi
# 初期化
kks = pykakasi.kakasi()
# 除去対象の法人格パターン
LEGAL_ENTITIES = [
r'株式会社', r'有限会社', r'合同会社', r'合資会社', r'合名会社',
r'一般社団法人', r'公益社団法人', r'一般財団法人', r'公益財団法人',
r'特定非営利活動法人', r'NPO法人', r'NPO法人', r'医療法人', r'社会福祉法人'
]
# 略称の標準化
ABBR_MAP = {
'㈱': '株式会社', '㈲': '有限会社', '㈭': '合同会社',
'(株)': '株式会社', '(株)': '株式会社',
'(有)': '有限会社', '(有)': '有限会社',
'(同)': '合同会社', '(同)': '合同会社'
}
def normalize_search_query(query: str) -> dict:
"""
検索クエリを正規化し、漢字コア名とカタカナ音韻クエリを生成
"""
if not query:
return {"clean": "", "kana": ""}
# 1. NFKC正規化(全角英数・半角カナの統一)
normalized = unicodedata.normalize('NFKC', query).strip()
# 2. 略称置換
for abbr, full in ABBR_MAP.items():
normalized = normalized.replace(abbr, full)
# 3. 法人格の除去
for entity in LEGAL_ENTITIES:
normalized = re.sub(rf'^{entity}', '', normalized)
normalized = re.sub(rf'{entity}$', '', normalized)
normalized = re.sub(rf'\s*{entity}\s*', '', normalized)
clean_query = normalized.strip()
# 4. pykakasi による全角カタカナ化(ひらがな・英字対応)
result = kks.convert(clean_query)
kana_query = "".join([item['kana'] for item in result])
# 5. 長音(ー)の表記揺れ対応(テクノロジ ➔ テクノロジー等)
return {
"raw": query,
"clean": clean_query,
"kana": kana_query
}
# 動作例
print(normalize_search_query("(株)サイバーエージェント"))
# {'raw': '(株)サイバーエージェント', 'clean': 'サイバーエージェント', 'kana': 'サイバーエージェント'}
print(normalize_search_query("さいばーえーじぇんと"))
# {'raw': 'さいばーえーじぇんと', 'clean': 'さいばーえーじぇんと', 'kana': 'サイバーエージェント'}
4. 実装:PostgreSQLのインデックス設計とチューニング
PostgreSQL側には、事前に正規化された社名(clean_name)と全角カタカナフリガナ(name_kana)を保持させておきます。
① 拡張機能とテーブル定義
-- 1. pg_trgm 拡張を有効化(トライグラム類似度計算用)
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- 2. 企業マスタテーブル定義
CREATE TABLE companies (
corporate_number CHAR(13) PRIMARY KEY,
name VARCHAR(255) NOT NULL, -- 正式名称(例: トヨタ自動車株式会社)
clean_name VARCHAR(255) NOT NULL, -- 法人格剥奪名(例: トヨタ自動車)
name_kana VARCHAR(255), -- 全角カタカナ(例: トヨタジドウシャ)
prefecture VARCHAR(20),
city VARCHAR(50),
postal_code VARCHAR(10),
phone VARCHAR(20),
website_url VARCHAR(500),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
② インデックスの使い分け(ここが最大のポイント)
部分一致と前方一致では最適なインデックスが異なります。2つのインデックスを併用します。
-- 1. トライグラムGINインデックス(部分一致・タイポ・あいまい検索用)
CREATE INDEX idx_companies_clean_name_trgm
ON companies USING gin (clean_name gin_trgm_ops);
CREATE INDEX idx_companies_kana_trgm
ON companies USING gin (name_kana gin_trgm_ops);
-- 2. B-Tree前方一致用インデックス(varchar_pattern_ops を指定)
-- 通常のB-TreeインデックスはロケールによりLIKE 'xxx%'で使われない場合があるため必須
CREATE INDEX idx_companies_clean_name_prefix
ON companies (clean_name varchar_pattern_ops);
CREATE INDEX idx_companies_kana_prefix
ON companies (name_kana varchar_pattern_ops);
③ PostgreSQL設定パラメータの最適化
GINインデックスのインメモリ走査と類似度閾値を調整します。
-- トライグラム一致判定の閾値(デフォルト0.3から調整)
SET pg_trgm.similarity_threshold = 0.25;
-- 複雑なソートとインデックススキャン用のメモリ割り当て
SET work_mem = '64MB';
5. 高速ハイブリッド検索クエリとベンチマーク
実際の検索APIで発行するSQLクエリです。完全一致、前方一致、トライグラム類似度の各条件を統合し、優先順位をつけて瞬時にTOP20件を取得します。
-- 「メルカリ」で検索する場合の実装クエリ
WITH search_candidates AS (
SELECT
corporate_number,
name,
clean_name,
name_kana,
prefecture,
website_url,
CASE
WHEN clean_name = 'メルカリ' THEN 100
WHEN clean_name LIKE 'メルカリ%' THEN 80
WHEN name_kana LIKE 'メルカリ%' THEN 70
ELSE similarity(clean_name, 'メルカリ') * 50
END AS rank_score
FROM companies
WHERE
-- B-Tree 前方一致または GIN トライグラムで候補を絞り込み
clean_name LIKE 'メルカリ%'
OR name_kana LIKE 'メルカリ%'
OR clean_name % 'メルカリ'
OR name_kana % 'メルカリ'
LIMIT 100
)
SELECT *
FROM search_candidates
ORDER BY rank_score DESC, corporate_number ASC
LIMIT 20;
パフォーマンス測定結果(500万件データセット)
| 検索手法 | 実行プラン (EXPLAIN ANALYZE) | レスポンス速度 | 評価 |
|---|---|---|---|
従来のLIKE検索WHERE name LIKE '%メルカリ%'
|
Seq Scan on companies | 6,420 ms (約6.4秒) | ❌ 実用不可(タイムアウト頻発) |
単一GINインデックスWHERE clean_name % 'メルカリ'
|
Bitmap Index Scan on trgm | 45.2 ms | ⚠️ やや重い |
B-Tree + GIN複合(Prefix OR Trigram)
|
BitmapOr on Prefix & Trgm | 7.8 ms | ✅ 超高速(オートコンプリート即応) |
前方一致を varchar_pattern_ops B-Tree で拾い、残りの揺らぎを GIN トライグラムで吸収する構造により、**従来の800倍以上の高速化(7.8ms)**を達成しました。
6. まとめと実運用での知見
500万社規模の日本企業データを高速に提供し続けるためのベストプラクティスです:
-
DBに生の入力文字列を直接投げない:
法人格の除去、全角半角の統一、カナ化をPython側で0.1msで行ってからSQLを発行することで、DB側のインデックスヒット率が飛躍的に高まる。 -
varchar_pattern_opsを忘れない:
PostgreSQLのデフォルトB-Treeインデックスは、Cロケール以外ではLIKE '前方%'検索でインデックスが使われないケースが多い。前方一致専用のB-Treeインデックスを作成することが極めて重要。 -
都道府県・市区町村のファセット絞り込みとの連携:
社名インデックスとprefecture(B-Tree)を組み合わせることで、さらに検索範囲を狭め、1ms台の超低遅延検索が可能となる。
本検索アーキテクチャは、日本全国500万社以上の法人オープンデータや求人・補助金インテントデータを網羅する企業検索プラットフォーム「Kigyou-list(企業リスト.com)」のバックエンド検索基盤として日々稼働しています。
大規模データベースの高速検索や日本語テキスト処理の参考になれば幸いです。
