ホーム技術ブログ › 10
2026-09-07 #10

日本語の全文検索を「表記が違っても引ける」ようにする正規化と読み

長音・中黒・全角半角・カタカナ英語。text_search と text_yomi の2列を持たせて吸収する。

Python全文検索日本語処理SQLite

やりたいこと

「コンピュータ」と「コンピューター」、「Surfshark」と「サーフシャーク」。 話している側は同じことを言っているのに、文字にすると別物になります。 検索する人はどちらで打つか分からないので、どちらでも引けるようにしたいところです。

ただ、表示用のテキストは触りたくありません。 番組が使っている表記に揃える校正をしている以上、検索の都合で書き換えるわけにいかない。 そこで、検索専用の列を2つ持たせました。

CREATE TABLE segments (
  video_id TEXT, idx INTEGER,
  text           TEXT,   -- 音声認識の生出力(触らない)
  text_corrected TEXT,   -- 校正後の表示用(これを画面に出す)
  text_search    TEXT,   -- ① 正規化した検索用
  text_yomi      TEXT    -- ② ひらがなに開いた検索用
);

① 正規化 — 記号と長音を落とす

def normalize_for_search(text):
    text = unicodedata.normalize("NFKC", text)   # 全角英数 → 半角
    text = text.lower()                          # 英字は小文字に
    text = "".join(c for c in text if c not in _STRIP_CHARS)
    return text

# _STRIP_CHARS には 中黒・スラッシュ・長音「ー」・空白・括弧・句読点 が入る
normalize_for_search("MS-DOS")   # → 'msdos'

効いたのは長音を落とすところです。 「コンピュータ/コンピューター」「サーバ/サーバー」がこれで同じ形になります。

② 読み — かなに開く

def yomi_for_search(text):
    t = normalize_for_search(text)
    t = _ASCII_TOKEN.sub(lambda m: english_yomi.get(m.group(0), m.group(0)), t)
    hira = "".join(w["hira"] for w in kakasi.convert(t))
    for a, b in (("ゔぁ","ば"), ("ゔぃ","び"), ("ゔ","ぶ")):   # ヴ→バ行に畳む
        hira = hira.replace(a, b)
    return normalize_for_search(hira)

yomi_for_search("形態論")     # → 'けいたいろん'

③ 英字の固有名詞は、カナ読みを登録する

# data/english_yomi.csv
surfshark,さーふしゃーく
geekom,ぎこむ
valuebooks,ばりゅーぶっくす

表示は正式表記のまま、検索は「さーふしゃーく」でも通ります。 表示と検索を分けているので、正式表記に揃える校正と、ゆるく引ける検索が両立しました。

④ クエリ側にも同じ関数を通す

当たり前ですが、片方だけ正規化しても一致しません。 検索語にも同じ関数をかけて、両方が同じ形に落ちるようにしています。

⑤ 落とし穴 — 長音を落とすと、漢字側と一致しない

この設計には穴がありました。 正規化で長音を落とすので、yomi_for_search("ソシュール")そしゅる になります。 一方、音声認識が「ソシュール」を漢字に誤変換した「祖収る」の読みは そしゅうる です。

>>> yomi_for_search("ソシュール")
'そしゅる'
>>> yomi_for_search("祖収る風に言うのだったら")
'そしゅうるかぜにいうのだったら'
>>> 'そしゅる' in 'そしゅうるかぜに...'
False        # 一致しない

誤変換を探すツールを書いたとき、これで実際に取りこぼしました。 対策は、長音を母音に開いた形も鍵にすることです。

def yomi_keys(word):
    keys = {normalize_for_search(yomi_for_search(w)) for w in expand_choon(word)}
    return sorted(k for k in keys if len(k) >= 3)

yomi_keys("ソシュール")   # → ['そしゅうる', 'そしゅる']

⑥ おまけ — SQLite で踏んだやつ

# ❌ SELECT の反復中に同一カーソルで UPDATE すると、ループが1件で止まる
for row in con.execute("SELECT ... FROM segments"):
    con.execute("UPDATE segments SET ...")

# ✅ 先に fetchall() する
for row in con.execute("SELECT ... FROM segments").fetchall():
    con.execute("UPDATE segments SET ...")

← SQLite FTS5(contentless)の索… 「この単語、もっと出てたはずなんだけど」を機械で探す… → 目次へ

ゆる言語学ラジオ・ゆるコンピュータ科学ラジオ 公認/株式会社pedantic の了承と、運営者・安東暁史との合意に基づく協力体制で運営しています。ドメイン yururadio.jp は株式会社pedanticが保有。開発・保守: 安東暁史·このサイトについて·お問い合わせ