Firestore から Algolia へコレクションデータを一括インポート

### まえがき

[Firebase Advent Calendar 2020](https://qiita.com/advent-calendar/2020/firebase) を覗いてみると本日12/14日に空白があり、悲しい気持ちになったので急遽当日に Firebase のネタを絞り出してみました👩‍🌾 12/19も埋まることを願っています！

僕は普段はてなブログで iOS・Android・Firebase 関連の記事を毎日書いているので、暇があればプロフィールページから覗いてみてください🙂


そして、今回はタイトルの通り `Firestore` のコレクションデータを `Algolia` に一括でインポートする方法を紹介したいと思います👷‍♀️間違ってるところや不明点があればぜひ教えてください！

## Algolia とは？

`Algolia` は、高機能な検索APIを提供する SaaS(Software as a Service) です。
`Firestore` は全文検索をサポートしていないので、 `Algolia` と組み合わせることでより高度な検索をクライアントから行うことができるようになります。また、全文検索以外にも AI を活用した `Algolia AI` と呼ばれるものがあるらしく、これらを活用することでより高度な検索体験を実現できるようになると思われます(触る機会があったら別途記事にします)。

https://www.algolia.com

また、`Firestore` のドキュメントでも全文検索のソリューションとして `Algolia` の使用が大々的に書かれているので、興味のある方は見てみてください。

https://firebase.google.com/docs/firestore/solutions/search?hl=ja


## 事前知識

`Algolia` は元のデータから直接検索を行わず、`Algolia` サーバーに送信されたデータをもとに検索を行います。そのため、検索に必要となるデータを **JSON record** に変換して `Algolia` に送信する必要があります。なお、ここで送信するデータに関しては元のデータ全要素を送信する必要はなく、検索・検索結果として使用する要素のみ含めることができます。

`Algolia` のデータ構造については、公式ドキュメントに詳しい解説が載っています。

https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/


そして今回行う `Firestore` のコレクションから `Algolia` へのデータの送信方法は、大きく分けて下記の二つの方法があり、今回は前者の `一括インポート` の方法を紹介します🤺 

後者の方が実用的ではあり、多くの記事も見かけますが、`Algolia` を途中から使い始めた場合は元のデータのインポートが必要だったため、この記事を書いてみました👩‍🌾

- `Firestore` から一括で `Algolia` にデータをインポートする。
- `Firestore` への書き込み(Create, Update, Delete) をトリガーに、`Firebase Functions` 経由で `Algolia` にデータを逐次インポートする。


## それではやっていく🧑‍🔧

今回は、`Firebase CLI` を使用して設定したディレクトリの`Functions` ディレクトリ内に、インポート用の `indexing.js` ファイルを作成する感じで実装した手順を紹介します。

1. まずは、`Firebase CLI` でプロジェクトを設定します。こちらについては記事の重要な部分ではないため割愛させていただきますが、Firebase の公式ドキュメントを参照すれば簡単にセットアップできます。

    https://firebase.google.com/docs/cli?hl=ja

2. 次に `functions` ディレクトリに移動して、インポートに使用する `modules` をインストールします。`npm 5.0.0` 以降からは `--save` オプションを付けずとも `save` されるようになったようですね。

        $ cd functions
        $ npm install algoliasearch
        $ npm install dotenv
        $ npm install commander
        $ npm install firebase
僕をはじめとして、`js` の `module` にあまり馴染みのない方向けにそれぞれの役割を書いておきます。

    |module|description|link|
    |---|---|---|
    |algoliasearch|`Algolia` のAPIと通信を行うために使用するモジュールで、ブラウザ・`Node.js` ともに動作します。|https://www.npmjs.com/package/algoliasearch|
    |dotenv|`.env` ファイルから環境変数を `process.env` にロードするためのモジュールです。今回の場合は、`algolia` の `Application ID` や `API Key` などの機密性の高い情報を安全に扱うために使用します。ちなみに `Process`  は、`Node.js`実行環境のグローバル変数で、`Node.js`の実行プロセスについて、情報の取得・操作を行うためのものです。|https://www.npmjs.com/package/dotenv|
    |commander|コマンドライン引数をパースして扱いやすくするために使用します。|https://www.npmjs.com/package/commander#installation|
    |firebase|`Firebase` のAPIと通信を行うためのモジュールです。|https://www.npmjs.com/package/firebase|



3. `.env` ファイルに必要な環境変数を設定します。`FIREBASE_PROJECT_ID` も今回は含めましたが、正直 `ALGOLIA_APP_ID` と `ALGOLIA_API_KEY`  が直接ソースコードに書き込まれなければいい気もします🤔

        ALGOLIA_APP_ID=YOUR_ALGOLIA_APP_ID
        ALGOLIA_API_KEY=YOUR_ALGOLIA_API_KEY
        FIREBASE_PROJECT_ID=YOUR_FIREBASE_PROJECT_ID
それぞれの値は適所プロジェクトに応じて書き換えてください。`Algolia`・`Firebase` の設定値はそれぞれのダッシュボードから確認することができます。

4. `indexing.js` にデータをインポートするためのコードをゴリゴリ書いていきます。

下記が最終的なコードになります。

```indexing.js
const algoliasearch = require('algoliasearch')
const dotenv = require('dotenv')
const firebase = require('firebase')
const program = require('commander')

program.parse(process.argv)
dotenv.config()

firebase.initializeApp({
  projectId: process.env.FIREBASE_PROJECT_ID
})

const collectionPath = program.args[0]
const indexName = program.args[1]

const db = firebase.firestore()

const algolia = algoliasearch(
  process.env.ALGOLIA_APP_ID,
  process.env.ALGOLIA_API_KEY
)
const index = algolia.initIndex(indexName)

const records = []
db.collection(collectionPath).get()
  .then((snapshot) => {
    snapshot.forEach((doc) => {

      const childKey = doc.id
      const childData = doc.data()

      childData.objectID = childKey

      records.push(childData)
      console.log(doc.id, '=>', doc.data())
    })

    index.saveObjects(records).then(() => {
      console.log('Documents imported into Algolia')
      process.exit(0)
    })
      .catch(error => {
        console.error('Error when importing documents into Algolia', error)
        process.exit(1)
      })
  })
  .catch((err) => {
    console.error('Error getting documents', error)
  })
```

次に前半と後半のブロックに分けてコードの解説をしていきます。

まず前半のブロックでは、それぞれのモジュールを初期化しています。`program.parse(process.argv)` では、コマンドラインで渡された引数をパースして、`Firebase` の `collectionPath` と `Algolia` に送信する `indexName` の値をそれぞれ定義しています。`dotenv.config()` では、`.env` ファイルから `3.` で設定した環境変数を読み込み、取得した値を元に `Firebase` と `Algolia` の初期化を行います。

```indexing.js
const algoliasearch = require('algoliasearch')
const dotenv = require('dotenv')
const firebase = require('firebase')
const program = require('commander')

program.parse(process.argv)
dotenv.config()

firebase.initializeApp({
  projectId: process.env.FIREBASE_PROJECT_ID
})

const collectionPath = program.args[0]
const indexName = program.args[1]

const db = firebase.firestore()

const algolia = algoliasearch(
  process.env.ALGOLIA_APP_ID,
  process.env.ALGOLIA_API_KEY
)
const index = algolia.initIndex(indexName)

```

後半のブロックでは、`collectionPath` で指定した `Firestore` 内のコレクションデータを取得し、その取得した情報を元に `index.saveObjects(records)` で `Algolia` にデータを送信しています。ポイントは、取得したドキュメントデータに `objectID` を付与することです。`Algolia` に設定する全てのレコードには `objectID` という一意の値をつける必要があります。今回は、`documentID` を `objectID` として設定しましたが、`objectID` の設定を `Algolia` 側に任せることも可能です。詳しくは下記を参照してください。

https://www.algolia.com/doc/api-client/methods/indexing/#the-object-id


※ 該当するインデックスは事前に `Algolia` で作成しておく必要があります。
※ 使用しているのは `admin-sdk` ではないため、コレクションの読み取りは `Firestore` に設定しているセキュリティルールに影響を受けることに注意してください。

```indexing.js
const records = []
db.collection(collectionPath).get()
  .then((snapshot) => {
    snapshot.forEach((doc) => {

      const childKey = doc.id
      const childData = doc.data()

      childData.objectID = childKey

      records.push(childData)
      console.log(doc.id, '=>', doc.data())
    })

    index.saveObjects(records).then(() => {
      console.log('Documents imported into Algolia')
      process.exit(0)
    })
      .catch(error => {
        console.error('Error when importing documents into Algolia', error)
        process.exit(1)
      })
  })
  .catch((err) => {
    console.error('Error getting documents', error)
  })
```

5.最後に作成した `indexing.js` を使用して、`collectionPath` と `indexName` と共にコードを実行します。`category/0/fields` が `Firestore` の `collectionPath` で `dev_field` が `Algolia` の `indexName` になります。

    $ node indexing.js category/0/fields dev_field
    FKR3VvSikpJS7aca5VYO => {
      imagePath: 'category/0/fields/FKR3VvSikpJS7aca5VYO.jpg',
      map: GeoPoint { _lat: 90, _long: 149 },
      name: '琵琶湖',
      updatedAt: Timestamp { seconds: 1603897200, nanoseconds: 0 },
      description: '滋賀県にあるバス釣りのメジャースポット',
      prefectureName: '滋賀県',
      createdAt: Timestamp { seconds: 1603897200, nanoseconds: 0 }
    }
    qcEJ2TR2rY0sDKtKl3zH => {
      map: GeoPoint { _lat: 49, _long: 53 },
      prefectureName: '茨城県',
      description: '茨城県にあるバス釣りのメジャースポット',
      createdAt: Timestamp { seconds: 1603897200, nanoseconds: 0 },
      updatedAt: Timestamp { seconds: 1603897200, nanoseconds: 0 },
      imagePath: 'category/0/fields/qcEJ2TR2rY0sDKtKl3zH.jpg',
      name: '霞ヶ浦'
    }
    Documents imported into Algolia


という感じで `Firestore` から `Algolia` にデータを一括でインポートできるようになりました🎉


## あとがき

なんとか書ききることができましたが、クライアントからの検索や `Firestore` からのトリガー処理について触れられなかったので、また別の記事で書こうと思います✍️

明日は @kawazu255 さんの記事です！