天网恢恢,疏而不漏,全文搜索 Firestore + Elasticsearch + React Native

引言

由于Firestore的查询功能较为繁琐,我开始研究如何使用Elasticsearch进行全文搜索数据。当我使用实时数据库时,找到了一些有用的文章可供参考,但在使用Firestore时,却没有找到合适的文章。最终,我不得不费力地将实时数据库的实现方式参考并修改为适用于Firestore的方法,这一过程非常困难。我将这些经验保存下来。
使用Firebase可以使得服务端开发的意识稍微减少,但方法还没有完全确定下来,我希望有更多的人尝试并不断地分享经验。如果有任何请求,请告诉我,我会尽力去做。

目标

    • firebase使いたいなと思ってる人

 

    • firestoreのクエリに物足りなさを感じてる人

 

    • Elasticsearchを使って見たい人

 

    気軽にネイティブのアプリケーションを作ってみたい人

做过的事概述

flashlightを書き換える。(原型はもう留めていないかも知れない)

GAEにnodeをdeployする。

GCEにElasticsearchのインスタンスを立てる。

react nativeから検索を投げる。

解释

节点

这里已经放置了代码本身,如果不需要解释的人请克隆并使用。
如果想查看原始代码,请使用这个。
需要注意的是,原始代码是为实时数据库设计的,所以使用方式略有不同。
在从实时数据库切换到云端数据库时,我最困惑的是是否可以直接访问数据库终端。
当firebase的数据库中的数据更新时,如果要将更新传达给Elasticsearch,
在实时数据库中,只需直接访问URL即可,但在云端数据库中,需要自己进行修改。

一旦理解了,就非常简单了,只需订阅Firestore并在发生更改时进行处理。

如果使用云函数,在触发 onCreate、onDelete 等事件时可以进行触发,所以也许那边更好,但是总之暂时使用运营来覆盖。

那么,接下来我们将转到内容的说明。
目录结构是这样的样子。

.
├── app.js  # 本体コード
├── app.yaml  # gaeにdeploy用の設定
├── config.example.js  # config
├── lib
│   ├── Registration.js  # firestoreに変更があったときに、Elasticsearchにupdateをかけにいくコード
│   ├── Search.js  # 検索クエリを投げられたときに。Elasticsearchに検索をかけ、結果を返すコード
│   └── initFirebase.js  # firebaseのsdkを初期化するコード
├── package-lock.json
└── package.json

1. SDK初始化

首先,按照平常的方式初始化Firebase的SDK。

const conf = require('../config');
const firebase = require('firebase');

const firebase_config = {
    apiKey: conf.apiKey,
    authDomain: conf.authDomain,
    databaseURL: conf.databaseURL,
    projectId: conf.projectId,
    storageBucket: conf.storageBucket,
    messagingSenderId: conf.messagingSenderId
};
firebase.initializeApp(firebase_config);

有时候我会忘记。

2. 查找功能

接下来是搜索功能的实现。
首先,我们创建一个名为”search_request”和”search_response”的Firestore集合。
“search_request”集合用于接收来自原生应用的搜索条件传递到Elasticsearch,而”search_response”集合用于存放搜索结果以传递回原生应用。
如果阅读本文,并不需要读完全部内容,但看一下标有”A little bit tricky”的部分的图示会更容易理解,
这对于后续的理解将会有所帮助。
在文章中,使用了云函数和实时数据库,但在本次实现中,我们使用GAE(Google App Engine)来代替云函数,并使用Firestore代替实时数据库,但其实现方法并没有很大的差异。

在Firebase中,您可以根据对实时数据库或云Firestore中的数据所做的更改来触发函数。

this.ref_req = firebase.firestore().collection('search_request');
this.unsubscribe = this.ref_req.onSnapshot(this._showResults.bind(this));

在Firestore中,我们可以监视集合并在整个集合发生更改时触发函数。但是,我们也可以监视文档。例如,我们可以监视集合并检查已发送请求是否有相应的响应,如果有匹配的数据,则可以在React端进行处理。但是从安全性和网络通信费用以及数据库运营的角度来看,这样做似乎不太合适。因此,在返回响应时,我们只订阅文档。

可以通过订阅搜索请求,向Elasticsearch发出搜索并将结果返回给响应来实现如下。

'use strict';

const firebase = require('firebase');
const _ = require('lodash');
//////////////////////////////////////////////////
/*
 * firebaseのデータにqueryが登録されたときに
 * ElasticSearchへ検索を投げて、結果をfirebaseの方に返す
 * 返却するときのデータ整形もあとで決める
*/

class Search {

    constructor(esc, refReq, refRes, index, type) {
        this.esc = esc;
        this.refReq = refReq;
        this.refRes = refRes;
        this.index = index;
        this.type = type;
    }

    init() {
        /*
         * subscribeをcollectionに張る
        */
        this.refReq = firebase.firestore().collection(this.refReq);
        this.refRes = firebase.firestore().collection(this.refRes);
        this.unsubscribe = null;
        this.unsubscribe = this.refReq.onSnapshot(this._showResults.bind(this));
    }

    _showResults(snap) {
        /*
         * firestoreに投げられた検索リクエストを取得して、Elasticsearchに渡すクエリに変換する
        */
        snap.forEach((doc) => {
            let { from, q, size } = doc.data();
            let query = {
                from,
                index: this.index,
                q,
                size,
                type: this.type,
            }
            this._searchWithElasticsearch(doc, query)
        })
    }

    _searchWithElasticsearch(doc, query) {
        /*
         * elasticsearchで検索を行い、
         * 結果のデータを整形してfirestoreに受けわたす関数
        */
        this.esc.search(query, function(error, response) {
            if(_.isUndefined(error)){
                let returnData = {}
                response.hits.hits.forEach((data) => {
                    returnData[data._id] = {
                        id: data._id,
                        source: data._source,
                        score: data._score,
                    }  // まだfirestoreのどのデータをelasticsearchに送るのか決めてないので、返ってきたものを全てfirestoreに受け渡している
                })
                console.log('_searchWithElasticsearch: success', query, returnData)
                this.refRes.doc(doc.id).set(returnData);
                this.refReq.doc(doc.id).delete();
            }else{
                console.log('_searchWithElasticsearch: failed', error)
            }
        }.bind(this));
    }

}

exports.init = function(esc, refReq, refRes, index, type) {
    new Search(esc, refReq, refRes, index, type).init();
}

将Firestore数据与Elasticsearch同步。

最後,這是關於在Firestore數據更新後向Elasticsearch發送更新的部分實現。
暫不考慮數據刪除的情況。
首先,在Firestore中創建一個名為”users”的集合。
我們希望在數據發生更改時向Elasticsearch發送更新,但在我自己操作的階段中,單獨為每個更新的文檔更新到Elasticsearch的次數有些困難,所以我嘗試使用標記進行管理。
具體來說,在react中進行更新時,我設置一個名為”ES_STATE”的字段,並將其更改為”STAY”或其他任意關鍵字。
然後,我們只訂閱”STAY”狀態的文檔,
在集合發生更改時,檢索所有”STAY”狀態的文檔並對其進行Elasticsearch的更新。
然後,在此之後,我們將去修改Firestore中管理的標記。

'use strict';

const firebase = require('firebase');
const _ = require('lodash');
//////////////////////////////////////////////////
/*
 * firebaseのデータに変更が加えられたときに、
 * ElasticSearchの方にデータを送る
*/

class Registration {

    constructor(esc, collection, index, type) {
        this.esc = esc;
        this.collection = collection;
        this.index = index;
        this.type = type;
    }

    init() {
        this.ref = firebase.firestore().collection(this.collection).where('ES_STATE', '==', 'STAY');
        this.unsubscribe = null;
        this.unsubscribe = this.ref.onSnapshot(this._showResults.bind(this));
    }

    _showResults(snap) {
        snap.forEach((doc) => {
            const sendData = {
                index: this.index,
                type: this.type,
                id: doc.id,
                body: {
                    name: doc.data().name,
                    text: doc.data().text,
                    updatedAt: doc.data().updatedAt,
                    createdAt: doc.data().createdAt,
                },  // bodyに何を送るかは別途考える。index, type, collectionはclassの外で定義出来るようにしたので、これも切り出したい
            }
            this._sendDataToElasticsearch(sendData)
            // 返り値を用意して、firestoreのフラグ変更の関数の発火を制御した方が良いかもしれない
            this._updateFlagInFirestore(doc)
        })
    }

    _sendDataToElasticsearch(sendData) {
        /*
         * firestoreで変更されたデータをElasticsearchに送信する関数
        */
        this.esc.index(sendData, function (error, response) {
            if(_.isUndefined(error)){
                console.log('_sendDataToElasticsearch: success')
            }else{
                console.log('_sendDataToElasticsearch: failed', error)
            }
        }.bind(this));
    }

    _updateFlagInFirestore(doc) {
        /*
         * firestoreの各ドキュメントにステータス管理用のfieldを用意している。
         * Elasticsearchにデータを更新し終わったら、firestoreのステータス管理fieldに
         * ステータス変更を書き込みに行く関数
        */
        return firebase.firestore().collection(this.collection).doc(doc.id).update({
            'ES_STATE': 'DONE',
        })
        .then(function() {
            console.log('_updateFlagInFirestore: success');
        })
        .catch(function(error) {
            // The document probably doesn't exist.
            console.error('_updateFlagInFirestore: failed', error);
        });
    }

}

exports.init = function(esc, collection, index, type) {
    new Registration(esc, collection, index, type).init();
}

实际上,传递的查询需要经过验证,还需要处理读写失败时的异常情况等,有很多事情要考虑,但现在先不考虑。
如果您需要创建的服务能够实现,我们可以再进一步考虑。
此外,本次只创建了简单的搜索查询,如果有更复杂的需求,如拼写错误处理等,我们也可以做一些其他的事情。

GAE

这样一来,

node app.js

虽然也可以,但我会在GAE上部署。
如果只是一个简单的后端服务,免费额度应该可以满足需求,而且也不需要考虑扩展等问题,所以GAE似乎是个不错的选择。
前端使用原生应用,后端使用GAE,数据库使用firebase,因此我们只需要管理代码,而且即使人手较少也更容易开发服务(虽然没有正式的开发经验)。
对于web应用而言,由于firebase可以提供hosting功能,所以应该基本没有问题。
而且通过使用cloud function,甚至可以实现SSR,因此即使是对于需要快速加载的服务,也能提供支持吧。
嗯,在进行功能测试时,使用node就足够了。

runtime: nodejs
env: flex
manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

由于当前状态只是个玩具,所以 YAML 可以这样设置没问题。
当通过 gcloud 从命令行界面部署时,如果使用的是未开启计费的帐户,会出现要求设置计费帐户的错误,所以如果不喜欢的话,可以使用 Heroku 或其他服务。
只是游玩的话,理应充分满足 GAE 免费额度的需求。

高级中学毕业文凭

在GCE中,您可以通过一个按钮启动Elasticsearch。
虽然GCE是付费的,但只是用来玩耍的话,我认为可以参考这里的成本感。
如果只是想在本地玩玩的话,可以从这里下载,

bin/elasticsearch

我认为只要启动就足够了。

React Native 反应原生

使用React Native将数据写入到Firestore非常简单,只需要使用add方法即可。
React Native的代码我放在这里。
由于这个代码本身只是草率地制作,所以如果想要详细了解原生应用程序的实现方法,建议查看其他文章。我会在合适的时候自己写一篇文章。

doSearch = async (query) => {
    const snap = await this.ref.add(query);
    const key = snap.id;
    this.unsubscribe = this.ref_res.doc(key).onSnapshot(this.showResults);
}

仅需以下步骤,您便可以使用Elasticsearch。
对于Firestore查询,我们将限制在最低限度上,将必要的搜索任务交给Elasticsearch处理。

总结

我认为可能还有一些我们无法完全掌握的部分,但我觉得Firebase非常方便。
对于我们无法掌握的部分,我认为我们可以在其他地方补充。
就像这个例子,如果我们依赖Elasticsearch,问题可能就可以解决了。

另外,即使在这次使用方式上不同,当我们希望在复杂的关系型数据库(RDB)进行分析时,我们也可以先努力编写SQL,然后将数据全部导入到非关系型数据库(NOSQL)中,并通过Elasticsearch进行一定程度的评分等处理,以便将数据转换为更易处理的形式。正常情况下,如果遇到这种情况,重新设计数据库可能会更好,但我还是抱有期望。

最近想做的事情太多了,感觉慢慢赶不上了。

广告
将在 10 秒后关闭
bannerAds