REQLY開発ブログ

プロの料理レシピ専門検索エンジンREQLY(レクリー)の開発ブログです。

形態素解析とシノニム変換による検索エンジンの改善

はじめに

REQLYは、プロの料理レシピ専門の高機能検索エンジンとして、人とレシピとのより良いマッチングを実現することを目指しています。しかしながら従来のREQLYには、以下の2つの大きな問題がありました。

  1. 料理レシピ特有の単語が適切に検索できない場合がある。
    • 例えば「火鍋」で検索したときに、「火鍋」で一単語と認識できず、「火」と「鍋」を別々に含むレシピにもマッチしてしまう。
  2. 同じものをさす別の単語で検索した場合で、異なる検索結果が表示される。
    • 例えば「プチトマト」と「ミニトマト」、「インゲン」と「いんげん」など。

このように一部の検索ワードの意味解釈に失敗する場合があり、これではお世辞にも"高機能"とは呼べません。過去記事で触れたように、REQLYはバックエンドの検索エンジンとしてElasticsearchを利用しています。最新のβリリースでは上記の問題点を改善するためにElasticsearchのチューニングを行いました。ここではREQLYの検索結果がどのように改善されたかを紹介いたします。

reqly.tokyo

検索エンジンの流れ

まずは検索エンジンが一般的にどのような仕組みで動いているのかを見ておきましょう。検索エンジンは入力された検索ワードをどのように処理して検索結果を返しているのでしょうか?

1. 完全一致検索

はじめは単純に、検索ワードを全文検索して完全一致したレシピを返す場合を考えてみましょう。この方法でもある程度は上手くいきますが、求めている結果の一部しか抽出されないという問題点があります。例えば鶏むね肉を使ったレシピが知りたくて「鶏むね」と検索したとします。レシピによって、鶏むね肉の表記は「鶏胸肉」「鶏むね肉」「鶏肉(むね)」と様々ですが、「鶏むね」という一語の単語に完全一致するかどうかで検索してしまうと、「鶏むね」という一続きの文字列が含まれているレシピしかヒットしなくなってしまいます。 f:id:reqly-tokyo:20180728202405j:plain

2. 形態素解析

そこで「鶏」と「むね」が文中で離れているようなレシピに対しても、「鶏むね」でヒットするように改善したいと思います。このとき、「鶏むね」という単語を「鶏 + むね」に分割して、両方を含むようなレシピを検索すれば、単語が離れているような場合にも検索ができるようになるでしょう。

f:id:reqly-tokyo:20180728192749j:plain

このような部分単位は形態素と呼ばれ、形態素に基づいて文を分割することを形態素解析と呼びます。

3. シノニム変換

形態素解析を行ったとしても、「鶏むね」という検索ワードでは「とり胸肉」にヒットしません。この問題に対処するためには「鶏」と「とり」、「胸」と「むね」が意味上は同じものを指していることを踏まえ、それらを検索上で同一視する必要があります。こうした関係性は類義語(シノニム)と呼ぶことができます。

f:id:reqly-tokyo:20180728192759j:plain


このように、形態素解析シノニム変換の2つを正しく組み込むことで、検索ワード(= 鶏もも)に込められたユーザーの意図を理解し、適切な検索結果(= 鶏もも肉を使ったレシピ)を提供することが可能になります。Elasticsearchでは、インデックスを貼る際にプラグインを導入したり、設定ファイルを書き換えたりすることで、検索の挙動を変更することができます。以下では、行ったチューニングの内容と、それにより検索結果がどう変化したのかの具体例を紹介します。

施策

1. 食材名/一部レシピ名の辞書登録

Elasticsearchのプラグインとして、新語・固有表現に強いと言われているmecab-ipadic-neologdをベースとなる辞書として利用し、形態素解析を行っています。これに加えて、REQLY内部で独自に定義している食材オントロジーおよびマニュアル操作によって、その辞書に登録されていない単語を追加して、検索する際の分かち書きをより正確にしました。

github.com

例えば「火鍋」は中国の伝統的な鍋料理であるが、これがElasticsearchの辞書に登録されていないことによって、「火」と「鍋」に形態素解析され、「火」および「鍋」を使う多くの料理にヒットしてしまいます。これでは、火鍋に到達することができません。そこで、「火鍋」を一語の単語として新規に登録することにより、この問題を回避しました。

f:id:reqly-tokyo:20180721215846p:plain

火鍋では、誤検出を減らすことに成功しました。

2. シノニムの登録

最初に示した「プチトマト」や「ミニトマト」、その他にも「ジャガイモ」と「ポテト」のように、本来は同一のものをさす単語であっても、異なる単語であると認識されて別々にインデキシングされてしまうことにより、単語毎に検索結果が異なるという現象が起きてしまいます。

こうしたものは類義語(シノニム)の関係にあります。これらをElasticsearchに登録することで、あるシノニムに含まれる単語がインデックスされている際に、そのシノニムのどの単語をキーワードに含めた検索に対しても、共通のシノニムを含むドキュメントが検索でヒットするように変更することができます。

例えば「ミニトマト」と「プチトマト」はさす対象を同一視しても問題ないと考えられますが、今までは別々に検索されていました。そこで、その2つをシノニムにまとめることで、どちらの単語で比較しても同じ結果が返ってくるようになりました。

f:id:reqly-tokyo:20180724135510p:plain

一回の検索で、「ミニトマト または プチトマト」の結果が得られるようになったことで、どちらの単語で検索した場合でもレシピ数が増加しました。全体の件数が減っているように見えるのは、レシピ内に「ミニトマト」と「プチトマト」の表記が混在しているものがあるためです。

3. Exclude 検索の実装

形態素解析を行うことで、「カツオ節」が「カツオ+節」と分かち書きされるように、辞書に登録された単語は形態素に分解された形で索引されるようになります。ところが、「カツオ」を使った料理を検索しようとした場合、カツオ節を使った料理がたくさん表示されると興ざめしないでしょうか。これに対する対処の方法として、

  1. 料理に関する単語は、形態素解析しないようにする。
  2. 全ての単語は形態素解析して、料理に関する単語では検索結果から、除外したい単語を含むレシピを除外するようにする。

この2通りのやり方が考えられますが、1. の方法はElasticsearchのmecab-ipadicの辞書を使うことができなくなるほか、恐らくElasticsearchのプラグインとして、単語ごとの場合分けの処理をする必要があるでしょう。どちらにしても、2. の方法よりシンプルではなくなります。Elasticsearchでは、除外検索を行うことができるので、除外する単語のリストを予め連想配列のようなオブジェクトで持っておいて、ある単語が検索ワードに含まれているとき、その単語のリストを自動的に除外対象に加えるような実装を行うことで、この問題に対処できるでしょう。

{"カツオ": ["鰹節", "カツオ節"]}

上記のように、「カツオ」を検索ワードとして入力する場合、自動で鰹節やカツオ節の結果を排除するようにしました。これにより、かつおを削ったものとしてではなく、主要な食材として利用するレシピが表示されるようになりました。

f:id:reqly-tokyo:20180717234828p:plain

カツオでは、カツオ節を取り除いたことにより、検索の精度が向上しました。

4. Alternative 検索の実装

鱈(タラ)を利用したレシピを検索したときに、「たら」という文字列で検索しようとすると、「〜したら」という単語にマッチしてしまいます。(なぜならば、〜したらという単語は動詞「する」の未然形「し」と、接続助詞「たら」に形態素解析できるためです)。

今まではこうした検索ワードは除外していたので検索結果では表示されませんでしたが、REQLYで「たら」という単語を検索する際に、接続助詞を検索したいという需要はないと考えられますので、食材としての「タラ」ないし「鱈」として検索するように、内部で変換を行うようにすることで、表示されるようになりました*1

ただし、「もも」のような場合では、果物の「桃」の場合と「鶏もも肉」のように肉の部位をさす場合で利用されることから、変換を行っておりません。果物の桃を利用したレシピを検索したい場合は、キーワード「桃」をお試し下さい。

まとめ

REQLYの検索エンジンはまだまだ改善を続けていきます。皆さまの食卓を彩る手料理のお供に、REQLYをぜひご利用下さい!

付録

参考として、Elasticsearchの設定ファイルでどのように辞書を登録したのかについて記載します。

{
  "settings": {
    "index": {
      "analysis": {
        "tokenizer": {
          "ja_tokenizer": {
            "type": "reloadable_kuromoji_neologd_tokenizer",
            "mode": "search",
            "user_dictionary": "userdict_ja.txt"
          }
        },
        "filter": {
          "ja_synonym": {
            "type": "synonym",
            "synonyms_path": "synonym.csv"
          }
        },
        "analyzer": {
          "analysis": {
            "type": "custom",
            "tokenizer": "ja_tokenizer",
            "char_filter": [
              "icu_normalizer",
              "kuromoji_neologd_iteration_mark"
            ],
            "filter": [
              "kuromoji_neologd_stemmer",
              "kuromoji_neologd_part_of_speech",
              "ja_stop",
              "kuromoji_number",
              "ja_synonym"
            ]
          }
        }
      }
    }
  }
}

userdict_ja.txt, synonym.csv は前もって、elasticsearchで指定している環境変数ES_HOME 以下のディレクトリに置いておきます。

*1:ただし、この場合はレシピの文章中に食材としての「たら」が記載されている場合は、どのみち検索結果からヒットすることはできません。こうした品詞による検索の改善は、今後の課題といえるでしょう