Firebase Cloud FunctionsとAlgoliaの連携を試してみた

[Firebase Cloud FunctionsとGoogle Cloud Vision APIで画像に自動タグ付け](https://qiita.com/daikiojm/items/4205957739263b053b27)に続き、Firebase Realtime Databaseに保存されたドキュメントの全文検索に[Algolia](https://www.algolia.com/)を使ってみたので、ざっくりとした手順とサンプルを紹介します。
AlgoliaはFirebase[公式のドキュメント](https://firebase.google.com/docs/firestore/solutions/search)でも紹介されている全文検索SaaSで、Node.jsのSDK経由でインデックスの登録ができる為、Firebase Cloud Functionsを使ったFirebase Realtime Databaseとの連携もしやすくなっています。
また、クライアント(Web、iOS、Androidなど)のSDKも充実しているので、簡単に検索機能を実現することが出来るようです。

[Firebase Cloud FunctionsとGoogle Cloud Vision APIで画像に自動タグ付け](https://qiita.com/daikiojm/items/4205957739263b053b27)で作成した画像URLとタグ情報をAlgoliaのIndexに登録して、キーワードから画像検索を行ってみたいと思います。

# 前提

今回のサンプルは、Firebase CLI(v3.16.0)で作成し、TypeScriptで実装していきます。
また、今回使用するFirebaseのプロジェクトには[Firebase Cloud FunctionsとGoogle Cloud Vision APIで画像に自動タグ付け](https://qiita.com/daikiojm/items/4205957739263b053b27)で使用したFunctionが既にデプロイされていることが前提です。


# 手順

## Algoliaのセットアップ

まずは、Algoliaにサインアップします。
今回は、Firebaseを使っているGoogleアカウントで登録しました。
※ 後ほどCloud Functions内からAlgoliaのAPIを呼び出す際は、APIキーを使った認証を行うため同一アカウントである必要はないです

コンソールにアクセスできるようになったら、画面左側のメニューから「API Kyes」を開きます。
![algolia_keys.png](https://qiita-image-store.s3.amazonaws.com/0/79414/d7aa29b9-611e-60bb-628a-fcaf59c40fb0.png)

APIの利用に必要な情報が表示されるので、Cloud Functionで使用する次の2つを控えておきます。

* Application ID
* Admin API Key



##  Cloud Functionの実装

今回の実装例では、Algoliaの認証情報をFunction内にべた書きしています...雑でごめんなさい。
実際に使用する際には `firebase functions:config:set` などのFirebase CLIのオプションを使って環境変数から読み込むのがよいかと思います。 (詳細: [環境の設定 -Firebase](https://firebase.google.com/docs/functions/config-env?hl=ja))

```typescript:createIndex.ts
import * as functions from 'firebase-functions';
import * as algoliasearch from 'algoliasearch';

// Algoliaの認証情報
const ALGOLIA_APP_ID = 'XXXXXXXXXX'
const ALGOLIA_ADMIN_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
// 作成するIndexの名前
const ALGOLIA_INDEX_NAME = 'images';

const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_ADMIN_KEY);

export const createIndex = functions.database.ref('images/{imageId}').onCreate(event => {
  const imageData = event.data.val();
  // AlgoliaのIndexに保存する情報 
  const algoliaObject = {
    objectID: event.params.imageId,
    ...imageData
  }

  // Indexを保存
  const index = client.initIndex(ALGOLIA_INDEX_NAME);
  return index.saveObject(algoliaObject);
});
```

* Algoliaの認証情報
    * Algoliaのセットアップで控えた認証情報を記述します。

* functions.database.ref()...
    * Realtime Databaseにドキュメントが追加された際にトリガーされます。今回の例では`images/`以下のドキュメントをサブスクライブしています。

* AlgoliaのIndexに保存する情報
    * ドキュメントのIDをobjectIDとしてAlgoliaのIndexに保存しているのは、検索結果からRealtime Databaseの単一ノードを取得可能とするためです。

* Indexを保存
    * `algoliasearch`のAPIを使ってIndexの保存を行っています。非常に完結に行うことができます。

package.jsonは次の通りです。

```json:package.json
{
  "name": "functions",
  "scripts": {
    "build": "./node_modules/.bin/tslint -p tslint.json && ./node_modules/.bin/tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase experimental:functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "main": "lib/index.js",
  "dependencies": {
    "algoliasearch": "^3.24.9",
    "firebase-functions": "^0.7.1"
  },
  "devDependencies": {
    "@types/algoliasearch": "^3.24.5",
    "tslint": "^5.8.0",
    "typescript": "^2.5.3"
  },
  "private": true
}
```

## Cloud Functionのデプロイ

次のコマンドでFirebase CLIでFunctionをデプロイ。

```
$ firebase deploy --only functions
```

## データを用意

まずは、FirebaseのコンソールからCloud Storageに直接画像をアップロードします。
今回は、[ぱくたそ](https://www.pakutaso.com/)でダウンロードした写真を30枚ほどアップロードしてみました。

![スクリーンショット 2017-12-30 18.11.25.png](https://qiita-image-store.s3.amazonaws.com/0/79414/f406e2e7-8d08-7acf-557a-4fe25dcf122a.png)

アップロードが完了すると同時にDatabaseには今回保存した画像のURLとタグ情報が保存されています。

また、Algoliaのコンソールをから`Indices`を開くと、今回作成された`images`が作成されていることがわかります。

![スクリーンショット 2017-12-30 18.21.00.png](https://qiita-image-store.s3.amazonaws.com/0/79414/edee28bd-8ce3-b030-c8af-dce66b3401c9.png)

## AlgoliaのUI Demoから全文検索を試す

Algoliaには作成したIndexから全文検索をするデモを簡単に作成できる`UI Demo`という機能があります。
今回は、これを使用してフロントのコーディングなしにGUIから全文検索を行ってみます。

Algoliaのコンソールの`Indices`ページから「UI DEMOS」タブを開きます。

`GENERATE A UI DEMO`をクリックし、Title、Subtitleを入力し、 `TemplateにInstant search & faceting`を選択します。

<img width="741" alt="スクリーンショット 2017-12-30 18.31.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/79414/7df4ed08-06eb-d3cd-8054-8a3057400248.png">


次の画面では、表示項目などを設定します。
今回は、次のように設定しました。

<img width="784" alt="スクリーンショット 2017-12-30 18.34.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/79414/f769c9fb-a791-50f0-f3a4-7b6efa954c53.png">

最後に「GENERATE UI & SHARE」を選択します。

作成されたデモ画面が表示され、全文検索が行えるかと思います。

![スクリーンショット 2017-12-30 18.42.18.png](https://qiita-image-store.s3.amazonaws.com/0/79414/34744f4d-8e6c-e196-faab-ccd2763fd4a5.png)


## まとめ
今回紹介した手順で、Realtime Databaseに保存された内容に対して全文検索が行えるようになりました。
Realtime DatabaseとAlgoliaのIndexの同期自体は、Cloud Functionひとつで実現できるため、非常にハードルが低いと感じました。
おそらく実際に利用する際には、Create時以外のRealtime DatabaseとAlgoliaのIndexの同期や、クライアント側の実装、Algoliaの詳細設定などが必要になってくるかと思います。
特に、クライアントアプリケーション側で、Algoliaから直接検索結果を取得する部分と、Realtime Databaseのデータをサブスクライブする部分の設計はよく考える必要がありそうです。

## 参考

* [全文検索 -Firebase](https://firebase.google.com/docs/firestore/solutions/search)
* [Firebase と Algolia Search を連携して検索機能を実装するぞ -Qiita](https://qiita.com/miuP/items/6ae39ec2c272514152c8)
* [Getting started with Algolia(JavaScript) -algolia DOCS](https://www.algolia.com/doc/tutorials/getting-started/quick-start-with-the-api-client/javascript/)