概要
2万枚以上の勉強に関する画像から、手元にある問題集の写真で同じ問題の画像を検索できる機能をリリースしました。
その実装方法の話をします。
TL;DR
- OCRと全文検索の組み合わせを使えば、文字量が多い画像の画像検索ができる
-
Cloud Vision APIの日本語解析の性能はいい感じ -
Algoliaはパラメータの調整によって「長文で長文を検索する」ユースケースにも対応できる -
AlgoliaとLaravelの連携は、Eloquent Modelを使っているならめっちゃ便利
できあがったもの
先に完成した画像検索機能をお見せします。
検索する画像はこちらです
よくつまづきがちな確率の問題です。懐かしいですね。

0.画像検索ページを開く
悲報:画像検索機能は撤退することになり削除されました。
画像検索機能はこちらのURLから利用できるので、手元に手頃な問題集がある方(?)は使ってみてください。
3.検索結果の一番目に出てきた質問は、検索した画像と同じ問題です

本記事では、この画像検索機能を開発するために活用したCloud Vision APIやAlgoliaなどの概要と、その組み合わせ方、ハマったポイントなどについてお話します。
背景
筆者について
勉強質問サイトNoSchoolを運営する株式会社NoSchoolでCTOをしています。
社員が僕と社長しか居ないので、理論上最小のCTOとして毎日一人でもくもく開発しています。
毎日中高生が「対数関数」や「現在完了形」といった勉強に関する質問をしていて、それに家庭教師や塾の先生が高品質な回答をするというサービスを運営しています。
https://noschool.asia/questions
開発のきっかけ
毎日質問と回答が溜まっていくわけなので、いずれは質問しようとした瞬間に、過去の質問と一致していたり、近いものがあるならばそれをレコメンドするという世界観だって作れるはずです。
また、大半の勉強の質問は問題集の写真や教科書の写真とともに投稿されています。
ということは、手元の問題集の写真を撮影しNoSchoolにアップロードすると、質問の本文を打たなくても過去の質問から類似したものが返ってくるというものができたらいいのでは、と思い立ち、今回の画像検索開発をすることになりました。
技術選定
僕は日頃からWeb開発に注力しており、スキルマップとしてはAWS、Firebase、Laravel、Nuxt.jsあたりを触っています。機械学習に関してはなんとなく勉強したことはあるものの、自分でモデルを学習させるようなことはほとんどやったことがないです。
画像で画像を検索する、となると何から始めていいかさっぱりでした。
特徴量算出は微妙かも
画像の特徴量を算出するAKAZEやORBといったアルゴリズムは存在するし、一般にはかなり高精度で似ている画像を判別できるのですが、そもそも数万枚以上の画像から近い特徴量の画像を探すというのは別の技術が必要になるというのと、ここでの「似ている」というのは勉強の質問を検索する際に必要な「似ている」とはちょっと違うなと思いました。
(手元で軽くPythonでAKAZE/ORBで特徴量を出して検索できるスクリプトを書いて実行したが、良いような悪いようななんともいえない感じでした。なにしろ勉強の質問の画像は基本的にくすんだ紙に文字がざっと印刷されているだけなのです。素人の僕がこれ以上のめり込んでも短期間では成果を出せない可能性が高いと判断しました)
というのも、先程の例では全く同じ質問がヒットしましたが、全く同じ質問が弊社のデータベース内にあることを保証できるわけでは有りません。
全く同じ質問が無いときは、理想で言えばその質問と同じ単元の質問くらいを出してやりたいところです。
そうすれば、察しのいい学生であれば、この問題のここの数値を変えれば自分のわからない質問に応用できるかも、となる可能性に期待できます。
OCR✕全文検索はどうか
同じ単元の質問を複数枚の画像から探すには、画像が似ているかどうかも重要ですが、画像の中に含まれている文字がどれくらい近いのか、というのも重要だというように考えました。
勉強の質問というのは、「還元剤」とか「平方根」とか珍しい言葉が多く入っています。「還元式」が入っていたらほぼ化学ですし、化学の中でも酸化と還元を扱っている単元である可能性が高いです。

ここまで考えて、「OCR」と「全文検索」の組み合わせというアイデアが浮かびました。
そもそも画像検索というのは「画像解析」と「大量の解析後データから検索する技術」に分解されるわけで、まともに特徴量を求めるやり方だと前者も後者も難しい上に勉強の画像で成果が出るか微妙、という懸念がありました。
OCRはCloud Vision APIを始めとして各種クラウド等からいい感じに日本語対応もしたAPIが色々と出ていますし、全文検索は最近巷でよく見るAlgoliaを思考停止で使えばいいのではと思い、この方法なら技術的にも実現できそうだし、勉強の質問という特殊な画像データにおいて検索できる可能性が少しでも高いのではないか、と考えました。
Cloud Vision APIの採用
AWSのRekognitionはたしか日本語に対応しておらず、精度も全然だったのですが、GoogleのCloud Vision APIは最初使ったとき震え上がったほど精度が良かったので採用しました。
Azureも精度が高いという話は聞いているのですが、Cloud Vision APIでそこまで困らなさそうだった&Firebaseを自社で使っているので既にGCPにクレカが登録されていて楽だった()という理由でCloud Vision APIにしました。
時間を作ってAzureも検証してみたいです。
Algoliaの採用
全文検索でAlgolia以外を知らなかったですし、ものは試しと使ってみるとLaravelとのIntegrationもあるというので、採用しました。他の選択肢をご存知の方がいればぜひ教えて下さい!
実現方法
ということでCloud Vision APIとAlgoliaを採用したので実現方法の話に移ります。
データの生成
過去画像を全部OCRしてデータベースに突っ込む
まずはサクッと画像をCloud Vision APIに投げて結果をデータベースに投げるLaravel Commandを書いて実行しました。
データベースになぜあえて入れるのかは、後述のAlgoliaとのIntegrationに便利だからです。また、質問データとリレーションをModelで張っておくことで、OCRしたテキストの他の活用も望めます。
Laravel Scout
LaravelにはScoutという、全文検索エンジンとEloquentモデルを同期してくれるライブラリをサポートしています。
https://readouble.com/laravel/5.7/ja/scout.html
LaravelのEloquent ModelでSearchableトレイトをuseし、あとはドキュメントに書いてあるとおり、Algoliaに投げるパラメーターを指定します。
このModelでは先程OCRしたテキストも参照できるようにしておきます。
僕はQuestionImageという質問添付画像のModelに対してSearchableトレイトをuseして統合し、OCR後のテキストをdetected_textとして扱うように設定しています。
そしてtoSearchableArrayメソッドを実装し、その中でAlgoliaに投げたいデータをreturnすればいいです。
OCRが失敗する画像も中にはあるので、失敗してnullになっている画像はAlgoliaに投げないとか、Algoliaに投げれるデータは1レコード当たり10,000バイトなので、OCR後のテキストが10,000バイトを超えたらそこまでmb_strcutするように実装するなど工夫しています。
実装が終わったら下記コマンドを実行します。
php artisan scout:import "App\{モデル名}"
実行中にAlgoliaのコンソールにいけば、データが次々入っていることを確認できるでしょう。
データの検索
検索用のAPI
冒頭で示したようなUXを想定しているので、画像を投げたらその画像で過去の質問の画像を検索し、ヒットした質問一覧を返すようなAPIを実装しました。
弊社はレイヤードアーキテクチャをなんとなく採用しているので、画像をアップロードする処理と、アップロードした画像のURLを受け取って解析する処理、最後にテキストでAlgoliaから検索する処理を分離して、例えば将来的にネイティブアプリからML KitですでにOCRされたテキストが渡ってくるようなユースケースにも対応可能にしておきました。
Algoliaの中からデータを探すときも、Model->searchメソッドで検索できるので、Scoutは本当に便利です。
$searchResult = QuestionImage::search(
$detectedText,
// Algoliaの検索オプション指定
function ($algolia, $query, $options) use ($subjectId) {
$options['getRankingInfo'] = true;
// 中略
return $algolia->search($query, $options);
}
);
Algoliaの検索パラメータ
実はここが一番肝なのですが、Algoliaの一般的なユースケースは数個の単語で全文検索をする場面です。
しかし画像で画像を探す場合、ユーザーの画像もOCRした上でそのテキストで検索するため、【長文で長文を検索する】ことになります。
この場合、独自でAlgoliaの検索パラメータを調整する必要があります。
また、長文での検索を許可する場合、あまり長い文章で検索しても意味がなくなってくるので、適度に分割して複数回検索し、それぞれの検索結果から、もっとも似ていると思われる質問の画像を独自に推論していく必要が出てきました。
最終的に決定してリリースしたパラメータは検証の末に調整したものなので、ここに貼るのはやめておきますが、もし同じく長文で長文を検索する状況になった方は、パラメータのドキュメントを読み込んで何度も検証することをおすすめします。
パラメータを決定したら、上記のソースコードで「中略」と書かれているところで指定してくと反映できます。
検索結果の活用
Algoliaは検索後に戻ってくるJSONにも便利な情報が入っていて、どれくらい文字列間に距離があったか(おそらくハミング距離のようなもの)が含まれています。
これを自社の画像で何度か検索してその結果と距離を目視で照らし合わせると、だいたい距離がどれくらいの数値を下回っていたら同じ画像かというのがわかってきます。
そこで、クライアントサイドにAPIの戻り値として最も小さい距離を返してあげることで、「検索結果内におそらく望ましい画像がないだろう」という前提に立った処理が実装可能です。
事実、今回のリリースでは検索結果が芳しくないときに、「NoSchool内には良い画像がないので、質問しよう」という質問画面への導線を出現させています。
まとめ
- OCRと全文検索の組み合わせを使えば、文字量が多い画像の画像検索ができる
-
Cloud Vision APIの日本語解析の性能はいい感じ -
Algoliaはパラメータの調整によって「長文で長文を検索する」ユースケースにも対応できる -
AlgoliaとLaravelの連携は、Eloquent Modelを使っているならめっちゃ便利
ちなみに実装期間はフロントエンドの実装まで全て含めてだいたい3週間弱です。
最後に
もしよかったら Twitter (https://twitter.com/Meijin_garden )もフォローお願いします!

