iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

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

今回はタイトルの通り、Firestore のコレクションデータを Algolia に一括でインポートする方法を紹介したいと思います👷‍♀️

Algolia とは?

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

www.algolia.com

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

firebase.google.com

事前知識

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

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

www.algolia.com

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

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

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

それではやっていく

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

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

firebase.google.com

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

$ cd functions
$ npm install algoliasearch
$ npm install dotenv
$ npm install commander
$ npm install firebase

僕をはじめとして、jsmodule にあまり馴染みのない方向けにそれぞれの役割を書いておきます。

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

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

ALGOLIA_APP_ID=YOUR_ALGOLIA_APP_ID
ALGOLIA_API_KEY=YOUR_ALGOLIA_API_KEY
FIREBASE_PROJECT_ID=YOUR_FIREBASE_PROJECT_ID

それぞれの値は適所プロジェクトに応じて書き換えてください。AlgoliaFirebase の設定値はそれぞれのダッシュボードから確認することができます。

4.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) => {
      // get the key and data from the snapshot
      const childKey = doc.id
      const childData = doc.data()
      // We set the Algolia objectID as the Firebase .key
      childData.objectID = childKey
      // Add object for indexing
      records.push(childData)
      console.log(doc.id, '=>', doc.data())
    })
    // Add or update new objects
    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) では、コマンドラインで渡された引数をパースして、FirebasecollectionPathAlgolia に送信する indexName の値をそれぞれ定義しています。dotenv.config() では、.env ファイルから 3. で設定した環境変数を読み込み、取得した値を元に FirebaseAlgolia の初期化を行います。

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 という一意の値をつける必要があります。今回は、documentIDobjectID として設定しましたが、objectID の設定を Algolia 側に任せることも可能です。詳しくは下記を参照してください。

www.algolia.com

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

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 を使用して、collectionPathindexName と共にコードを実行します。category/0/fieldsFirestorecollectionPathdev_fieldAlgoliaindexName になります。

$ 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 にデータを一括でインポートできるようになりました🎉

参考

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com