Logo
ブログ運営
microCMS
Next.js

Next.JS+microCMSで作成したブログに、AWS Lambdaで全文検索機能を追加する

2021年07月16日

初めに

microCMSや、Contentfulを活用して、オウンドメディアや、ブログを作成することはよくあると思います。そういった場合、記事の全文検索機能を作ってほしいというニーズはよくあります。全文検索を行う場合、以下のような選択肢があると考えられます。

  • Algoliaを使って実現する
  • 自分で作る
  • microCMSのAPIを利用する

今回は、一番簡単で、お金のかからないmicroCMSのAPIを利用することにします。

検索APIのコールの方法

あらかじめ検索結果がわかっているカテゴリ検索や、タグ検索と違い、全文検索は検索結果のリストをあらかじめ作っておくことができません。当然APIのコールはブラウザでコールする必要があります。
しかしながら、microCMSのAPIをコールするためには、API-KEYが必要です。
このAPI-KEYは、

のように、ブラウザ側から見えるようになると危険です。なので、このような場合、Next.jsのAPI Routerという機能を使ってサーバーサイドでAPIをコールするようにするのが一般的です。しかしながら、今回このブログはAmplifyでホスティングをしており、SSG(静的サイトジェネレータ)としてホスティングしています。
Amplify自体は、最近の更新で、SSRモードでNext.jsを実行することができるのですが、SSRで実行すると

  • SSGでは、特に表に出ていなかったCloudFrontなどが見えてしまう

のような欠点(欠点といえるほどではなく、こだわりの範疇ですが・・)があり、SSGのままで出来ないかと考えました。
さらに調べたところ、公式サイトのmicroCMSブログのクローンを作るという記事に、Netlify Functionsを使てって実装する方法が載っていました。これと同じことをAWS LambdaとAPI Gatewayで出来ないかと思い試してみることにしました。

AWS Lambdaの実装

まず、AWSのLambdaを実装します。Lambdaは、Pythonで作成することにしました。以下のようなコードのPythonのソースコードを記述します。

import os
import json
import requests
from urllib.parse import quote

api = os.environ['MICRO_CMS_API']
service_domain = os.environ['MICRO_CMS_SERVICE_DOMAIN']
x_api_key = os.environ['MICRO_CMS_PUBLIC_X_API_KEY']
fields = os.environ['MICRO_CMS_PUBLIC_X_API_FIELDS']

def lambda_handler(event, context):
    request = json.loads(event['body'])
    q = request['q']
    limit = request['limit']
    offset = request['offset']

    headers = {'x-api-key': x_api_key}
    url = 'https://{}.microcms.io/api/v1/{}?q={}&limit={}&offset={}&fields={}'.format(service_domain, api, quote(q),
                                                                                      limit, offset, quote(fields))
    rSucess = requests.get(url, headers=headers)
    print(rSucess.status_code)
    print(rSucess.content)
    return rSucess.content


なお、検索対象のAPI、サービスのドメイン、X-API-KEY、取得するFieldは、環境変数で設定できるようにし、検索キーワード、オフセット、リミットだけをリクエストのボディで取得するようにしました。
検索キーワードと、オフセットと、リミットだけをコール時に設定可能という設計にしたのは、

  • 検索結果のページングを実現したかった
  • Fieldもリクエストボディで指定できるようにすると、検索結果の表示に必要ない、Fieldも取得できるようになってしまう(microCMSはデータ転送量に対して課金されるので、不要なデータをレスポンスで取得できないようにしたかった)

という理由からです。

API Gatewayを設定する

次に、作成したLambdaをWeb API化するためにAPI Gatewayを設定しました。今回APIは安価で簡単な、HTTP APIを使用しました。
ポイントとしては、CORSの設定をちゃんとする必要があります。ブログのURLをちゃんとAccess-Control-Allow-Origiに設定しないとAPIコール時にエラーになります。
それ以外は、本当にマウスでポチポチやっていけば設定完了です。


検索結果の表示ページを作成する

最後に検索結果を表示するページを作成します。カテゴリ検索や、一覧表示時のページングのページ番号は、URLのパスパラメータで表現していましたが、全文検索の一覧結果のページングのページ番号は、クエリパラメータで取得することにしました。

なので、search/index.tsxファイルを以下のように実装しました。

const Page: NextPage<PageProps> = ({ categoryList }) => {
  const classes = useStyles()
  const [query, setQuery] = useState<Query>()
  const { isLoading, page, list, fetch } = useBlogs()
  const router = useRouter()

  useEffect(() => {
    setQuery(router.query as Query)
  }, [router.query])

  useEffect(() => {
    if (query) {
      fetch(query.keyword || "", query.page ? toNumber(query.page) : 1)
    }
  }, [query])

  const handleNextPage = useCallback((page: number) => {
    router.push(`/blogs/search?page=${page}`)
  }, [])

  return (
    <>
      {!isLoading ? (
        <>
          <HeadTitle title={`「${query?.keyword || ""}」の検索結果`} />
          <BlogListLayout
            title={`「${query?.keyword || ""}」の検索結果`}
            blogList={list}
            categoryList={categoryList}
            page={page}
            onChangePage={handleNextPage}
          />
        </>
      ) : (
        <div className={classes.loading}>
          <CircularProgress />
        </div>
      )}
    </>
  )
}


上の実装ではuseBlogというカスタムHooksを作成し、その中で、API GatewayのAPIをコールするようにしています。
ページが表示されると、クエリパラメータで、検索キーワードと、ページ番号を取得して、それに応じた一覧を取得するようになっています。

検索結果を試してみる

デプロイしたブログで、全文検索できるかためしてみます。検索キーワードに、「シンタックスハイライト」と入力して検索を実行すると、

のように、想定通りの検索結果が返ってきました。ちなみに、「シンタックス」だけで検索すると、検索結果は0件でした。microCMSの全文検索の仕様でしょうか?

最後に

今回、AWSのLambdaを使用して、microCMSの全文検索機能を実現してみました。
作っているときに、これって、microCMSのAPIの一部機能を、API-KEY無しでコールできるようにしているのでは?という疑問を持ちましたが、全文検索する以上やむを得ないですし、Lambda側でmicroCMSのAPI本来の機能を一部隠蔽しているので、API-KEYをブラウザに持たせるよりは全然よいかなと最後には思いました。
まだ、プレビュー機能を実装していないですが、プレビュー機能も同じようにLambda経由でコールするようにするか、もしくは、プレビュー用にアクセスコントロールしたホスティング環境をAmplifyで別途作成し、そちらで、API-KEYを渡して実現しようかなと思います。

参考資料