algolia+instantsearchで全文検索に対応した

#Gatsby.js#algolia
2025/01/05
ArticleImage:01JGT8DR23JG0SZ4AH5AVAMZBM

はじめに

gatsby-plugin-algoliagatsby-plugin-algolia と react-instantsearchreact-instantsearch を利用して当ブログに記事を全文検索できる機能を追加しました
この記事含めまだ3件しか書いていないので全文検索を活用するべく2025年はブログの1年にしていきたい所存

ページのIndex

レコードが10KBalgolia-record-sizeに収まるよう本文を3000文字に絞っています
異体字etcに関してはフォントをサブセット化している関係上、当ブログではそもそも使用できないため考慮していません
絵文字も性格上あまり使わない気がするので多分大丈夫

Copy
import type { GatsbyConfig } from "gatsby";
import { parse } from "@formkit/tempo";

require("dotenv").config({
  path: `.env.${process.env.NODE_ENV}`,
});

const config: GatsbyConfig = {
  siteMetadata: {
    title: "blog.miyamo.today",
    siteUrl: "https://blog.miyamo.today",
    lang: "ja",
  },
  graphqlTypegen: true,
  plugins: [
    {
      resolve: `gatsby-plugin-algolia`,
      options: {
        appId: process.env.GATSBY_ALGOLIA_APP_ID,
        apiKey: process.env.ALGOLIA_ADMIN_KEY,
        indexName: process.env.GATSBY_ALGOLIA_INDEX_NAME,
        queries: [
          {
            query: `{
              allMarkdownRemark(filter: { frontmatter: { id: { ne: "Noop" } } }) {
                nodes {
                  excerpt(pruneLength: 3000, truncate: true)
                  frontmatter {
                    id
                    title
                    tags {
                      name
                    }
                    thumbnail
                    createdAt
                  }
                }
              }
              site {
                siteMetadata {
                  title
                  siteUrl
                  lang
                }
              }
            }`,
            transformer: ({
              data: { site, allMarkdownRemark },
            }: {
              data: { site: SiteMetadataForAlgolia; allMarkdownRemark: GetAllArticlesForAlgoria };
            }) =>
              allMarkdownRemark.nodes.flatMap((node) => {
                return {
                  id: node.frontmatter?.id ?? "",
                  content: node.excerpt ?? "",
                  title: node.frontmatter?.title ?? "",
                  publishedAt: parse(
                    node.frontmatter?.createdAt ?? "1970-01-01T00:00:00Z",
                    "YYYY-MM-DDTHH:mm:ssZ",
                    "en"
                  ),
                  tags: (() => {
                    return node.frontmatter?.tags
                      ?.filter((tag) => tag && typeof tag.name === "string" && tag.name.length > 0)
                      .flatMap((tag) => tag?.name ?? "")
                      .map((tagName) => tagName);
                  })(),
                  hierarchy: {
                    lvl0: site?.siteMetadata?.title ?? "",
                    lvl1: node.frontmatter?.title ?? "",
                  },
                  thumbnail: node.frontmatter?.thumbnail ?? "",
                  type: "lvl1",
                  url: `${site?.siteMetadata?.siteUrl ?? ""}/articles/${node.frontmatter?.id ?? ""}`,
                };
              }),
          },
        ],
        settings: {
          searchableAttributes: ["title", "content", "tags"],
          indexLanguages: ["ja"],
          queryLanguages: ["ja"],
          attributesToSnippet: [`content:10`],
        },
        mergeSettings: true,
        chunkSize: 10000,
        dryRun: process.env.ALGOLIA_DRY_RUN,
      },
    },
  ],
};

こだわり

yamada-uiのユーザーガイドyamada-ui-guideのようなダミーの検索窓をクリックすると検索モーダルが展開するUIにしてみました

yamada-ui ユーザーガイドの検索モーダル

blog.miyamo.todayの検索モーダル

またyamada-uiはユーザーガイドもOSSとして公開しているため実装面でも参考にすることができました

ハマったこと(IME対応)

algolia や instantsearch とは直接関係のないことですが
Gatsby.jsadding-search-with-algoliaのサンプルコード通りに
onChangeイベントでalgoliaへのリクエストを行うと全角入力の変換とバッティングしてしまうため
onCompositionStartCompositionEvent発火後は
onChangeイベントでは入力文字列のStateのみ更新し

onCompositionEnd内でalgoliaへのリクエストを行う形で対応しています

Copy
import React, { useState } from "react";
import { useSearchBox } from "react-instantsearch";
import { faSearch, faCancel} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@yamada-ui/fontawesome";
import { Input, InputGroup, InputLeftElement, InputRightElement } from "@yamada-ui/input";
import { IconButton } from "@yamada-ui/button";

export interface SearchBoxProps {}

const SearchBox = (props: SearchBoxProps) => {
  const { query, refine, clear } = useSearchBox();
  const [ enteredValue, setEnteredValue ] = useState(query);
  const [ compositionOngoing, setCompositionOngoing ] = useState(false);

  return (
    <InputGroup>
      <InputLeftElement>
        <FontAwesomeIcon icon={faSearch} />
      </InputLeftElement>
      <Input
        value={enteredValue}
        onChange={(e) => {
          setEnteredValue(e.target.value)
          if (compositionOngoing) {
            return;
          }
          refine(e.target.value);
        }}
        onCompositionStart={() => { setCompositionOngoing(true) }}
        onCompositionEnd={() => {
          setCompositionOngoing(false);
          refine(enteredValue);
        }}
      />
      <InputRightElement clickable>
        <IconButton
          icon={<FontAwesomeIcon icon={faCancel} />}
          onClick={() => {
            setEnteredValue("");
            clear();
          }}
        />
      </InputRightElement>
    </InputGroup>
  );
};

export default SearchBox;

仕事ではAWS CDK、Newman拡張、バリデーション処理ぐらいでしかJS/TSを触ったことがないのですがフロントエンド開発のあるあるネタなのでしょうか...

algoliaへのリクエスト数を絞るのもやぶさかではないのでインクリメンタルサーチ自体今後やめるかも


Recommend Articles

Gatsby.jsとgqlgenでブログを作った

はじめに 最初は単にGraphQL、gRPC、ECS、New Relic etc 自分が興味のある技術トピックでHello World Enterprise Editionをやるだけのつもりだったものの GraphQL -> ヘッドレスCMS -> ブログ という連想ゲームの結…

2024/12/23

ArticleImage:01JFT54WPPZ0ET3XTKZ0JTCMRE

Buy Me a Coffeeのwidgetを追加した話; widgetをコンテンツの最大幅に合わせる

はじめに このブログにBuy Me a Coffeeのwidgetを追加してみました Gatsby.jsでBuy Me a Coffeeのwidgetを利用する方法についてはこちらのブログを参考にさせていただきました Buy me a coffeeのウィジェットをGatsbyサ…

2025/01/10

ArticleImage:01JH6WW79W2EETG451N2W80ZPD

Copyright © miyamo2 All rights reserved.