AddSearchでFirestoreのCloud Functionsで同期しよう

# まえがき

FirebaseのFirestoreは便利だが、クエリーの書き方に制限が多くて使い物にならない。
制限を解消するため他のサービスと連携する必要がある。
Algolia(https://www.algolia.com/)
というサービスもあるが、
他にもAddSearch(https://www.addsearch.com/)
というAlgoliaよりも微妙に安いサービスが有る。
今回はAddSearchの記事です。
僕が人柱になって記事書けば日本で発展してくれて、賢い方たちが情報発信してくれたら嬉しいなーっていう魂胆で書きましたよ。

プラン（お金関連）はココにあります。
https://www.addsearch.com/pricing/

Linux開発者のリーナス同じフィンランドのヘルシンキにある会社のサービス。
使い放題らしい。
サンプルや世間の情報があまりないのが残念。
情報が英語ですらあんまりない。
「AddSearch」でググっても「Add」と「Search」での結果が出る始末。

Firestoreの中身の値をAddSearchへ中身をコピーするSync（同期）する仕組みつくって、
コピーされたAddSearchからクエリーを書く方法で対処したいです。

FirebaseのデータベースサービスのFirestore上でドキュメントが生成・削除・更新があれば、
それを検知して、AddSearchに更新をかけるということ。

SyncするためにFirebaseのCloud Functionsで書きます。

「Cloud Functions for Firebase」内のデータベースをsyncさせるイメージです。

`users/`配下にある前提で話を勧めていきますね。

# Cloud Functiondsの準備

## キーの設定

firebase functions:config:set addsearch.sitekey="{サイトキー}" addsearch.secretkey="{シークレットキー}"

`{サイトキー}`これはあなたの管理画面の「Your Site Key」にしてください。
`{シークレットキー}`これはあなたの「Your Secret API Key」にしてください。

`{`と`}`はいらないですよ。


・スクショ貼っておきます。

<img width="993" alt="Screen Shot 2020-07-24 at 19.14.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11951/3f5c93a5-d572-3c9f-2db4-18c3cb516b3b.png">

## コード

TypeScriptで書きます。
一応、JSとAddSearchで使う公式っぽい`npm`はあります(https://www.npmjs.com/package/addsearch-js-client)が、Issueできいたら検索専用なので今回使いません。
ちなみにそのnpmはTypeScriptは対応してない。
TypeScriptの型定義ファイルがなかったです。

自分でHTTTPリクエストする必要がある。
無難に`axios`を使います。使わないやり方は自分で読み替えて下さい。


# Syncするための仕組み作成

## 準備

以下で色々npm入れて下さい。

`npm install -g npm`
`npm i -g firebase-tools`

`firebase init functions` して下さい。
中に生成されるので最初にフォルダ切ったほうがいいです。
`? What language would you like to use to write Cloud Functions?`では`TypeScript`を忘れずに！
                                    
```
> firebase init functions                                                                                                               Mon Jul 13 10:53:23 2020

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /Users/shinriyo/development/functions_apps


=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: hoge-project (hoge)
i  Using project hoge-project (hoge)

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use TSLint to catch probable bugs and enforce style? Yes
✔  Wrote functions/package.json
✔  Wrote functions/tslint.json
✔  Wrote functions/tsconfig.json
✔  Wrote functions/src/index.ts
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes

> protobufjs@6.9.0 postinstall /Users/shinriyo/development/functions_apps/functions/node_modules/protobufjs
> node scripts/postinstall

npm notice created a lockfile as package-lock.json. You should commit this file.
added 287 packages from 221 contributors and audited 287 packages in 12.772s
```

気をつけてほしいのが「"node": "10"」にしておかないとnodeが古いとCloud Functionsがサポートしなくなるのでおそらく8となってるのを10に変えて下さい。
しかし、11はサポートしてないみたいですｗ

```
{
  "name": "functions",
  "scripts": {
    "lint": "tslint --project tsconfig.json",
    "build": "tsc",
    "serve": "npm run build && firebase emulators:start --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "engines": {
    "node": "10"
  },
  "main": "lib/index.js",
  "dependencies": {
    "algoliasearch": "^4.3.0",
    "axios": "^0.19.2",
    "firebase-admin": "^8.10.0",
    "firebase-functions": "^3.6.1"
  },
  "devDependencies": {
    "tslint": "^5.12.0",
    "typescript": "^3.8.0",
    "firebase-functions-test": "^0.2.0"
  },
  "private": true
}
```

## 設定ファイル

こんな感じに作ってきました。
`export` してる箇所ありますが後で作るのでこの瞬間はエラーになると思います。

```src/index.ts:typescript
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp();
const env = functions.config();

// AddSearch
export const baseURL = 'https://api.addsearch.com';
// 設定のYour Site Key (Index public keyのこと)
export const siteKey = env.addsearch.sitekey;
// 設定のYour Secret API (Secret key is used in the authentication header)
export const secretKey = env.addsearch.secretkey;

// 各Functionのため
export * from './user.onDelete';
export * from './user.onUpdate';
export * from './user.onCreate';
```

## 生成

* 公式ドキュメント：https://www.addsearch.com/docs/api/indexing-update/

今回生成ですが、UpdateやDeleteなども作ります。（後述）


本当はCREATEするときにPOST使いたいと思うが、
https://www.addsearch.com/docs/api/indexing-create/
`id`はこちらで定義されたやつは使えない！！
仕方ないので
POSTはAddSearchの自動生成のテキトーなIDを使いたいときではいいが、
今回はFiresoreのdocumentIdを使いたいので、

なので以下これはだめですね。

```typescript
        axios.post(`${baseURL}/v2/indices/${siteKey}/documents/`, args, {
```

https://www.addsearch.com/docs/api/indexing-update/
新規作成での同期は`UPDATE`でも使えるうPUTで書きます。新規も可能。

コード全体。
ファイル名にわざと`.`つけてますが動きます。
嫌なら名前を好きに付けて下さい。

```typescript:src/user.onCreate.ts:
import { index, baseURL, siteKey, secretKey } from './index';
import * as functions from 'firebase-functions';

// AddSearchのDocument生成
export const userOnCreateForAddSearch = functions.firestore
    .document('users/{userId}')
    .onCreate(async (snap, context) => {
        const axios = require('axios');
        const data = snap.data();
        const objectID = snap.id;

        const args = {
            withCredentials: true,
            custom_fields: {
                ...data
            },
            headers: {
                "Content-Type": "application/json",
            }
        }

        axios.put(`${baseURL}/v2/indices/${siteKey}/documents/${objectID}`, args, {
            // HTTP Basic Auth
            auth: {
                username: siteKey,
                password: secretKey,
            }
        })
            .then((response: any) => {
                console.log(response.data)
            })
            .catch((error: any) => {
                console.log(error)
            })
            .then(function () {
                console.log("*** finish ***")
            })
    });

```

### 注意点

axios.postの第三引数に「HTTP Basic Auth」にいるものを書く。
```
            // HTTP Basic Auth
            auth: {
                username: siteKey,
                password: secretKey,
            }
```
の箇所。ハマった。
`args`に含めてもだめだった。


内容は、`custom_fields`を使って書くこと。`data`と書いてハマった。

```
{
  "id": objectID,
  "custom_fields": {
    ...data
  }
}
```

みたいにやること。
`custom_fields`の中に欲しいデータ書くらしい。
idはAlgoliaみたくobjectIDと書くんじゃなく、単にid

`withCredentials`はおそらく必須。

## 削除処理

* 公式ドキュメント：
https://www.addsearch.com/docs/api/indexing-delete/

PUTSの処理をそもままやればいけると思ったらできない。

`args` は2番目だったと思ったら違う。

https://blog.logrocket.com/how-to-make-http-requests-like-a-pro-with-axios/

このサイト、よくみたら
PUTとDELETEで引数の順番が違う。

```
axios.delete(url[, config])
axios.put(url[, data[, config]])
```

```typescript:src/user.onDelete.ts
import { index, baseURL, siteKey, secretKey } from './index';
import * as functions from 'firebase-functions';

// AddSearchのDocument生成
export const userOnCreateForAddSearch = functions.firestore
    .document('users/{userId}')
    .onCreate(async (snap, context) => {
        const axios = require('axios');
        const data = snap.data();
        const objectID = snap.id;

        const args = {
            withCredentials: true,
            custom_fields: {
                ...data
            },
            headers: {
                "Content-Type": "application/json",
            }
        }

        axios.put(`${baseURL}/v2/indices/${siteKey}/documents/${objectID}`, args, {
            // HTTP Basic Auth
            auth: {
                username: siteKey,
                password: secretKey,
            }
        })
            .then((response: any) => {
                console.log(response.data)
            })
            .catch((error: any) => {
                console.log(error)
            })
            .then(function () {
                console.log("*** finish ***")
            })
    });
```

## 更新

```typescript:src/user.onUpdate.ts
import { index, baseURL, siteKey, secretKey } from './index';
import * as functions from 'firebase-functions'

export const userOnUpdateForAddSearch = functions.firestore
    .document('users/{userId}')
    .onUpdate(async (event, context) => {
        const axios = require('axios');
        // const oldData = event.before.data();
        const newData = event.after.data();
        const documentId = event.after.id;
        const args = {
            withCredentials: true,
            custom_fields: { ...newData },
            headers: {
                "Content-Type": "application/json",
            }
        }

        // PUT /v2/indices/{index public key}/documents/{document id}
        axios.put(`${baseURL}/v2/indices/${siteKey}/documents/${documentId}`, args, {
            // HTTP Basic Auth
            auth: {
                username: siteKey,
                password: secretKey,
            }
        })
            .then((response: any) => {
                console.log(response.data)
            })
            .catch((error: any) => {
                console.log(error)
            })
            .then(function () {
                console.log("*** finish ***")
            })
    });
```

# Cloud Functionsにデプロイします

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

# あとがき

## 全部削除はわからない

消し方わかりません。中の人に消してもらいましたｗ

## 非対応の型？

タイムスタンプや、マップやマップの配列はうまく同期されてませんでした。
対応してないのか？教えてほしい。

## ログ

これ叩いてちまちま見ている。
もっといいやり方あったら教えてほしい。

```
firebase functions:log
```

AddSearchに今はいってるのか確かめるのはPostmanとかで見るしか無いです。
Webで見る仕組みを来月2020/08くらいに出すっぽいです。

<img width="871" alt="Screen Shot 2020-07-24 at 19.27.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/11951/55f7cad9-4e0a-5953-5562-1fee3728e441.png">

## おまけ

* 現状確認しているバグ

自動生成したやつではなく、マニュアルで（自分で）作ったIDでは検索できない。
サーバーエラーになる。（ほうこくすると今対処してるらしい）