Astroで作る静的サイトに、超高速のAlgoliaの検索システムを導入する
前回、MeilisearchをAstroに導入しましたが、日本語の漢字語彙の検索にまだ少し難があるため、Algoliaも試しました。
Algoliaは、ドキュメント数10,000・月10,000サーチまでが無料となっています。個人や小規模のサイトには十分ですが、中規模以上になると料金がかさむというのが巷の評価(?)ですね。
コードの書き方に多少の違いがあるものの、流れ的には、Meilisearchの導入の手順と同じです。
- Algoliaにユーザー登録
 - Astroプロジェクト内にalgoliasearchをインストール
 - 検索用のデータを構築してAlgoliaに送信
 - 検索フォーム・検索結果表示用コンポーネントを作成
 - ページ内で検索コンポーネントを読み込む
 - スタイリング
 
動作環境:
- Node v18.12.1
 - Astro v2.0.11
 - algoliasearch v4.14.3
 - dotenv v16.0.3
 
尚、今回の例で利用するAlgoliaのライブラリは、JavaScriptだけで動く「InstantSearch.js v4」です。
リンク What is InstantSearch.js? | Algolia
今回は試していませんが、Algoliaには、React用・Vue用ライブラリ等もあります。
Astroプロジェクトの構造
今回は、以下の構造でAstroサイトを作ることとします。
src/
└─ pages/
     └─ posts/
          ├─ first-post.md
          ├─ second-post.md
          └─ ...さらに、MarkdownのデータのYAML frontmatterは以下ようにしています。
---
title: My first post
slug: first-post
---
dignissimos aperiam dolorem qui eum facilis quibusdam animi sint suscipit qui sint possimus cum quaerat magni maiores excepturi ipsam ut commodi dolor voluptatum modi aut vitaeAlgoliaに登録
セルフホストも可能なMeilisearchと違い、Algoliaはクラウド版のみです。
トップページの「START FREE」から登録を進めましょう。
© Algolia
    
    
確認メールで認証リンクを押せば登録完了です。
Algolia上でアプリケーションの作成
最初にログインをすると、アプリケーションのセットアップ画面に自動で移動します。
アプリケーション内に「インデックス(箱)」を作り、その中に検索データとなる「レコード(リスト)」を入れる仕組みです。
インデックス名は今回、「devposts」としました。テスト用、本番用として、インデックス名の頭に「dev」「prod_」等で分けることが推奨されています。
© Algolia
    
    
インデックス作成が完了したら、セッティング画面へ行き、アプリケーション名を変更しておきましょう。
© Algolia
    
    
© Algolia
    
    
© Algolia
    
    
レコード編集用のAPI keyの作成
Algoliaは、クラウド上でレコードを手動追加したり、JSONファイルなどをアップロードすることでレコードを追加できますが、今回はリモートでレコードを追加します。
そのため、リモートで編集可能なAPI KEYが必要となります。マスターキーでも操作可能ですが、セキュリティのため書き換え専用のAPIキーを作ります。
先ほどの「API KEYS」の画面から、「All API keys」タブを開き、「New API key」ボタンをクリック。
© Algolia
    
    
先ほど作成したインデックス(今回の例では「dev_posts」)を指定し、一番下の「ACL」で、
- addObject
 - deleteObject
 
を選択。
© Algolia
    
    
「Create」で作成したら、API keyを控えておきます。
Astroにalgoliasearchとdotenvをインストール
ここからはAstroプロジェクトでの作業です。
AstroでAlgoliaを利用するために、プロジェクト内にalgoliasearchをインストールします。
# npmの場合
npm install algoliasearch
# yarnの場合
yarn add algoliasearchさらに、環境変数をjsファイル内で扱うため、dotenvをインストールします。
# npmの場合
npm install dotenv
# yarnの場合
yarn add dotenv検索用データの構築
次に、検索データを構築・送信するため、ファイルを作成します。
- libフォルダー内に、
algoliasearch.js(ファイル名、ファイルの場所は任意) - ルート直下に
.env 
src/
├─ pages/
│    │  └─ ...
│    ├─ posts/
│    │    ├─ first-post.md
│    │    ├─ second-post.md
│    │    └─ ...
│    └─ lib/
│         └─ algoliasearch.js <--これと
├─ .env <--これ.envファイルの編集
.envファイルに、以下の環境変数を入れておきます。
ALGOLIA_APP_ID=xxxxxxxxxx
ALGOLIA_SEARCH_ONLY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ALGOLIA_WRITE_API_KEY=xxxxxxxxxxxxALGOLIA_WRITE_API_KEYは、先ほど作成したAPI keyです。その他のデータは、ホームボタン(Overview)→「API keys」で確認可能。
© Algolia
    
    
© Algolia
    
    
algolia.jsの作成
次に、Algoliaに作った「インデックス」に入れるデータ「レコード」を送るためのJavaScriptファイルを作成します。
基本形
Alogoliaにデータを送信するためのコードの基本形は、こんな感じです。
import algoliasearch from "algoliasearch"
const client = algoliasearch("アプリID", "編集用APIキー")
client.initIndex("インデックス名").saveObjects("JSONデータ")
// .then((res) => console.log(res))Meilisearchの時とほぼ同じです。「JSONデータ」の部分に、必要なデータを投稿から集めてAlgoliaに送信します。
dotenvをインポート
algolia.jsのファイル冒頭で、dotenvを有効にします。
import * as dotenv from "dotenv"
dotenv.config()
// 続く送信部分を記述
続いて、骨格部分を追加。
// 続き
import algoliasearch from "algoliasearch"
const client = algoliasearch(
  process.env.ALGOLIA_APP_ID,
  process.env.ALGOLIA_WRITE_API_KEY
)
// 1. ここでレコードを作る(後述)
// 2. JSONデータを作ってから送信
client
  .initIndex("dev_posts")
  .saveObjects("JSONデータ")
  .then(res => console.log(res)) //送信結果表示用検索用データセットの作成
次に、検索用のデータセット(records)を作ります。
今回はMarkdownによる投稿を例としています。外部CMSを使っている場合はfetch()等でデータを取得するなど、適宜アレンジしてください。
Markdownのタグを除去するため、remove-markdownを利用しています。必要な場合はインストールしてください。
// 続き
// 1. ここでJSONデータを作る
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import removeMd from "remove-markdown"
const filenames = fs.readdirSync(path.join("./src/posts"))
const data = filenames.map(filename => {
  try {
    const markdownWithMeta = fs.readFileSync("./src/posts/" + filename)
    const { data: frontmatter, content } = matter(markdownWithMeta)
    return {
      objectID: frontmatter.slug,
      slug: frontmatter.slug,
      title: frontmatter.title,
      content: removeMd(content).replace(/\n/g, ""),
    }
  } catch (e) {
    // console.log(e.message)
  }
})
// 2. JSONデータを作ってから送信
// 略ポイントは以下の通り。
import.meta.glob()はここでは動かないため、fs・path・matterを使用(インストール不要)objectIDは必須だが、saveObjects()のオプションで自動生成可能。今回はslugをobjectIDとして利用- ここでは
contentを使い、全文を取得。slice()などを使って短くしても良い 
送信データを代入
作ったdataをJSON形式にして、saveObjects()に投入。
// 続き
// 2. JSONデータを作ってから送信
client
  .initIndex("dev_posts")
  .saveObjects(JSON.parse(JSON.stringify(data)))
  .then(res => console.log(res))algolia.jsコードまとめ
import * as dotenv from "dotenv"
dotenv.config()
import algoliasearch from "algoliasearch"
const client = algoliasearch(
  process.env.ALGOLIA_APP_ID,
  process.env.ALGOLIA_WRITE_API_KEY
)
// 1. ここでJSONデータを作る
import fs from "fs"
import path from "path"
import matter from "gray-matter"
import removeMd from "remove-markdown"
const filenames = fs.readdirSync(path.join("./src/posts"))
const data = filenames.map(filename => {
  try {
    const markdownWithMeta = fs.readFileSync("./src/posts/" + filename)
    const { data: frontmatter, content } = matter(markdownWithMeta)
    return {
      id: frontmatter.slug,
      title: frontmatter.title,
      content: removeMd(content).replace(/\n/g, ""),
    }
  } catch (e) {
    // console.log(e.message)
  }
})
// 2. JSONデータを作ってから送信
client
  .initIndex("dev_posts")
  .saveObjects(JSON.parse(JSON.stringify(data)))
  .then(res => console.log(res))以上でalgolia.jsは完成です。
検索用データ(records)を送信
algolia.jsファイルができたら、Nodeを使って実行します。
Astroプロジェクトのルートで、以下を実行。※algolia.jsを違う場所に置いたり他のファイル名にした場合は、その場所とファイル名を指定。
node src/lib/algolia.js無事にデータが送信完了すると、ファイル内に記述したconsole.log(res)によって、以下のように表示されます。
{
  taskIDs: [ 125508379002 ],
  objectIDs: [ 'third-post', 'second-post', 'first-post' ]
}Algoliaのダッシュボードに移動して、インデックスを確認してみましょう。登録されていますね🙂
© Algolia
    
    
検索結果を表示するコンポーネントの作成
srcフォルダー直下のcomponentsディレクトリ(なければ作成)下に、検索ボックス+検索結果を表示するコンポーネントを作成。ここではファイル名を「Search.astro」としました。
src/
├─ components/
│    └─ Search.astro <-- これ
├─ pages/
│    ├─ posts/
│    │    ├─ first-post.md
│    │    ├─ second-post.md
│    │    └─ ...
│    └─ lib/
│         └─ algoliasearch.js
├─ .envAstroにMeilisearchを導入した際とほぼ同様に、こんな風にしてみました。
<div class="wrapper">
  <div id="searchbox"></div>
  <div id="hits"></div>
</div>
<script
  is:inline
  src="https://cdn.jsdelivr.net/npm/[email protected]/dist/algoliasearch-lite.umd.js"
  integrity="sha256-dImjLPUsG/6p3+i7gVKBiDM8EemJAhQ0VvkRK2pVsQY="
  crossorigin="anonymous"
></script>
<script
  is:inline
  src="https://cdn.jsdelivr.net/npm/[email protected]/dist/instantsearch.production.min.js"
  integrity="sha256-3s8yn/IU/hV+UjoqczP+9xDS1VXIpMf3QYRUi9XoG0Y="
  crossorigin="anonymous"
></script>
<script is:inline>
  const search = instantsearch({
    indexName: 'dev_posts',
    searchClient: algoliasearch(
      import.meta.env.ALGOLIA_APP_ID,
      import.meta.env.ALGOLIA_SEARCH_ONLY_API_KEY
    ),
  })
  search.addWidgets([
    instantsearch.widgets.searchBox({
      container: '#searchbox',
    }),
    instantsearch.widgets.hits({
      container: '#hits',
      templates: {
        item: `
          <a href='/{{#helpers.highlight}}{ "attribute": "slug" }{{/helpers.highlight}}/'>
            <h2 class="hit-name">
              {{#helpers.highlight}}{ "attribute": "title" }{{/helpers.highlight}}
            </h2>
            <p>{{#helpers.highlight}}{ "attribute": "content" }{{/helpers.highlight}}...</p>
          </a>
      `,
      },
    }),
  ])
  search.start()
</script>注意⚠️Astroで外部のCDNスクリプトを利用する場合、is:inlineを使ってコンポーネント内でスクリプトを走らせることになります。そうするとHTML内にスクリプトが挿入されることになり、ページの表示速度が損なわれます。
このコンポーネントを他のコンポーネントやテンプレート内で読み込めばOKです。
表示は以下のようになります。
      
  
    
モーダル表示用のコンポーネントを作って、その中でこのSearch.astroを読み込んで表示させるのがいいですね。なるべくBodyの閉じタグ直前で読み込むとベター。
スタイルを適用させる
スタイルの適用方法としては、いくつか選択肢があります。
- クラス名を確認して自分で作る
 - reset.cssまたはsatellite.cssを読み込む(npmまたはCDN)
 
クラス名を確認して自分で作る
コンポーネント内に表示されていないクラス名は、is:globalを使って適用させます。
<!-- 続き -->
<style is:global>
  .ais-Hits-item {
    margin-bottom: 1em;
  }
</style>satellite.cssを読み込む
参考 Style your widgets | Algolia
インストールする場合
# npmの場合
npm install instantsearch.css
# yarnの場合
yarn add instantsearch.css---
// リセットCSSのみ
import 'instantsearch.css/themes/reset.css'
// または、サテライトテーマ(リセットCSS含む)
import 'instantsearch.css/themes/satellite.css'
---
<div class="wrapper">
  <div id="searchbox"></div>
  <div id="hits"></div>
</div>
// ...CDNで読み込む場合
<link
  rel="stylesheet"
  href="https://cdn.jsdelivr.net/npm/[email protected]/themes/satellite-min.css"
  integrity="sha256-TehzF/2QvNKhGQrrNpoOb2Ck4iGZ1J/DI4pkd2oUsBc="
  crossorigin="anonymous"
/>表示例
      
  
    
日本語用の設定
日本語で利用する場合は、インデックスの設定で日本語がきちんと読み込まれるように設定しておきます。
左メニューの「Search」から、Indexの「Configuration」タブを開き、「Language」に行きます。
「Index Languages」と「Query Languages」に「Japanese」を追加して、保存します。
© Algolia
    
    
補足
AlgoliaはReactやVueのライブラリも提供しているため、Astroにそれらをインストールすれば、もっと作業が楽になるかもしれません。
MeilisearchがAlgoliaとの互換性を考えて作られているため、ひとまずAlgoliaで作っておいて、あとで無料プランのレコード数の多いMeilisearchへの移行もスムーズです。