OpenSearchのkuromoji_readingformで日本語検索の表記揺れを自動吸収する
概要
OpenSearchで日本語の全文検索を実装する際に、表記揺れを吸収する方法を検証してみました。
ちなみにここで言う表記揺れは、「ネコ」「猫」「ねこ」のように同じ読みのカタカナ/漢字/ひらがなの単語を想定しています。
結論から
kuromoji_tokenizerでsynonymsを定義することで同義語辞書を作り実現することも可能ですが、都度手動追加が必要になってしまうため、代替案を検討したところkuromoji_readingformフィルターを使えば表記揺れを自動吸収することができました。
今回はこちらのサンプルコードをベースに解説します。
動作環境
- OpenSearch 2.11.0
- analysis-kuromoji plugin(形態素解析)
- analysis-icu plugin(文字正規化)
- Python 3.9+(検証スクリプト用)
- Docker Docker Compose(環境構築)
Kuromojiの仕組み
kuromoji_readingformフィルターとは
kuromoji_readingformは、トークンを読み仮名(カタカナまたはローマ字)に変換してくれるフィルターです。
{
"type": "kuromoji_readingform",
"use_romaji": false // false=カタカナ、true=ローマ字
}
トークン化の流れは以下のようになります:
1. 入力: "猫が好き"
↓ kuromoji_tokenizer(形態素解析)
2. トークン化: ["猫", "が", "好き"]
↓ kuromoji_readingform(読み変換)
3. 読み変換: ["ネコ", "ガ", "スキ"]
↓ kuromoji_stemmer(ステミング)
4. 最終トークン: ["ネコ", "ガ", "スキ"]
すべての表記を同じカタカナ形式に統一することで、表記の違いを吸収します。
インデックス時:
- 「猫が好き」 →
["ネコ", "ガ", "スキ"] - 「ねこが好き」 →
["ネコ", "ガ", "スキ"] - 「ネコが好き」 →
["ネコ", "ガ", "スキ"]
検索時:
- 「猫」で検索 →
["ネコ"] - 「ねこ」で検索 →
["ネコ"] - 「ネコ」で検索 →
["ネコ"]
どの表記で検索しても、インデックスされたトークン"ネコ"とマッチするため、すべてヒットするという仕組みです。
実装手順
今回のサンプルはローカルでの動作確認までとなります。
環境構築
1. Dockerfileの作成
以下の2つのプラグインをインストールする必要があります。
- analysis-kuromoji
- analysis-icu
FROM opensearchproject/opensearch:2.11.0
# Install analysis plugins
RUN /usr/share/opensearch/bin/opensearch-plugin install analysis-kuromoji && \
/usr/share/opensearch/bin/opensearch-plugin install analysis-icu
2. docker-compose.ymlの設定
version: '3'
services:
opensearch-node:
build:
context: .
dockerfile: Dockerfile
container_name: opensearch-node
environment:
- cluster.name=opensearch-cluster
- node.name=opensearch-node
- discovery.type=single-node
- bootstrap.memory_lock=true
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
- "DISABLE_INSTALL_DEMO_CONFIG=true"
- "DISABLE_SECURITY_PLUGIN=true"
ulimits:
memlock:
soft: -1
hard: -1
ports:
- 9200:9200
volumes:
- opensearch-data:/usr/share/opensearch/data
volumes:
opensearch-data:
3. 起動
docker-compose up -d
インデックス設定
次に、日本語アナライザーを定義したインデックスを作成します。
{
"settings": {
"analysis": {
"tokenizer": {
"kuromoji_tokenizer": {
"type": "kuromoji_tokenizer",
"mode": "search"
}
},
"filter": {
"katakana_readingform": {
"type": "kuromoji_readingform",
"use_romaji": false
},
"katakana_stemmer": {
"type": "kuromoji_stemmer"
}
},
"analyzer": {
"japanese_analyzer": {
"type": "custom",
"tokenizer": "kuromoji_tokenizer",
"char_filter": ["icu_normalizer"],
"filter": [
"katakana_readingform",
"katakana_stemmer",
"lowercase"
]
}
}
}
},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "japanese_analyzer"
},
"content": {
"type": "text",
"analyzer": "japanese_analyzer"
}
}
}
}
サンプルコードの scripts/create_index.py を実行するとローカルのOpenSearchにインデックスが作成されます。
設定項目
| 設定項目 | 役割 |
|---|---|
kuromoji_tokenizer |
日本語を形態素解析してトークン化 |
mode: "search" |
検索用のトークン分割(細かく分割) |
icu_normalizer |
Unicode正規化(全角半角の統一など) |
katakana_readingform |
漢字→カタカナ変換(表記揺れ吸収のキモ) |
katakana_stemmer |
語幹抽出(「走る」「走った」を統一) |
lowercase |
英数字の小文字化 |
各フィルターの役割
フィルターは記載順に適用されるため、順序が重要です:
-
char_filter(前処理)
-
icu_normalizer: 全角半角の統一、濁点の正規化など
-
-
tokenizer(トークン化)
-
kuromoji_tokenizer: 形態素解析
-
-
filter(後処理)
-
katakana_readingform: 読み仮名に変換(表記揺れ対策) -
katakana_stemmer: 語幹抽出 -
lowercase: 英数字の小文字化
-
アナライザーの動作確認
Analyze APIを使って、実際にどのようにトークン化されるか確認してみました。
curl -X POST "http://localhost:9200/blog_posts/_analyze?pretty" \
-H 'Content-Type: application/json' \
-d '{
"analyzer": "japanese_analyzer",
"text": "猫が好き"
}'
結果:
{
"tokens": [
{ "token": "ネコ", "start_offset": 0, "end_offset": 1 },
{ "token": "ガ", "start_offset": 1, "end_offset": 2 },
{ "token": "スキ", "start_offset": 2, "end_offset": 4 }
]
}
「猫」が「ネコ」に変換されています!
同様に、異なる表記でも試してみました:
# ひらがな
$ curl ... -d '{"analyzer": "japanese_analyzer", "text": "ねこが好き"}'
→ ["ネコ", "ガ", "スキ"]
# カタカナ
$ curl ... -d '{"analyzer": "japanese_analyzer", "text": "ネコが好き"}'
→ ["ネコ", "ガ", "スキ"]
すべて同じトークン列になっています。これで表記揺れを吸収できます。
検証
テストデータの準備
以下のようなサンプルデータを用意しています
blog_posts = [
{
"title": "我が家の猫について",
"content": "我が家には3匹の猫がいます。猫はとても可愛くて、毎日癒されています。猫の世話は大変ですが、それ以上に楽しいです。",
"author": "田中太郎",
"published_date": "2024-01-15",
"tags": ["ペット", "猫", "日常"]
},
{
"title": "ねこカフェに行ってきた",
"content": "初めてねこカフェに行ってきました。たくさんのねこたちと触れ合えて、とても楽しかったです。ねこ好きにはたまらない空間でした。",
"author": "佐藤花子",
"published_date": "2024-02-20",
"tags": ["カフェ", "ねこ", "お出かけ"]
},
{
"title": "ネコの健康管理",
"content": "ネコを飼う上で大切なのは健康管理です。定期的な健康診断や適切な食事が、ネコの長寿につながります。愛するネコのために、しっかりケアしましょう。",
"author": "鈴木一郎",
"published_date": "2024-03-10",
"tags": ["ペット", "健康", "ネコ"]
},
{
"title": "犬の散歩コース",
"content": "毎朝犬の散歩をしています。犬は散歩が大好きで、いつも尻尾を振って喜んでいます。近所の公園は犬の散歩に最適です。",
"author": "山田次郎",
"published_date": "2024-01-25",
"tags": ["ペット", "犬", "散歩"]
},
{
"title": "いぬの種類について",
"content": "いぬにはたくさんの種類があります。小型のいぬから大型のいぬまで、それぞれに個性があります。自分に合ういぬを見つけることが大切です。",
"author": "高橋美咲",
"published_date": "2024-02-05",
"tags": ["ペット", "いぬ", "種類"]
},
{
"title": "プログラミング学習の始め方",
"content": "プログラミングを学ぶには、まず基本的な概念を理解することが重要です。Pythonは初心者にもおすすめのプログラミング言語です。",
"author": "伊藤健太",
"published_date": "2024-03-01",
"tags": ["技術", "プログラミング", "学習"]
},
{
"title": "効率的なプログラム作成法",
"content": "良いプログラムを作るには、設計が重要です。プログラムの保守性を高めるため、コードの可読性に気をつけましょう。",
"author": "渡辺修",
"published_date": "2024-03-15",
"tags": ["技術", "プログラム", "開発"]
},
{
"title": "データベース設計の基本",
"content": "データベースの設計は、システム開発において非常に重要です。適切なデータベース設計により、パフォーマンスと保守性が向上します。",
"author": "小林さくら",
"published_date": "2024-02-28",
"tags": ["技術", "データベース", "設計"]
}
]
サンプルコードの以下のスクリプトを実行すると上記のデータが作成されます。
$ python3 scripts/insert_data.py
上記の中で猫に関するものだけをヒットさせます。
検索結果
「猫」(漢字)で検索
サンプルコードのスクリプトでの実行例
$ python3 scripts/search.py 猫
ヒット件数: 3件
[1] スコア: 2.02
タイトル: 我が家の猫について
ハイライト: 我が家の<em>猫</em>について
[2] スコア: 2.02
タイトル: ネコの健康管理
ハイライト: <em>ネコ</em>の健康管理
[3] スコア: 1.58
タイトル: ねこカフェに行ってきた
ハイライト: <em>ねこ</em>カフェに行ってきた
想定通り感じで検索した際もカタカナ/ひらがながヒットしています。
「ねこ」(ひらがな)で検索
$ python3 scripts/search.py ねこ
ヒット件数: 3件
- 我が家の猫について
- ネコの健康管理
- ねこカフェに行ってきた
「ネコ」(カタカナ)で検索
$ python3 scripts/search.py ネコ
ヒット件数: 3件
- 我が家の猫について
- ネコの健康管理
- ねこカフェに行ってきた
✅ すべての表記で同じ3件がヒットしています!
Dev Toolsで試しても同様の結果になっています。
GET blog_posts/_search
{
"query": {
"multi_match": {
"query": "ネコ",
"fields": ["title", "content"]
}
}
}
{
"took": 18,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 3,
"relation": "eq"
},
"max_score": 1.5143714,
"hits": [
{
"_index": "blog_posts",
"_id": "5WljeJoBheBdMtvPKRB6",
"_score": 1.5143714,
"_source": {
"title": "ねこカフェに行ってきた",
"content": "初めてねこカフェに行ってきました。たくさんのねこたちと触れ合えて、とても楽しかったです。ねこ好きにはたまらない空間でした。",
"author": "佐藤花子",
"published_date": "2024-02-20",
"tags": [
"カフェ",
"ねこ",
"お出かけ"
]
}
},
{
"_index": "blog_posts",
"_id": "5GljeJoBheBdMtvPKRBZ",
"_score": 1.4625832,
"_source": {
"title": "我が家の猫について",
"content": "我が家には3匹の猫がいます。猫はとても可愛くて、毎日癒されています。猫の世話は大変ですが、それ以上に楽しいです。",
"author": "田中太郎",
"published_date": "2024-01-15",
"tags": [
"ペット",
"猫",
"日常"
]
}
},
{
"_index": "blog_posts",
"_id": "5mljeJoBheBdMtvPKRCN",
"_score": 1.4142201,
"_source": {
"title": "ネコの健康管理",
"content": "ネコを飼う上で大切なのは健康管理です。定期的な健康診断や適切な食事が、ネコの長寿につながります。愛するネコのために、しっかりケアしましょう。",
"author": "鈴木一郎",
"published_date": "2024-03-10",
"tags": [
"ペット",
"健康",
"ネコ"
]
}
}
]
}
}
まとめ
OpenSearchのKuromojiプラグインを使って、日本語検索の表記揺れを自動的に吸収する方法を試してみました。
今回アナライザーの設定で完結したことで同義語辞書を管理する必要もなくシンプルな構成で実現することができました。既存のトークンを変換するだけなのでトークンが増えることもほぼなくパフォーマンスへの影響も少ないと考えています。
デメリットとしてはカタカナに変換する過程で同音異義語も同一視されてしまうため、もし同音異義語を区別したい場合は、別のアプローチ(辞書ベースの同義語管理など)が必要になります。
Discussion