日本語クエリオートコンプリーションの実現

## はじめに

スタンバイ一発目のアドベントカレンダーです。
現在スタンバイでは、クエリオートコンプリーション機能のリプレイスを行っており、その際にぶち当たった難しさや日本語クエリオートコンプリーション実現の考え方を書いています。
また、日本語のクエリオートコンプリーションに関しての記事が少なかったので一助になれたらいいなと思っています。



## 前提
Luceneを使った実現方法
実際のコードなどは書いていない

## クエリオートコンプリーションとは

百聞は一見にしかずということで↓この機能です。

検索窓に対して文字を入力するとその文字で始まるサジェストワードを一覧で出してくれる機能のことを言います。


![スクリーンショット_2022_11_26_23_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/fcb8e990-f9ef-a995-e1db-605ecf6955c6.png)



## どのようなことを実現したいのか

ではどのような文字列を受け取った時にどのような検索クエリ候補を期待するのかを具体的に一つずつ確認していきます。


#### 1. 入力文字列から始まる検索クエリ候補を出す


「看護師」と入力した時に「看護師」から始まる検索クエリ候補を一覧に出す。

![スクリーンショット_2022_11_26_23_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/89099562-5d69-f8de-9c28-e0f13a279482.png)



#### 2. 入力途中文字列から始まる検索クエリ候補を出す

「看護師」と完全な単語だけではなく、「看」のような入力途中の文字列に対しても「看護師」のような検索クエリ候補を出す。

![スクリーンショット_2022_11_26_23_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/ffc43fc5-7d13-1f3b-2ce6-2b7b58ec6363.png)



#### 3. ひらがな入力でも対応

「かんごし」と入力した時に「かんごし」から始まる検索クエリ候補だけではなく、漢字の「看護師」のような検索クエリ候補を出す。

日本語入力をする際にエンターを押さないと漢字変換されない、漢字変換される前のひらがなだけの入力でも検索クエリ候補を出してあげた方がユーザーにとって使いやすい。


![スクリーンショット_2022_11_26_23_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/297ca1e8-7490-4e99-0fee-455c58f5f5e5.png)


#### 4. ひらがな入力途中でも対応

「かん」でも「看護師」検索クエリ候補を出す。


![スタンバイ｜国内最大級の仕事・求人探しサイトなら.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/5cf24846-945f-e802-d438-34c2def44484.png)


#### 5. カタカナ入力でも

ひらがなだけではなく、カタカナにも対応する。

「カンゴシ」でも「看護師」検索クエリ候補を出す。

![スタンバイ｜国内最大級の仕事・求人探しサイトなら.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/288aac3f-acc4-b741-4167-dae26e5c7c97.png)


#### 6. カタカナ入力途中でも対応

「カン」だけでも「看護師」検索クエリ候補を出す。

![スタンバイ｜国内最大級の仕事・求人探しサイトなら.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/b839479f-e10d-c9f8-2a07-88bfe75f7502.png)


#### 7. ローマ字入力にも対応

「かんg」と入力した場合にも「看護師」といった検索クエリ候補を出す。

PCで入力する際にはこのような入力方法になるので対応する必要がある。

![スタンバイ｜国内最大級の仕事・求人探しサイトなら.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/f47dba32-22a0-9839-3439-999b7af72ee6.png)

#### 8. 複合語にも対応（入力文字列側）

ユーザーの入力クエリとしては「看護師」のような一単語だけではなく、「看護師　未経験」「看護師　パート」のような入力に対しても「看護師　未経験」「看護師　パート」のような2単語以上の複合語もクエリ候補として出す。

![スタンバイ｜国内最大級の仕事・求人探しサイトなら.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/545b1f35-5472-3097-9114-7d7e371bdaf4.png)


#### 9. 複合語の入力途中にも対応

複合語ももちろん入力途中にも対応する。

「看護師　未」に対しては、「看護師　未経験」

「看護師　パ」に対しては、「看護師　パート」のような検索クエリ候補を出す。

この時気をつける点としては、「看護　未」という入力に対しては、「看護師　未経験」は出さない。

![スタンバイ｜国内最大級の仕事・求人探しサイトなら.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/150144/598aafd1-d6f3-1a0c-7bd1-20c932c32d10.png)


以上、最低限クエリオートコンプリーションを実現するために考えないといけないことです。
では実際にどのようなデータを検索エンジンで持っておけばいいのか見ていきます。

## データの持ち方

インデックスと検索クエリをそれぞれで分けて考える必要があります。

### インデックスの構造

以下フィールドを前提に考える。
```shell
original_text・・・表示用テキスト
suggest_text・・・検索に使うためのフィールド
```

### 一単語の場合
ローマ字読みを取得して、元々の表示テキストと読みを一文字ずつ分割してインデックスする。

```shell
original_text: 
看護師

suggest_text: 
看
看護
看護師
k
ka
kan
kang
kango
kangos
kangosh
kangoshi
```

### 複合語の場合

「看護師　バイト」をindexに登録する場合

ローマ字読みを取得して、空白も含めて元々の表示テキストと読みを一単語ずつ分割してインデックスする。

```bash
original_text: 
看護師 バイト

suggest_text: 
看
看護
看護師
看護師　
看護師　バ
看護師　バイ
看護師　バイト
k
ka
kan
kang
kango
kangos
kangosh
kangoshi
kangoshi　
kangoshi　b
kangoshi　ba
kangoshi　bai
kangoshi　bait
kangoshi　baito
```

### 検索クエリの構造

### 一単語の場合

ローマ字読みを取得する

```shell
かん -> kan
看護 -> kanngo
```


### 複合語の場合

空白を含めてローマ字読みを取得する
```shell
看護師　ば -> kangoshi ba
```

## 検索する

以上で組み立てたindexと検索クエリを用いて検索するだけです。

検索クエリ: 「かん」の場合以下のクエリになり、suggest_textフィールドに検索をかけ見事ヒットします。

```shell
suggest_text: kan
```


## 日本語での実現の難しさ


以上でクエリオートコンプリーションの実現はできていますが、日本語特有の難しさと対策を書いています。

#### 複数読み漢字問題
日本語には同じ漢字でも音読み訓読みがあり、文脈や紐づく単語によって読みが違います。
例えば、
```txt
東日本 -> higashi
東京 -> tou
```

といった読みになります。

後述する日本語tokenizer kuromojiとtokenFilterを使ってローマ字読みを取得する際に、「東」は「higashi」の読みだけを取得されます。
これでクエリを組み立ててしまうと、「東」と入力した時に「東京」といったサジェストワードがヒットしなくなります。

#### 対策
##### 1. kuromojiのN-best解をいじる
「東」という単語だけでも複数の品詞・読み情報が辞書に登録されています。
その中からベストパス（生起コストと連接コストの総和が最小になる）として「higashi」読みを取得しています。
例えば、「東」でN-bestの10位までを出力すると以下のようになります。

```shell
東	名詞,一般,*,*,*,*,東,ヒガシ,ヒガシ
東	名詞,一般,*,*,*,*,東,アズマ,アズマ
東	名詞,固有名詞,人名,姓,*,*,東,アズマ,アズマ
東	名詞,固有名詞,一般,*,*,*,東,ヒガシ,ヒガシ
東	名詞,固有名詞,地域,一般,*,*,東,ヒガシ,ヒガシ
東	名詞,固有名詞,人名,姓,*,*,東,ヒガシ,ヒガシ
東	名詞,固有名詞,地域,一般,*,*,東,アズマ,アズマ
東	名詞,固有名詞,人名,名,*,*,東,ヒガシ,ヒガシ
```
この中のベストパス（一番上の行）からローマ字読みを取得していることになります。
ただこの方法だと、後述するtokenFilterの改修が必要になるので開発コストがかかります。

参考：
https://www.slideshare.net/techblogyahoo/17lucenesolr-solrjp-apache-lucene-solrnbest
https://analytics-note.xyz/programming/mecab-n-best/


##### 2. 入力クエリをそのまま検索に使う

「東」という入力クエリそのものも検索使うことで読み関係なく「東」から始まるサジェストワードも出すことができます。
クエリは↓のようになります。

```shell
suggest_text: higashi suggest_text: 東
```

こうすることで、「東」と入力した際には、「東日本」「東京タワー」といった、同じ漢字でも違う読みのサジェストワードを出すことが可能になります。


#### 複数ローマ字入力問題

ローマ字入力にも複数の入力パターンがあります。
例えば
```shell
し -> si, shi
ん -> n, nn
ふ -> hu, fu
しゃ -> sha, sya
```

例えば、kanngosiと打っても、kanngoshiと打っても「看護師」を出せるようにする必要があります。

#### 対策
後述するtokenFilterで複数ローマ字読みは取得できます。
あとはこの組み合わせを全て網羅するカスタムフィルターを作成するだけです。
例えば「看護師」の場合は、「ん」と「し」が複数入力パターンがあるので全て網羅してインデックスを作成します。
```shell
original_text: 
看護師

suggest_text: 
看
看護
看護師
k
ka
kan
kang
kango
kangos
kangosh
kangoshi
kangosi
kann
kanng
kanngo
kanngos
kanngosh
kanngoshi
kann
kanng
kanngo
kanngos
kanngosh
kanngoshi
kann
kanng
kanngo
kanngos
kanngosi
```



## 実現方法
以上のような機能を満たすためにLuceneのどのような機能を使っているか利用できる機能の紹介をしています。
基本的にはLuceneのcharFilter, tokenizer, tokenFilterを組み合わせてanalyzerを作成し、インデックス・検索クエリを組み立てるといった方法になります。

### tokenizer
#### JapaneseTokenizer

https://lucene.apache.org/core/9_3_0/analysis/kuromoji/org/apache/lucene/analysis/ja/JapaneseTokenizer.html

日本語形態素解析器（kuromoji）
デフォルトの辞書はipadicになっている


### tokenFilter

#### JapaneseCompletionFilter
カタカナ変換・複数ローマ字読み・長音符対応などしている

https://lucene.apache.org/core/9_3_0/analysis/kuromoji/org/apache/lucene/analysis/ja/JapaneseCompletionFilter.html

#### EdgeNgramTokenFilter
指定の間隔で単語を文字に分割する

https://lucene.apache.org/core/9_3_0/analysis/common/org/apache/lucene/analysis/ngram/NGramTokenFilter.html

#### 自前のフィルター

各ローマ字読みのパターンを結合させるなど、いくつか自前でtokenfilterを作成する必要があります。
その際には↓のTokenFilterを実装しAnalyzerに組み込むことで、インデックス時・クエリ作成時に、自前のtokenFilterを適応することができます。

https://lucene.apache.org/core/9_3_0/core/org/apache/lucene/analysis/TokenFilter.html

## その他考慮すること

以上である程度の品質でクエリオートコンプリーションを実現できます。
その他により質を上げるために考慮すべきことがあります。
例えば

- ランキング
- データ元
- データ元のETL処理（加工）

これらに関してはクエリオートコンプリーションで実現したいことによって様々な方法があり、この記事では割愛します。


## まとめ

日本語クエリオートコンプリーションを実現方法を書いてきました。
検索エンジンの理解がグッと深まり、日本語って難しいなって改めて感じる機会でした。
最後まで読んでいただきありがとうございました。