エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

怪談のためにElasticsearch入門した話

この記事はエムスリーAdvent Calendar 2020 24日目の記事です。

クリスマス付近の予定はこのアドカレ執筆だけ、エンジニアリングGのowlです。

ところでみなさんは怪談がお好きだと思います、もちろん自分も大好きです。常日頃から手当たり次第に読んでいるのですが、最近では読むものが少なくなってきたので中国古典の志怪小説や江戸時代以前に書かれた怪談集や説話集も集めています。いつか現代語訳されているものも読みつくしてしまったら外国語や古典籍のくずし字を解読する羽目になるかもしれません。

ちなみに怪談は好きですがホラー映画は苦手です、びっくりするので。

さて、大量の怪談を読むうちにその内容や語られ方が時代や地域によって大きく異なりそれぞれが興味深い特徴を持っていたり非常に似通った類話が存在することに気づきました。例えば江戸時代からいわゆる幽霊物の占める割合が急激に増える、現代の都市伝説的怪異の中には「色」をキーワードとするものが多い、日本である年代から突然「霊感」という概念が一般に広まる……など。

そこで少し真面目に怪談に関する考察をやってみようと思い立ち、その前準備としてなるべく怪談をデータベース化していくことにしました。

f:id:owl-m3:20201225064251p:plain

やりたいこと

もともと怪談のデータベース作成自体は行っておりSQLiteをデータベースとして保存していました。しかしこれを作成したのは7,8年前で自分がまだエンジニアですらなく、また当時の目的は単なる保存だったためデータベースといってもSQLiteへデータを流し込んだだけのものです。

そこで、まずはこのデータベースを検索や解析に適したものへ移行する作業を行います。取り急ぎ実現したかったことが5つありました。

  1. 高速な全文検索
  2. あいまい検索(類義語検索)
  3. 類似文書検索
  4. 自動タグ付け(重要語抽出)
  5. カテゴリ分類提案

これらをどう実現するかを調べてみたところ、今まで遠巻きに見ていたElasticsearchにきちんと向き合うべき時が来たのだとわかりました。基本的にどれも驚くほど簡単に実現出来ます、そうElasticsearchならね。このアプリを公開することはないので全てローカルにdocker-composeを使って構築します。

使う技術スタック

  • Elasticsearch
  • MySQL
  • Ruby on Rails
  • Redis

Web画面の操作や処理スクリプトのためにRuby on Railsを採用しています。Webアプリの方は何でもよく、自分が慣れていて考えずに組めるからというのが採用の理由です。またスクリプト処理などで揮発性のKVSが欲しかったためRedisも使っています。

細かい話をすると、Elasticsearchのプラグインとしてanalysis-kuromoji(日本語全文検索のため)、Railsのgemとしてelasticsearch-rails(ESをActiveRecordと繋ぐ)を利用しています。

アプリの構造はシンプルです。RailsからまずMySQLへ怪談データが投入され、手動あるいは操作にフックしてElasticsearchへ同期されます。Web画面からはElasticsearchを利用した各種検索(ElasticsearchのクエリをWeb画面から直接呼ぶこともできる)やデータ編集、同期処理などを行うことができます。

やりたいことのうち類似文書検索まではサクっと実装できましたが「重要語抽出」「カテゴリ分類」はいくつかの問題に対処中で未完成です。その詳細については後述します。

実装に関する短い説明

ざっくり言えば以下のようなコードを書くだけで基本的な機能は使えるようになりました。

  • elasticsearch-rails gemを利用してActiveRecordに対応するマッピング定義を追加
  • Elasticsearchへのインデックス初期化やデータ追加を行うための処理をバックグラウンドジョブとして実装
  • 利用したい各種のElasticsearchクエリを設計して実装

具体的なコードについては各gemやElasticsearchの基本的なドキュメントの通りなので省略しますが、自分はElasticsearchにタッチするのはほぼ初めて(プロダクトの一部では利用されているが触る機会がなかった)だったのでマッピング定義やクエリの書き方が主な躓きポイントでした。

「類話」の話

Elasticsearchは最初からMore Like This Queryという強力な類似文書検索の機能を提供しています。これを使うことで簡単に任意の文章と類似した文書を検索可能で、そのレスポンスには類似度スコアも含まれています。

そもそもこのアプリで類似文書検索が必要となる理由ですがそれは2つあります。まず類話を調べるためです。もっといえば類話の中でも特に「核心モチーフを共有する物語」を探すためです。

類話研究は神話や民話の分野でも研究テーマの一つとしてよく出てきます。例えば最も有名なヨーロッパの昔話であるシンデレラはその核心モチーフを共有する類話が登場人物やアイテム、一部ストーリーを変えて驚くほど離れた地域(古代エジプト、東アジア、南北アメリカなど)に存在します。

怪談でも同様にこういった核心モチーフを共有する物語が存在し、例えば「食べら(殺さ)れそうになった魚が人間に姿を変えて命乞いや警告をする話」は中国や日本で細部を様々に変えて語られるモチーフであり……しかし記事の本筋ではないので先に進みます。

もう一つの理由がむしろ重要で「重複する話を弾くため」です。収集している話が数千を超えてくるとどうやっても「ほぼ同じ話だがほんの微妙に語られ方が異なる」ような話が紛れ込んできますが、重複する話をデータベースに入れたくありません。

そこで、新しい話を入れる前に類似文書検索を行って、一定の閾値以上の類似度スコアを持つ話が既にある場合は重複と考えて弾きます。しかし重複する話は弾きたい一方で類話は収集したいので実際のデータでスコアを見ながら閾値を上手く設定する必要がありました。

それでも類話か同一話かを完全に自動で判断するのは難しいとわかったので、最終的に「ほぼ重複確定」と「重複の疑惑あり」でレベルを分けて後者は後から自分で確認してから判断することにしました。

実装後にさっそく全ての収集済み怪談に対して類似度スコアを調べて上位から比較して眺めていくと、やはり相当数の重複している話が見つかりました。それらの大半は削除しましたが、いくつかは核心モチーフを共有する別々の話というまさに探していたものでした。

また、想定外で興味深かったのが、同じ話でありながらも収集した時期と話者が違うと語られ方が変化している(重要でない部分が省略されるなど)ことがあり、怪談が変化して伝えられていく経過のようなものも見ることが出来ました。

重要語の抽出とテキスト分類

本当に実現したいのは怪談のテキスト分類なのですが、それには一つ致命的な問題があります。分類すべきカテゴリーがまだないことです。

そもそもElasticsearchなどでテキスト分類を行うには学習モデルを作る必要があり、ラベルのついた教師データが必要です。データセットの作成自体は労力さえかければ解決できますが、そもそも「怪談をどうやって分類するか」自体が研究テーマになるようなトピックで適切な分類方法というのが現状ありません(おそらく最も近い先行研究としては神話や民話では民話学者スティス・トンプソンによるモチーフの観点から見た分類方法がある)。

ですが、それにしても文書をカテゴリ分けしていく上でヒントになるようなものが欲しいところです。それもせっかくデータベース化するので自動的に生成できるようなものが。

そこで、まずは重要語の抽出から進めていくことを考えました。文章の中に存在する重要語を取り出して、それらからタグを作ることでカテゴリ分類のヒントにしようという目論見です。

Elasticsearchではある単語の共起語(頻繁にセットで出てくる単語)を取得したり、文章内で単語が登場回数をスコアリングすることは容易に可能ですが、ここで欲しい重要語とは単なる「頻繁に出てくる単語」ではなく「文章の特徴をよく表している単語やフレーズ」なのでElasticsearch単体ではちょっと解決が難しく、これまでの機能のようにちょっとコードを書くだけで魔法のように使えるようにはならなそうです。

といった経緯で、今はキーワード(あるいはキーフレーズ)を良い感じに自動抽出するためのアルゴリズムを探しながら試している状態です。WIP!

入門してみた感想

正直今までElasticsearchのことを高速で全文検索してくれるということ以外はよく知りませんでしたが、利用できる機能を調べてみると様々な機能を手軽に使える上に個別でチューニング可能な範囲が広いので驚きます。類似文書検索の高速さとお手軽さは感動ものでした。

このアプリも基本的な機能だけなら数時間で組みましたが、それでも重複した記事の検出など様々な頭の痛い問題を解決出来ました。一方でより理想的な処理結果を得るためには各種パラメータの地道なチューニング作業も必要であることがわかりました。

どうしても扱う対象によって最適な設定は異なり、例えば今回の怪談では非常にごく短い、わずか数行の記事も多かったためmin_term_freq(検索で考慮する最低限の単語出現回数)などを実際にクエリの実行結果を見ながらチューニングを行うなどしています。

しかし思ったよりもずっとお手軽にElasticsearchへ入門できたことに気を良くしたため、他にも何か活用法がないか探していきたいと思います。

We're hiring !

エムスリーでは技術を一緒に楽しめるエンジニアを募集しています。 社員とカジュアルにお話もできますので、興味を持たれた方は下記よりお問い合わせください。

jobs.m3.com