Skip to content

Instantly share code, notes, and snippets.

@x7ddf74479jn5
Last active August 3, 2023 07:12
Show Gist options
  • Save x7ddf74479jn5/e9b8c3761aa72ac5ca9b05a5e4928f3f to your computer and use it in GitHub Desktop.
Save x7ddf74479jn5/e9b8c3761aa72ac5ca9b05a5e4928f3f to your computer and use it in GitHub Desktop.
AstroにMeilisearch(全文検索)を導入する
tags createdAt updatedAt
astro
meilisearch
2023-02-19 16:09:10 +0900
2023-02-20 16:12:12 +0900

AstroにMeilisearch(全文検索)を導入する

Algolia互換のMeilisearchをブログに組み込んだのでそのときの実装メモ。MeilisearchはAlgoriaの一部APIには非互換。その反面、instansearchには対応しており、UIが簡単に実装できる部分は共通。フリープランではAlogoliaの方はPowerd By Algoliaを明示しなければならないが、MeilisearchはOSSであるため不要なのもいい。1

TOC

方針

  • Astro V2のコンテンツコレクションを使用
  • Astroのビルド時にMeilisearchのIndexを更新する
    • Index更新用のスクリプト(参考実装)を別途作成してビルドプロセスから分離させた方が取り扱いやすいが、AstroとViteの環境に乗っかりたい。
  • Reactコンポーネント
  • UIはinstantsearchを使うが、CSSは独自
    • 初めはinstantsearchのテーマを利用するつもりだったが、ダークテーマに対応しておらずTailwindでの上書きがやりづらかったので、後述のreact-instantsearch-hooks-webのUI Widgetを通してスタイリングする方法を選択した。2

環境

node 18.14.1
@astrojs/react 2.0.2
@meilisearch/instant-meilisearch 0.11.0
astro 2.0.12
meilisearch 0.31.1
react 18.2.0
react-dom 18.2.0
react-instantsearch-hooks-web 6.40.0


DocumentをIndexに追加する

バンドラーのビルド時についでにIndex更新する方法を採ったので、ViteのAPI(import.meta.env, import.meta.glob())とAstroのAPI(getCollection())が使える。

saveSearchIndex関数を作成

import type { CollectionEntry } from 'astro:content'
import { MeiliSearch } from 'meilisearch'
import removeMd from 'remove-markdown'

// server-only
const client = new MeiliSearch({
  host: import.meta.env.PUBLIC_MEILISEARCH_HOST,
  apiKey: import.meta.env.MEILISEARCH_MASTER_KEY
})

export type SearchDocument = Pick<
  CollectionEntry<'posts'>['data'],
  'title' | 'description' | 'tags'
> & { id: string; slug: CollectionEntry<'posts'>['slug'] }

export const saveSearchIndex = (data: CollectionEntry<'posts'>[]) => {
  const docs: SearchDocument[] = data.map(({ body, data, slug }) => {
    return {
      id: slug,
      slug,
      title: data.title,
      description: data.description,
      tags: data.tags,
      content: removeMd(body).replace(/\n/g, '')
    }
  })

  client
    .index('posts')
    .addDocuments(docs)
    .then(res => console.log(res))
}

// 確認用
  client
    .index('posts')
    .search('search word')
    .then(res => console.log(res))

Documentの型が検索のヒット時にMeilisearchから渡ってくる。idはoptionalでIDっぽいもの(e.g. objectId)が含まれていればMeilisearchがよしなに設定してくれる。ユニークであればいいので今回は明示的にslugを設定。

saveSearchIndex関数をindex.astroで呼び出す

if (import.meta.env.PROD) {
  saveSearchIndex(posts)
}

index.astroのコードフェンス内に追記。実装初期段階以外で余分なインデックス更新を走らせたくないので、環境変数の振り分けでプロダクションビルドに限定。

別のやり方

更新用のスクリプトを書く。こちらの実装例を参考にできる。AstroとViteの便利APIが使えないデメリット、インデックス更新をビルドプロセスから分離できるメリットが挙げられる。インデックス更新では課金されないが。3

UI実装

スタイリング方法は以下の2通り

  1. 予約済みのclassNameにCSSをあてがう
  2. UI WidgetのclassNamesにコンポーネントの内部パーツごとのCSSを設定する

今回はもともとTailwindを採用していたので相性のいい2の方法を採用する。

Clientの設定

import { instantMeiliSearch } from '@meilisearch/instant-meilisearch'

export const searchClient = instantMeiliSearch(
  import.meta.env.PUBLIC_MEILISEARCH_HOST,
  import.meta.env.PUBLIC_MEILISEARCH_SEARCH_KEY,
  {
    placeholderSearch: false,
    primaryKey: 'id'
  }
)

フロントエンド用クライアントを別途定義する。placeholderSearch: falseを設定すると、コンポーネントのマウント時に空文字でクエリが飛ぶ挙動を抑制できる。

meilisearch/instant-meilisearch: The search client to use Meilisearch with InstantSearch.

スタイリング例

<Hits
 hitComponent={HitComponent}
 classNames={{
  list: 'py-1',
  item: 'rounded-md shadow-none block my-2 p-0 hover:bg-zinc-500'
 }}
/>

UI Widgetの詳細はこちら -> React InstantSearch Hooks Reference | Algolia

別のやり方

予約済みのclassにCSSを当て込む方法。なお、instantsearchのテーマを使う場合はCSSの読み込み順に注意する。例えば、Next.jsで外部CSSファイルを読み込む場合、_app.tsx_document.tsxでグローバルに読み込む。対して、AstroはMPAなので必要なコンポーネント内で読み込まざるを得ない。コンポーネント内で読み込んだとしてもCSSのclass名はグローバルに公開される。コンポーネント内でCSSをimportした場合、優先度はグローバルのCSS定義 < コンポーネントのCSS定義となる。つまり、上書きするつもりで書いたCSSが逆にテーマのCSSに上書きされる。これを回避したい場合、@layerで優先度を明示的に定義する方法がある。

@import '~instantsearch.css/themes/satellite-min.css' layer(satellite);

.ais-Hits-list {
  @apply my-2;
}

優先度は、後に宣言したレイヤーが優先度が高くなる。また、レイヤーを明示的に宣言しなければ無名レイヤーとして宣言した形になり、他のレイヤーより優先度が高くなる。単純な例


Links

References

Footnotes

  1. MeilisearchはAlgoliaほど多機能ではないが無料枠が大きく小規模の利用なら十分採用可能。Algolia互換を意識しているため、高機能なAPIを使っていないなら移植も簡単だろう。FirebaseのIntegrationにも対応している。気をつける点では、2023年2月20現在で日本リージョンがない(最近でシンガポール)。利点は、OSSであるため商用利用可能でセルフホスティングもできるところ。

  2. 複数のクラスがカスケードしているときに当たるCSSがあり、オーバーライドを困難にしている。基本的にはライトモードで環境でCSSの大幅な改変がない要件が想定されている。

  3. Pricingはドキュメント容量と検索APIの利用で課金。100K Documentsと10K search/monthまで無料枠。Meilisearch | Pricing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment