FirestoreのデータをElastcsearchへ移行してみた

FirebaseのFirestoreの検索がいまいちなので、
Elasticsearchを使うことにしてみました。
特にFirestoreのgeohashでの検索の精度がいまひとつ悪い、
というのが一番の理由です。

というわけで、FirestoreのデータをElasticsearchへインポートしてみました。

#Elasticsearchの準備
今回は、[Elastic Cloud](https://cloud.elastic.co/)サービスを利用することにしました。
無料トライアルを利用してみて、このCloudサービスから利用した方がコントロールパネルが充実してそうなのでこちらを選択することにしました。

<img width="1140" alt="スクリーンショット 2020-07-23 19.03.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/53b7fbff-70ff-d412-083d-10780accaff3.png">

たぶん、料金はGCP、AWSから直で使っても同等の値段になると思います。



##indexの作成

testというindexを作成

<img width="965" alt="スクリーンショット 2020-07-23 23.24.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/6fde1328-b11a-6000-ed34-298c83e4b251.png">


##mapping設定

Elasticsearchでは基本データ投入時に自動で型を設定してくれるようですが、
それだと緯度経度がgeo_pointという型にならないので予めgeo_pointを定義します。

<img width="1034" alt="スクリーンショット 2020-07-23 23.28.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/ef092956-61c3-cbc7-670d-174cc2b9e414.png">

実行するとエラーに、

**400 — Bad Request**

```json
{
  "status": 400,
  "error": {
    "root_cause": [
      {
        "reason": "Root mapping definition has unsupported parameters:  [items : {properties={location={type=geo_point}}}]",
        "type": "mapper_parsing_exception"
      }
    ],
    "type": "mapper_parsing_exception",
    "reason": "Root mapping definition has unsupported parameters:  [items : {properties={location={type=geo_point}}}]"
  }
}
```

前もって定義しようとしてもエラーになる、他にいい方法があるかもしれないけど仮のデータを投入してからgeo_pointを定義してみる。

###仮のデータ挿入

<img width="930" alt="スクリーンショット 2020-07-23 23.59.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/324a625a-f7de-b1f7-5734-1fcaeb53f468.png">

念のためデータができてるか確認

<img width="931" alt="スクリーンショット 2020-07-24 0.00.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/faa25bd1-4590-e92d-a83e-9d3e6d6eee16.png">

###改めてmapping設定

<img width="902" alt="スクリーンショット 2020-07-24 0.04.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/1e264ceb-82a0-e32f-a20b-f7ddc8683908.png">

無事に成功、念のためmapping設定の確認

<img width="936" alt="スクリーンショット 2020-07-24 0.05.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/8a6189c7-c6bc-4aa7-c36e-28a5797f63a5.png">

###仮のデータ削除

<img width="929" alt="スクリーンショット 2020-07-24 0.07.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/c0e21816-e7a1-d740-902b-f3a908d4be41.png">

#データ移行処理

１件づつ移行するととても時間がかかるのでbulkという方法でまとめてINSERTします。
Functionsでも応用できるのでnodejsを利用します。


```javascript:firebase_to_es.js
var admin = require("firebase-admin");
var serviceAccount = require("../project-0000000-firebase-adminsdk-xxxx-xxxxx.json");
admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "https://project-0000000.firebaseio.com"
});
var db = admin.firestore();
const esconfig = {
	"url" 		: "https://xxxxx.asia-northeast1.gcp.cloud.es.io:9243/",
	"username" 	: "elastic",
	"password" 	: "xxxxxxxxxxx"
};
const request = require('request-promise');

var startId = 0;

(async() => {

	do{
		items = await getItems(startId);

		if(items){
			var data = "";

			for(i in items){
				var item = items[i];
				item.location = {lat: item.location._latitude, lon: item.location._longitude};
				data += '{ "index":{ "_id" : "'+item.id+'" } }\n';
				data += JSON.stringify(item)+'\n'
			}


			let elasticsearchRequest = {
				method: "POST",
				uri: esconfig.url + "test/items/_bulk",
				auth: {
					username: esconfig.username,
					password: esconfig.password,
				},
				body: data,
				headers: {
					"Content-type": "application/x-ndjson",
				},		
				json: false
			};

			await request(elasticsearchRequest).then(response => {
				//console.log('Elasticsearch response', response);
			})		
		}

	}while(items)


	console.log("finish!");


})();

async function getItems(id){
	console.log("id", id);

	var snapshot = await new Promise( (resolve, reject) => {
		db.collection('items').where('id', '>', id).orderBy('id').limit(50).get()
				.then(snapshot => {
					if (snapshot.empty) {
						console.log('No matching documents.');
						resolve(null);
						return;
					}
					resolve(snapshot);
					return;
				})
				.catch(err => {
					console.log('Error getting documents', err);
					resolve(null);
				});
	}).catch(function(error) {

		console.log("The error is handled, continue normally");

	});

	if(snapshot===null){
		return false;
	}

	var items = [];

	snapshot.forEach(doc => {
		items.push(doc.data());
		startId = parseInt(doc.id);
	});	

	return items;

}


```

移行処理の実行

```shell
$ node firebase_to_es.js
```

##KibanaでデータをMapで確認

<img width="930" alt="スクリーンショット 2020-07-29 0.13.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/a8417494-3888-cb2d-fe12-72d9cac5072d.png">

お値段が↓こんな感じで、リーズナブルで運用できそうです。

<img width="1172" alt="スクリーンショット 2020-07-23 19.22.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/678541/a91488db-55ac-bcf3-160c-1610b01b0734.png">