RailsとElasticsearchで検索機能をつくり色々試してみる - Synonym（類義語）編

## はじめに
[elasticsearch-rails](https://github.com/elastic/elasticsearch-rails/tree/master)を使うことが前提の記事になります。
記事の中で出てくるサンプルや環境は、[RailsとElasticsearchで検索機能をつくり色々試してみる - その１：サンプルアプリケーションの作成](https://qiita.com/yamashun/items/6ecaa6f161b4cf283db3)をもとにしています。

## やりたいこと
今回はSynonym（類義語）を扱っていきます。
Synonym（類義語）を登録しておくことで、例えば「ひろあか」と入力して「ヒーローアカデミア」を検索できるようにしていきます。

例）
「ひろあか」 → 「ヒーローアカデミア」
「てんすら」 → 「スライム」
「すらだん」 → 「スラムダンク」


## Synonymを登録する方法
ElasticsearchでSynonymを登録する方法は大きくは2種類あります。

- Synonymを定義したファイルを作成して、ファイルのパスをフィルターに定義する
- フィルターに直接Synonymの定義を登録する

これら二つの方法で登録する方法をサンプルコードを使って紹介していきます。
修正前の状態のコードをのせておきます。 [修正前のコード](https://github.com/yamashun/rails_es/tree/add_word_suggest)

## カスタムAnalyzerの登録
Synonymの登録ではAnalyzerにFilterを登録していくことになります。そのためビルドインの`kuromoji`ではなく独自に定義したAnalyzerを定義し、そのAnalyzerにFilterを追加していきます。
まずは検索処理用のConcernにAnalyzerを定義します。

```rb:app/models/concerns/manga_searchable.rb
module MangaSearchable
  extend ActiveSupport::Concern

  included do
    include Elasticsearch::Model

    index_name "es_manga_#{Rails.env}"

+   settings analysis: self.analyzer_settings do  # 追加したanalyzer_settingsメソッドでAnalyzerを登録する
      mappings dynamic: 'false' do
        indexes :id,                   type: 'integer'
        indexes :publisher,            type: 'keyword'
        indexes :author,               type: 'keyword'
        indexes :category,             type: 'text', analyzer: 'kuromoji'
+       indexes :title,                type: 'text', analyzer: 'custom_kuromoji' # 追加したAnalyzerを使う
+       indexes :description,          type: 'text', analyzer: 'custom_kuromoji' # 追加したAnalyzerを使う
      end
    end

　　・
　　・
　　・

  end

　　・
　　・
　　・

  class_methods do
　　・
　　・
　　・
    # Synonymを追加していくためのAnalyzerを定義。中身はビルドインのkuromojiをベースにしています
+   def analyzer_settings
+     {
+       analyzer: {
+         custom_kuromoji: {
+           type: 'custom',
+           char_filter: [],
+           tokenizer: "kuromoji_tokenizer",
+           filter: [
+             'kuromoji_baseform', 'kuromoji_part_of_speech',
+             'cjk_width', 'kuromoji_stemmer', 'lowercase'
+           ]
+         }
+       },
+     }
+   end
  end
end

```

## ファイルを使ったSynonymの登録
準備ができたのでここからSynonymの話です。まずはファイルを使った登録を行っていきます。

```text:files/synonym.txt
ひろあか,ヒロアカ => ヒーローアカデミア
てんすら,テンスラ => スライム
```

今回は`=>`を使った左側の単語を右側の単語に置き換える書き方にしていますが、`ひろあか,ヒロアカ,ヒーローアカデミア`のようにカンマ区切りでグルーピングする方法もあります。

ファイルができたら、Elasticsearchが読み込めるようにファイルを配備して、さきほどのAnalyzerを修正していきます。

### ファイルの配備
さきほど作成したファイルをコンテナ内にコピーします。

```sh
$ docker cp files/synonym.txt  elasticsearch_sample:/usr/share/elasticsearch/config/synonym.txt
# コピーしたファイルは必要に応じてElasticsearchから参照できるようにオーナーやパーミションを修正
```

### Analyzerの修正
さきほど配備したsynonym.txtを使ったfilter(manga_synonym)を定義して、それをAnalyzer(custom_kuromoji)のfilterに追加します。

```rb:app/models/concerns/manga_searchable.rb
module MangaSearchable

　　・
　　・
　　・

    def analyzer_settings
      {
        analyzer: {
          custom_kuromoji: {
            type: 'custom',
            char_filter: [],
            tokenizer: "kuromoji_tokenizer",
            filter: [
+             'manga_synonym', # 追加したfilterを先頭に追加
              'kuromoji_baseform', 'kuromoji_part_of_speech',
              'cjk_width', 'kuromoji_stemmer', 'lowercase'
            ]
          }
        },
+       filter: { # synonym用のfilterを追加
+         manga_synonym: {
+           type: 'synonym',
+           synonyms_path: 'synonym.txt',
+         }
+       }
      }
    end
  end
end
```

定義が完了したのでindexを再作成、データを入れ直します。

```sh
$ docker exec -it rails_es_rails_1 /bin/bash
/app# bundle exec rake elasticsearch:create_index
/app# bundle exec rake elasticsearch:import_manga_all
```

これで「ひろあか」で検索すると「ヒーローアカデミア」で検索した結果と同じ結果が得られると思います。


## フィルターに直接Synonymを定義
次にフィルターに直接定義する方法を見ていきます。

### Analyzerの修正
filter(manga_synonym_2)を定義し、その中で直接Synonymを定義します。定義したfilterをAnalyzer(custom_kuromoji)のfilterに追加します。

```ruby:app/models/concerns/manga_searchable.rb
module MangaSearchable

　　・
　　・
　　・

    def analyzer_settings
      {
        analyzer: {
          custom_kuromoji: {
            type: 'custom',
            char_filter: [],
            tokenizer: "kuromoji_tokenizer",
            filter: [
              'manga_synonym', 
+             'manga_synonym_2', # 新たにfilterを追加
              'kuromoji_baseform', 'kuromoji_part_of_speech',
              'cjk_width', 'kuromoji_stemmer', 'lowercase'
            ]
          }
        },
        filter: {
          manga_synonym: {
            type: 'synonym',
            synonyms_path: 'synonym.txt',
          },
+         manga_synonym_2: { # filterを追加
+           type: 'synonym',
+           synonyms: [ # synonymsに直接登録していく
+             'すらだん => スラムダンク',
+           ]
+         },
        }
      }
    end
  end
end
```

定義が完了したのでindexを再作成、データを入れ直します。

```sh
$ docker exec -it rails_es_rails_1 /bin/bash
/app# bundle exec rake elasticsearch:create_index
/app# bundle exec rake elasticsearch:import_manga_all
```

これで「すらだん」で検索すると「スラムダンク」で検索した結果と同じ結果が得られると思います。


## まとめ
elasticsearch-railsを使ってRailsでSynonymを使う方法を紹介しました。（[今回の修正内容](https://github.com/yamashun/rails_es/pull/4/files)になります）
どちらの方法もわりと簡単に登録できて便利ですね。単語の数が増えてくるとメンテナンスの方法などは工夫が必要になってくると思いますが、検索の精度を上げるためにも積極的に使っていきたいところです。


## 参考
[Custom Analyzer \| Elasticsearch Reference [7.3] | Elastic](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-custom-analyzer.html)
[Amazon Elasticsearch Service で類義語（Synonym）を扱う ｜ DevelopersIO](https://dev.classmethod.jp/cloud/aws/synonym-in-amazon-elasticsearch-service/)