tags | createdAt | updatedAt | ||
---|---|---|---|---|
|
2023-02-19 16:09:10 +0900 |
2023-02-20 16:12:12 +0900 |
Algolia互換のMeilisearchをブログに組み込んだのでそのときの実装メモ。MeilisearchはAlgoriaの一部APIには非互換。その反面、instansearchには対応しており、UIが簡単に実装できる部分は共通。フリープランではAlogoliaの方はPowerd By Algoliaを明示しなければならないが、MeilisearchはOSSであるため不要なのもいい。1
- Astro V2のコンテンツコレクションを使用
- Astroのビルド時にMeilisearchのIndexを更新する
- Index更新用のスクリプト(参考実装)を別途作成してビルドプロセスから分離させた方が取り扱いやすいが、AstroとViteの環境に乗っかりたい。
- Reactコンポーネント
- UIはinstantsearchを使うが、CSSは独自
- 初めはinstantsearchのテーマを利用するつもりだったが、ダークテーマに対応しておらずTailwindでの上書きがやりづらかったので、後述の
react-instantsearch-hooks-web
のUI Widgetを通してスタイリングする方法を選択した。2
- 初めはinstantsearchのテーマを利用するつもりだったが、ダークテーマに対応しておらずTailwindでの上書きがやりづらかったので、後述の
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
バンドラーのビルド時についでにIndex更新する方法を採ったので、ViteのAPI(import.meta.env
, import.meta.glob()
)とAstroのAPI(getCollection()
)が使える。
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
を設定。
if (import.meta.env.PROD) {
saveSearchIndex(posts)
}
index.astro
のコードフェンス内に追記。実装初期段階以外で余分なインデックス更新を走らせたくないので、環境変数の振り分けでプロダクションビルドに限定。
更新用のスクリプトを書く。こちらの実装例を参考にできる。AstroとViteの便利APIが使えないデメリット、インデックス更新をビルドプロセスから分離できるメリットが挙げられる。インデックス更新では課金されないが。3
スタイリング方法は以下の2通り
- 予約済みのclassNameにCSSをあてがう
- UI WidgetのclassNamesにコンポーネントの内部パーツごとのCSSを設定する
今回はもともとTailwindを採用していたので相性のいい2の方法を採用する。
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;
}
優先度は、後に宣言したレイヤーが優先度が高くなる。また、レイヤーを明示的に宣言しなければ無名レイヤーとして宣言した形になり、他のレイヤーより優先度が高くなる。単純な例。
- はじめに🚀Astroドキュメント
- React / Next.jsにalgoliaを導入して検索機能を追加しよう | fwywd(フュード)powered by キカガク
- meilisearch/instant-meilisearch: The search client to use Meilisearch with InstantSearch.
- React InstantSearch Hooks Reference | Algolia
- Meilisearch
Footnotes
-
MeilisearchはAlgoliaほど多機能ではないが無料枠が大きく小規模の利用なら十分採用可能。Algolia互換を意識しているため、高機能なAPIを使っていないなら移植も簡単だろう。FirebaseのIntegrationにも対応している。気をつける点では、2023年2月20現在で日本リージョンがない(最近でシンガポール)。利点は、OSSであるため商用利用可能でセルフホスティングもできるところ。 ↩
-
複数のクラスがカスケードしているときに当たるCSSがあり、オーバーライドを困難にしている。基本的にはライトモードで環境でCSSの大幅な改変がない要件が想定されている。 ↩
-
Pricingはドキュメント容量と検索APIの利用で課金。100K Documentsと10K search/monthまで無料枠。Meilisearch | Pricing ↩