elasticsearchで人名漢字の読みがな検索を行う



# TL;DR

漢字で保持しているユーザの名前をひらがな、カタカナでも検索できるようにします。

![はたらくを面白くするビジネスSNS・Wantedly_と_プロトタイピング導入がもたらす5つのメリット～UI_Crunch__3レポ（DeNA×グッドパッチ×クックパッド×スタンダード×グリー）_-_エンジニアtype.jpg](https://qiita-image-store.s3.amazonaws.com/0/4943/43283d27-2458-2a96-d274-ccbb7227a5f7.jpeg "はたらくを面白くするビジネスSNS・Wantedly_と_プロトタイピング導入がもたらす5つのメリット～UI_Crunch__3レポ（DeNA×グッドパッチ×クックパッド×スタンダード×グリー）_-_エンジニアtype.jpg")


# 使ってみる

以下のURLにアクセスして、上部のサーチボックスから検索が行えます。
ログインするとよりたくさんの結果が返ります。お試しください。

https://www.wantedly.com/projects


# 環境

- [Elasticsearch](http://www.elasticsearch.org/) v1.2.1
- [kuromoji for Elasticsearch](https://github.com/elasticsearch/elasticsearch-analysis-kuromoji) v2.2.0

 



# 方針

- `kuromoji_tokenizer`で形態素解析した単語の漢字部分を `kuromoji_readingform`(TokenFilter)でカタカナに変換してインデックスします。
- 変換のために別途人名辞書を作成し`kuromoji_tokenizer`のユーザ辞書に登録して漢字とカタカナの紐付けを行います。
- ユーザ辞書はトークンと読みがなを１対１でしか紐づけできないので、`word_delimiter`(TokenFilter)を活用して１対Nで紐づけできるようにします。

# 人名辞書（ユーザ辞書）

## ユーザ辞書の仕様


カンマ区切りのCSV形式ファイルで文字コードは`UTF8`です。
１つのトークンに対して、１つの読みがなを紐付けることができます。

### 仕様

```
<text>,<token 1> ... <token n>,<reading 1> ... <reading n>,<part-of-speech tag>
```

### 例 東京 (トークンが１つ）

```
東京,東京,トウキョウ,カスタム名詞
```

### 例 東京スカイツリー (トークンが２つ）

東京スカイツリーは、トークン`東京`, `スカイツリー`にわけられて、`東京`トークンの読みがなは`トウキョウ`と定義しています。

```
東京スカイツリー,東京 スカイツリー,トウキョウ スカイツリー,カスタム名詞
```

ファイル名は任意になります。

### 例

```
person.dic
```

## 辞書ファイルの配置場所

### OSX


elasticsearchは`brew`でインストールしました。
以下の`1.2.1`の部分は任意のバージョンになります。

```
/usr/local/Cellar/elasticsearch/1.2.1/config/person.dic
```

### Ubuntu

elasticsearchは`apt`でインストールしました。

```
/usr/local/etc/elasticsearch/person.dic
```


### Other Linux

```
/etc/elasticsearch/person.dic
```

## 人名辞書ファイルの作成

ファイル名をここでは`person.dic`としました。


`イマイ` ⇒ `今井`と読みがなを紐付けたい場合は以下のようになります。

```
$ ack '^今井' person.dic
今井,今井,イマイ,人名固有名詞
```

## 人名漢字に多様な読みがなを紐付ける

Kuromojiのユーザ辞書の仕様はトークンと読みがなが１対１で対応します。
`イマイ` ⇒ `今井`は記述できましたが、`カワサキ` `カワザキ` ⇒ `川崎` は記述できません。
以下のように記述すると辞書をロードしたときにエラーになります。


```NG
$ ack '^川崎' person.dic
川崎,川崎,カワサキ カワザキ,人名固有名詞
```
```NG
$ ack '^川崎' person.dic
川崎,川崎 川崎,カワサキ カワザキ,人名固有名詞
```

人名の漢字は多様な読み方をする場合が多いので対応は必須です。
以下の方法にて対応しました。

読みがなを任意のデミリター区切りで記述しました。
ここでは`%`にしました。

```
$ ack '^川崎' person.dic
川崎,川崎,カワサキ%カワザキ,人名固有名詞

$ ack '^山田' person.dic
山田,山田,ヤダ%ヤマタ%ヤマダ%ヨウダ%ヨマダ,人名固有名詞

$ ack '^井上' person.dic
井上,井上,イウエ%イカミ%イガミ%イナイ%イナエ%イネ%イネイ%イノウエ%イノエ,人名固有名詞
```

このままでは正しく検索できないので、`word_delimiter`(TokenFilter)でデミリター区切りを分割して、個々の読みがなとして切り出します。（ 詳細は後述 ）


# Index setting

```
{
  index: my_index,
  body: {
    settings: {
      analysis: {
        tokenizer: {
          # kuromojiにユーザ辞書を設定したトークナイザー
	      kuromoji_pserson_dic: { type: 'kuromoji_tokenizer', user_dictionary: 'person.dic' }
	    },
        filter: {
          # 読みがな変換用のフィルター
          katakana_readingform: {
            type: 'kuromoji_readingform',
            use_romaji: false
          },
          # デミリター区切りを分割するフィルター
          split_delimiter: {
            type: 'word_delimiter',
            generate_word_parts: true,
            generate_number_parts: false,
            catenate_words: false,
            catenate_numbers: false,
            catenate_all: false,
            split_on_case_change: false,
            preserve_original: false,
            split_on_numerics: false,
            stem_english_possessive: false
          }
        },
        analyzer: {
          # ユーザ辞書トークナイザーに読みがな変換用のフィルター、デミリター区切りを分割するフィルターを
          # 設定したアナライザー
          yomigana_analyzer: {
            type: 'custom',
            tokenizer: 'kuromoji_pserson_dic',
            filter: ['katakana_readingform', 'split_delimiter']
          }
        }
      }
    }
  }
}

```


## yomigana_analyzer

下記アナライザーにより後述のようにインデックスが行えるようになります。


```
  # ユーザ辞書トークナイザーに読みがな変換用のフィルター、デミリター区切りを分割するフィルターを
  # 設定したアナライザー
  yomigana_analyzer: {
    type: 'custom',
    tokenizer: 'kuromoji_pserson_dic',
    filter: ['katakana_readingform', 'split_delimiter']
  }
```


### 例　山田　花子

＜INPUT＞
`山田　花子`

↓

＜kuromoji_tokenizer＞
`山田`, `花子`

↓

＜katakana_readingform＞
`ヤダ%ヤマタ%ヤマダ`, `ハナコ%カコ`

↓

＜split_delimiter＞
`ヤダ`, `ヤマタ`, `ヤマダ`, `ハナコ`, `カコ`



## split_delimiter

`word_delimiter`は、文字列をオプションにより多様な方法で分割するフィルターです。

### オプション

#### `generate_word_parts` 
単語トークンを生成 "PowerShot" ⇒ "Power" "Shot" 
 
#### `generate_number_parts` 
数字トークンを生成 "500-42" ⇒ "500" "42"

#### `catenate_words` 
英字を連結したトークンを生成 "wi-fi" ⇒ "wifi"

#### `catenate_numbers` 
数値を連結したトークンを生成 "500-42" ⇒ "50042"

#### `catenate_all` 
単語を連結したトークンを生成 "wi-fi-4000" ⇒ "wifi4000"

#### `split_on_case_change` 
ケースセンシティブが出現した箇所で分割 "PowerShot" ⇒ "Power" "Shot" 

#### `preserve_original` 
オリジナルの文字列を含んでトークンを生成 "500-42" ⇒ "500-42" "500" "42"

#### `split_on_numerics` 
数字が出現した箇所で分割 "j2se" ⇒ "j" "2" "se"

#### `stem_english_possessive`
末尾の`s`"O’Neil’s" ⇒ "O", "Neil" 

デフォルトで`true`になっている項目もあるので、使わないものは明示的に`false`にしています。


refs [word delimiter token filter](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/analysis-word-delimiter-tokenfilter.html#analysis-word-delimiter-tokenfilter)

```
  # デミリター区切りを分割するフィルター
  split_delimiter: {
    type: 'word_delimiter',
    generate_word_parts: true,
    generate_number_parts: false,
    catenate_words: false,
    catenate_numbers: false,
    catenate_all: false,
    split_on_case_change: false,
    preserve_original: false,
    split_on_numerics: false,
    stem_english_possessive: false
  }
```


# Document mapping

```
{
  index: my_index,
  type: 'user',
  body: {
    user: {
      _id:        { path: 'id' },
      _timestamp: { enabled: true },
      _source:    { enabled: true },
      properties: {
      	 # 漢字の名前のアナライザーとして、yomigana_analyzer を設定します
        name_yomigana:   { type: 'string', store: true, index: 'analyzed', analyzer: 'yomigana_analyzer' }
      }
    }
  }        
}

```

# 検索文言の正規化


人名のユーザ辞書は読みがなをカタカナで保持しているので、検索文言がひらがなの場合は、カタカナに変換して検索する必要があります。


# 参考

- [実践！Elasticsearch - Wantedly Engineer Blog ](http://engineer.wantedly.com/2014/02/25/elasticsearch-at-wantedly-1.html)
- [Elasticsearch の Kuromoji でユーザー辞書を使う](http://qiita.com/kijtra/items/d391179e1ea0be65e3f5)

# 謝辞

- [elasticsearch.org](http://www.elasticsearch.org/)