2万枚以上から探せる画像検索をCloud Vision APIとAlgoliaを使って開発した話

# 概要
**2万枚以上の勉強に関する画像から、手元にある問題集の写真で同じ問題の画像を検索できる機能**をリリースしました。
その実装方法の話をします。

# TL;DR
- OCRと全文検索の組み合わせを使えば、文字量が多い画像の画像検索ができる
- `Cloud Vision API`の日本語解析の性能はいい感じ
- `Algolia`はパラメータの調整によって「長文で長文を検索する」ユースケースにも対応できる
- `Algolia`と`Laravel`の連携は、Eloquent Modelを使っているならめっちゃ便利

# できあがったもの
先に完成した画像検索機能をお見せします。

**検索する画像はこちらです**
よくつまづきがちな確率の問題です。懐かしいですね。
![iOS の画像 (6).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/3d75dbda-8b4c-4c34-245a-43712d9cf31e.jpeg)

**０．画像検索ページを開く**

> 悲報：画像検索機能は撤退することになり削除されました。

~~画像検索機能はこちらのURLから利用できるので、手元に手頃な問題集がある方（？）は使ってみてください。~~


**１．まずは問題の科目を選択します**
![スクリーンショット 2019-09-25 17.14.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/2cbc2adc-799b-7781-c8ef-dc50b213b732.png)

**２．次に検索したい画像をアップロードします**
![スクリーンショット 2019-09-25 17.14.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/5bc38672-59f8-d251-60b6-cf944591fc09.png)

**３．検索結果の一番目に出てきた質問は、検索した画像と同じ問題です**
![スクリーンショット 2019-09-25 17.20.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/5c4b8634-8e7e-7489-cc5c-6c539d6b03d9.png)

[ヒットした質問の詳細ページはこちらです](https://noschool.asia/question/%e9%9d%92%e3%83%81%e3%83%a3%e3%83%bc%e3%83%88%e7%8b%ac%e7%ab%8b%e8%a9%a6%e8%a1%8c)

本記事では、この画像検索機能を開発するために活用した`Cloud Vision API`や`Algolia`などの概要と、その組み合わせ方、ハマったポイントなどについてお話します。

# 背景
## 筆者について
勉強質問サイト[NoSchool](https://noschool.asia/)を運営する株式会社NoSchoolでCTOをしています。
社員が僕と社長しか居ないので、*理論上最小のCTO*として毎日一人でもくもく開発しています。
毎日中高生が「対数関数」や「現在完了形」といった勉強に関する質問をしていて、それに家庭教師や塾の先生が高品質な回答をするというサービスを運営しています。
https://noschool.asia/questions

## 開発のきっかけ
毎日質問と回答が溜まっていくわけなので、いずれは質問しようとした瞬間に、過去の質問と一致していたり、近いものがあるならばそれをレコメンドするという世界観だって作れるはずです。
また、**大半の勉強の質問は問題集の写真や教科書の写真とともに投稿されています**。

ということは、手元の問題集の写真を撮影しNoSchoolにアップロードすると、**質問の本文を打たなくても過去の質問から類似したものが返ってくる**というものができたらいいのでは、と思い立ち、今回の画像検索開発をすることになりました。

# 技術選定
僕は日頃からWeb開発に注力しており、スキルマップとしては`AWS`、`Firebase`、`Laravel`、`Nuxt.js`あたりを触っています。機械学習に関してはなんとなく勉強したことはあるものの、自分でモデルを学習させるようなことはほとんどやったことがないです。
画像で画像を検索する、となると何から始めていいかさっぱりでした。

## 特徴量算出は微妙かも

画像の特徴量を算出する`AKAZE`や`ORB`といったアルゴリズムは存在するし、一般にはかなり高精度で似ている画像を判別できるのですが、そもそも数万枚以上の画像から近い特徴量の画像を探すというのは別の技術が必要になるというのと、ここでの「似ている」というのは勉強の質問を検索する際に必要な「似ている」とはちょっと違うなと思いました。
（手元で軽く`Python`で`AKAZE/ORB`で特徴量を出して検索できるスクリプトを書いて実行したが、良いような悪いようななんともいえない感じでした。なにしろ**勉強の質問の画像は基本的にくすんだ紙に文字がざっと印刷されているだけ**なのです。素人の僕がこれ以上のめり込んでも短期間では成果を出せない可能性が高いと判断しました）


-  [Python + OpenCVで画像の類似度を求める](https://qiita.com/best_not_best/items/c9497ffb5240622ede01)
-  [AKAZE特徴量による画像マッチング](https://tat-pytone.hatenablog.com/entry/2019/03/31/150449)

というのも、先程の例では全く同じ質問がヒットしましたが、全く同じ質問が弊社のデータベース内にあることを保証できるわけでは有りません。
**全く同じ質問が無いときは、理想で言えばその質問と同じ単元の質問くらいを出してやりたい**ところです。
そうすれば、察しのいい学生であれば、この問題のここの数値を変えれば自分のわからない質問に応用できるかも、となる可能性に期待できます。

## OCR✕全文検索はどうか

同じ単元の質問を複数枚の画像から探すには、画像が似ているかどうかも重要ですが、画像の中に含まれている文字がどれくらい近いのか、というのも重要だというように考えました。

**勉強の質問というのは、「還元剤」とか「平方根」とか珍しい言葉が多く入っています**。「還元式」が入っていたらほぼ化学ですし、化学の中でも酸化と還元を扱っている単元である可能性が高いです。
![還元剤.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/156567/2a586ff9-5b54-3940-da88-4c901205ccb6.jpeg)


ここまで考えて、**「OCR」と「全文検索」の組み合わせ**というアイデアが浮かびました。

そもそも**画像検索というのは「画像解析」と「大量の解析後データから検索する技術」に分解される**わけで、まともに特徴量を求めるやり方だと前者も後者も難しい上に勉強の画像で成果が出るか微妙、という懸念がありました。
OCRは[Cloud Vision API](https://cloud.google.com/vision/?hl=ja#vision-api-)を始めとして各種クラウド等からいい感じに日本語対応もしたAPIが色々と出ていますし、全文検索は最近巷でよく見る[Algolia](https://www.algolia.com/)を思考停止で使えばいいのではと思い、この方法なら技術的にも実現できそうだし、勉強の質問という特殊な画像データにおいて検索できる可能性が少しでも高いのではないか、と考えました。

## 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`に投げれるデータは**１レコード当たり10,000バイトなので、OCR後のテキストが10,000バイトを超えたらそこまで`mb_strcut`する**ように実装するなど工夫しています。

実装が終わったら下記コマンドを実行します。

```bash
php artisan scout:import "App\{モデル名}"
```

実行中に`Algolia`のコンソールにいけば、データが次々入っていることを確認できるでしょう。

## データの検索

### 検索用のAPI
冒頭で示したようなUXを想定しているので、画像を投げたらその画像で過去の質問の画像を検索し、ヒットした質問一覧を返すようなAPIを実装しました。
弊社はレイヤードアーキテクチャをなんとなく採用しているので、画像をアップロードする処理と、アップロードした画像のURLを受け取って解析する処理、最後にテキストで`Algolia`から検索する処理を分離して、例えば将来的にネイティブアプリから`ML Kit`ですでにOCRされたテキストが渡ってくるようなユースケースにも対応可能にしておきました。

`Algolia`の中からデータを探すときも、Model->searchメソッドで検索できるので、Scoutは本当に便利です。

```php
            $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 ）もフォローお願いします！