wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

Firestoreで全文検索の機能を作成する

新設計のFirestoreの実装が、あともう少しで完了しそうなので、今回はその途中で実装した内容について記事にしてみました。

ペペロミアでは、以下みたいにタイトルを入力するとサジェストを表示する機能を実装していました。

f:id:wheatandcat:20201129135922p:plain:w200

今までは、単純に自身のCalendarの情報を取得して、そこからフロントエンド内でフィルタリングして表示させてましたが、 辞書情報をFirestoreに保存して、そこからサジェストリストを取得する実装に変更しました。

Fiestoreで全文検索を実装

上記の機能を実装するために辞書情報から全文検索で値を取得する必要があります。 Firestoreで全文検索するには、一般的にAlgoliaを使用されています。

firebase.google.com

ただし、Algoliaは有料サービスなので、あまり使いたくないなーと思ってググったところ良い記事を見つけたので、 今回はこちらを参考に実装してみました。

qiita.com

方式としては、テキストを以下みたいなbi-gramに変換して

代々木公園  =>  ["代々", "々木" ,"木公", "公園"]

その情報をオブジェクト形式でFirestoreに保存

bigram: {
  "代々": true,
  "々木": true,
  "木公": true,
  "公園": true
}

それで、その情報を以下のようなwhere句で検索すると全文検索のように扱えるという仕組みです

text := "代々木公園"
bigrams, err := ngram(text, 2)
if err != nil {
    return nil, err
}

var query = f.Collection("version/1/dictionary").Limit(100)

for _, bigram := range bigrams {
    key := "bigrams." + bigram
    // ここでプロパティがtrueかチェックする
    query = query.Where(key, "==", true)
}

matchItem := query.Documents(ctx)
docs, err := matchItem.GetAll()

これでFirestoreの制限に引っかからず情報の取得が可能になります。

それでが、以下からペペロミアで実装してみます。

実装

まず、辞書データ作成のスクリプトを作成するスクリプトを作成

github.com

Goだと、どうやっても以下みたいな動的なプロパティの変数を作るのが難しかったので、

bigram :=    {
  "代々": true,
  "々木": true,
  "木公": true,
  "公園": true
}

Goでデータ作成のJSONファイルを作成して、Firestoreのデータ作成のみjsonを読みこんで書き込みするスクリプトをnode.jsで作成しました。

以下でdictionaryに必要な情報をJSONにして作成

https://github.com/wheatandcat/PeperomiaTool/blob/master/GenerateDictionary/main.go

上記のスクリプトでFirestore上に存在するスケジュール情報を抜き出して以下のようなJSONファイルを作成

■ dictionary.json

[
 {
  "text": "新宿駅",
  "bigrams": [
   "新宿",
   "宿駅"
  ]
 },
 {
  "text": "新宿御苑",
  "bigrams": [
   "新宿",
   "宿御",
   "御苑"
  ]
 },
 {
  "text": "TOHOシネマズ新宿",
  "bigrams": [
   "TO",
   "OH",
   "HO",
   "Oシ",
   "シネ",
   "ネマ",
   "マズ",
   "ズ新",
   "新宿"
  ]
 },
 {
  "text": "浅草寺二天門前",
  "bigrams": [
   "浅草",
   "草寺",
   "寺二",
   "二天",
   "天門",
   "門前"
  ]
 },
 {
  "text": "葛西臨海公園水上バス",
  "bigrams": [
   "葛西",
   "西臨",
   "臨海",
   "海公",
   "公園",
   "園水",
   "水上",
   "上バ",
   "バス"
  ]
 },
 {
  "text": "葛西臨海公園",
  "bigrams": [
   "葛西",
   "西臨",
   "臨海",
   "海公",
   "公園"
  ]
 },
 {
  "text": "市ヶ谷駅",
  "bigrams": [
   "市ヶ",
   "ヶ谷",
   "谷駅"
  ]
 },
 {
  "text": "市ヶ谷フィッシュセンター",
  "bigrams": [
   "市ヶ",
   "ヶ谷",
   "谷フ",
   "フィ",
   "ィッ",
   "ッシ",
   "シュ",
   "ュセ",
   "セン",
   "ンタ",
   "ター"
  ]
 },
 {
  "text": "上野駅",
  "bigrams": [
   "上野",
   "野駅"
  ]
 },
 {
  "text": "カナルカフェ",
  "bigrams": [
   "カナ",
   "ナル",
   "ルカ",
   "カフ",
   "フェ"
  ]
 },
 {
  "text": "飯田橋駅",
  "bigrams": [
   "飯田",
   "田橋",
   "橋駅"
  ]
 },
 {
  "text": "お花見",
  "bigrams": [
   "お花",
   "花見"
  ]

上記のJSONファイルを以下のnode.jsで読み込ませて書き込み

https://github.com/wheatandcat/PeperomiaTool/blob/master/GenerateDictionary/insert/main.js

これで以下のようなデータが作成されました。

f:id:wheatandcat:20201129224448p:plain

ここまでデータ作成は完了。次はサジェストを取得するAPIを作成していきます。

Pull Requestは、こちら

github.com

作成したGraphQLでは、引数にテキストを送ると一致したテキスト情報をレスポンスするようになっています。

スクリーンショット 2020-11-29 0 34 01

最後に、上記のAPIをアプリ側に組み込んで完成です。

github.com

以下のような custom hooksを作成してテキスト入力毎に、サジェストを返すようにしています。

https://github.com/wheatandcat/Peperomia/blob/cd3fecd1d7f3382f1b3d0ed27047c0392a9ebfbc/src/hooks/useItemSuggest.tsx

import { useCallback, useState } from 'react';
import { useSuggestionTitleQuery } from 'queries/api/index';

type UseItemSuggest = {
  setSuggestList: (title: string) => void;
  suggestList: string[];
};

const useItemSuggest = (): UseItemSuggest => {
  const [text, setText] = useState('');

  const { data, error } = useSuggestionTitleQuery({
    variables: {
      text,
    },
  });

  const setSuggestList = useCallback((title: string) => {
    setText(title);
  }, []);

  const suggestList = data?.suggestionTitle || [];

  if (error) {
    console.log('err:', error);
  }

  return {
    setSuggestList,
    suggestList,
  };
};

export default useItemSuggest;

諸々の対応が完了して、以下のように動作になりました。

f:id:wheatandcat:20201129225523g:plain:w300