api-search-meilisearch について
api-search-meilisearchは、TypeScriptプロジェクトでMeilisearchを使うためのガイド型スキルです。アプリ検索を実装する開発者向けに、非同期インデックス処理、設定、フィルター、ソート、複数インデックス検索、マルチテナントのアクセス方式を扱います。
主な機能
- 01Meilisearch TypeScriptクライアントの設定
- 02非同期インデックス処理とタスク管理
- 03フィルター・ソート・検索属性の設定
- 04フィルター・地理・複数インデックス検索
- 05テナントトークンによるアクセス制御
活用シーン
- 01アプリへの全文検索とオートコンプリート追加
- 02フィルターやファセット付きナビゲーション構築
- 03近隣検索と距離順ソートの実装
- 04複数インデックスやテナントをまたぐ検索
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ agents-inc/ skills --skill api-search-meilisearchインストール後、Claude Code で/api-search-meilisearchを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
metadata.yamlreference.mdexamples/core.mdfiltering.mdsecurity.mdsettings.md詳細
Meilisearch のパターン
クイックガイド: Meilisearch v1.x の TypeScript クライアントには
meilisearch(v0.56 以降)を使用します。すべての書き込み操作(ドキュメントの追加、設定の変更、インデックスの作成)は非同期です。EnqueuedTaskPromiseを返し、バックグラウンドキューで処理されます。検索クエリでフィルターやソートを使用する前に、インデックスにfilterableAttributesとsortableAttributesを設定する必要があります。設定すると、インデックス全体が再作成されます。遅延参照(ネットワーク呼び出しなし)にはclient.index("name")を使用し、サーバーから取得するにはclient.getIndex("name")を使用します。.waitTask()はスクリプト、シード、テストでのみEnqueuedTaskPromiseに対して使用してください。リクエストハンドラーでは決して使用しないでください。
<critical_requirements>
重要:このスキルを使用する前に
すべてのコードは CLAUDE.md のプロジェクト規約(kebab-case、名前付きエクスポート、インポート順序、
import type、名前付き定数)に従う必要があります。
(検索クエリで filter を使用する前に、インデックスに filterableAttributes を設定する必要があります。属性が filterableAttributes に含まれていないと、フィルターは結果を返さず、エラーも通知されません。)
(検索クエリで sort を使用する前に、インデックスに sortableAttributes を設定する必要があります。未設定の属性によるソートは無視され、エラーも通知されません。)
(本番環境のリクエストハンドラーで .waitTask() を呼び出してはいけません。タスクが完了するまで Meilisearch をポーリングしてイベントループをブロックします。スクリプト、シード、テストでのみ使用してください。)
(ドキュメントに id フィールドがない場合は、プライマリーキーを明示的に設定する必要があります。Meilisearch がプライマリーキーを自動推定するのは最初のドキュメント追加時のみです。誤った推定は、後続のバッチ処理でインデックス作成の失敗を引き起こします。)
</critical_requirements>
使用例
- 基本パターン -- クライアントのセットアップ、ドキュメント操作、検索の基本、タスク管理、TypeScript 統合
- フィルタリングとファセット -- フィルター構文、ファセット検索、地理検索、ソート可能な属性
- インデックス設定 -- ランキングルール、タイポ許容、同義語、ストップワード、検索可能な属性、ページネーション
- セキュリティとマルチテナンシー -- API キー、テナントトークン、検索ルール、マルチテナントパターン
追加リソース:
- reference.md -- 検索パラメーターの早見表、設定のデフォルト値、判断フレームワーク、アンチパターン
自動検出: Meilisearch、meilisearch、MeiliSearch、meilisearch-js、client.index、addDocuments、updateDocuments、multiSearch、filterableAttributes、sortableAttributes、searchableAttributes、rankingRules、typoTolerance、tenant token、generateTenantToken、EnqueuedTaskPromise、waitTask、facets、_geoRadius、_geoBoundingBox、_geoPoint、instantsearch
使用する場面:
- アプリケーションに全文検索を追加する場合(商品検索、コンテンツ検索、オートコンプリート)
- ファセットナビゲーションを実装する場合(カテゴリーの絞り込み、価格帯、属性の件数)
- 地理情報を利用した検索を構築する場合(近隣検索、距離順の並べ替え)
- テナントが1つのインデックスを共有しながら、それぞれのドキュメントのみを参照できるマルチテナント検索
- 複数のインデックスを同時に検索する場合(マルチサーチ、フェデレーション検索)
- タイポ許容機能を備えたインスタント検索で、ドキュメントをリアルタイムにインデックス化する場合
対象となる主なパターン:
- クライアントの初期化と接続管理
- 非同期タスク処理を伴うドキュメントの CRUD 操作
- フィルタリング、並べ替え、ファセット、ハイライトを使った検索
_geoRadius、_geoBoundingBox、距離による並べ替えを使った地理検索- インデックスをまたいだマルチサーチとフェデレーション検索
- インデックス設定(ランキングルール、タイポ許容、同義語、ストップワード)
- マルチテナントのアクセス制御に使うテナントトークン
- 型付き検索結果のための TypeScript ジェネリクス
使用しない場面:
- リレーショナルデータベースでの全文検索(簡単なケースでは、データベースに組み込まれた全文検索を使用してください)
- ログ集約や分析クエリ(専用のログ/分析検索エンジンを使用してください)
- キーワードを使わないベクトルのみのセマンティック検索(専用のベクトルデータベースを使用してください)
- ドキュメントが約1,000件未満の場合(クライアント側でのフィルタリングのほうが簡単です)
基本理念
Meilisearch は検索エンジンであり、データベースではありません。ドキュメントをインデックス化してすばやく取得できるようにしますが、信頼できる唯一の情報源ではありません。基本原則は次のとおりです。
- すべてを非同期で処理する -- すべての書き込み操作(ドキュメント、設定、インデックス管理)はキューに登録され、非同期で処理されます。API はすぐにタスク ID を返します。インデックス化が即座に完了することを前提としないアプリケーション設計にしてください。
- 検索前に設定する -- フィルター可能な属性、ソート可能な属性、検索可能な属性は、検索クエリで使用する前に設定する必要があります。この設定により、すべてのドキュメントが再インデックス化されます。
- デフォルトでタイポを許容する -- Meilisearch は初期状態でタイポに対応します。手動で完全一致を実装しようとするのではなく、特定のフィールド(商品コード、シリアル番号)でタイポ許容を無効にするには、
typoTolerance設定を調整してください。 - 主キーは重要です -- すべてのドキュメントに一意の主キーが必要です。Meilisearch は最初のドキュメントから自動的に推測しますが、暗黙的に設定するより明示的に設定するほうが適切です。インデックスの作成時に設定してください。
- クエリではなく検索を使う -- Meilisearch は人間による検索クエリ(タイポ許容、前方一致、ランキング)に最適化されています。SQL の代替ではありません。構造化クエリにはフィルターを、自然言語には検索を使用してください。
基本パターン
パターン 1:クライアントの設定
ホストと API キーを指定してクライアントを初期化します。遅延評価されるローカル参照には client.index() を使用します(ネットワーク呼び出しは発生しません)。サーバーにアクセスする client.getIndex() よりこちらを優先してください。
// Good Example -- Typed client setup
import { Meilisearch } from "meilisearch";
function createSearchClient(): Meilisearch {
const host = process.env.MEILISEARCH_URL;
const apiKey = process.env.MEILISEARCH_API_KEY;
if (!host) {
throw new Error("MEILISEARCH_URL environment variable is required");
}
return new Meilisearch({ host, apiKey });
}
export { createSearchClient };
良い理由: 環境変数を検証し、名前付きエクスポートを使用しています。apiKey は省略可能です(Meilisearch では開発環境で認証なしのアクセスが許可されています)。
// Bad Example -- Hardcoded credentials
import { Meilisearch } from "meilisearch";
const client = new Meilisearch({
host: "http://localhost:7700",
apiKey: "masterKey123",
});
悪い理由: ホストと API キーをハードコードすると、バージョン管理に漏えいします。マスターキーも公開されます(本番環境ではスコープを設定した API キーを使用してください)。
ヘルスチェック、AbortController の使用方法、カスタムリクエスト設定については、examples/core.md を参照してください。
パターン 2:ドキュメントのインデックス登録(非同期)
すべてのドキュメント操作は EnqueuedTaskPromise を返します。ドキュメントはすぐには検索可能にならず、タスクキューに追加されます。
// Good Example -- Add documents with explicit primary key
interface Product {
productId: string;
name: string;
description: string;
price: number;
categories: string[];
}
const index = client.index<Product>("products");
// First add: set primary key explicitly
const task = await index.addDocuments(products, { primaryKey: "productId" });
// task.taskUid: number -- use this to track progress
良い理由: 主キーを明示することで自動推論の問題を防ぎ、TypeScript のジェネリック型によってドキュメントの形状に型安全性を持たせています。
// Bad Example -- Relying on auto-inference
const index = client.index("products");
await index.addDocuments(products); // No primary key specified
// If first document has both 'id' and 'productId', Meilisearch guesses wrong
悪い理由: Meilisearch は最初のドキュメントから主キーを推論します。推測が誤っていると、その後の追加がすべて主キーの競合によって失敗する可能性があります。
更新、削除、バッチ処理、タスク管理のパターンについては、examples/core.md を参照してください。
パターン 3:フィルターを使った検索
フィルターを使うには、あらかじめ filterableAttributes を設定する必要があります。フィルター構文では、AND/OR/NOT と SQL に似た演算子を使用します。
// Good Example -- Search with filter and sort
const MIN_PRICE = 10;
const MAX_PRICE = 100;
const results = await index.search("wireless headphones", {
filter: `price >= ${MIN_PRICE} AND price <= ${MAX_PRICE} AND categories = "electronics"`,
sort: ["price:asc"],
limit: 20,
});
// results.hits: Product[], results.estimatedTotalHits: number
良い理由: フィルター値に名前付き定数を使用し、テキスト検索と構造化フィルタリングを組み合わせ、上限を明示しています。
// Bad Example -- Filtering without configuring filterableAttributes
const index = client.index("products");
// MISSING: await index.updateFilterableAttributes(["price", "categories"])
const results = await index.search("headphones", {
filter: "price < 50", // Returns 0 results -- silently fails!
});
悪い理由: 属性が filterableAttributes に含まれていない場合、エラーなしでフィルター結果が空になります。これは Meilisearch で最もよくある落とし穴です。
ファセット検索、地理フィルター、高度なフィルター構文については、examples/filtering.md を参照してください。
パターン 4:マルチ検索
1 回のリクエストで複数のインデックスを検索します。フェデレーション検索では、結果を統合リストにまとめます。
// Good Example -- Multi-search across indexes
const results = await client.multiSearch({
queries: [
{ indexUid: "products", q: "laptop", limit: 5 },
{ indexUid: "articles", q: "laptop review", limit: 5 },
],
});
// results.results[0].hits -- products
// results.results[1].hits -- articles
// Federated search -- merged results
const federated = await client.multiSearch({
federation: {},
queries: [
{ indexUid: "products", q: "laptop" },
{ indexUid: "articles", q: "laptop" },
],
});
// federated.hits -- single merged list sorted by relevance
良い理由: 複数のインデックス検索を 1 回のネットワークリクエストで実行し、フェデレーション検索によってインデックスを横断した統一ランキングを実現しています。
クエリの重み付けを使ったフェデレーション検索については、examples/core.md を参照してください。
パターン 5:タスク管理
書き込み操作は非同期です。タスク UID を使って進行状況を追跡してください。.waitTask() はスクリプトとテストでのみ使用してください。
// Good Example -- Task tracking in a seed script
const task = await index.addDocuments(products, {
primaryKey: "productId",
});
// In scripts/seeds: wait for completion
const completed = await task.waitTask();
if (completed.status === "failed") {
throw new Error(`Indexing failed: ${completed.error?.message}`);
}
console.log(`Indexed ${completed.details?.indexedDocuments} documents`);
良い理由: .waitTask() をリクエストハンドラーではなくシードスクリプトで使用し、エラーステータスを確認して、タスクの詳細を調べています。
// Bad Example -- Waiting in a request handler
app.post("/products", async (req, res) => {
const task = await index.addDocuments([req.body]);
await task.waitTask(); // BLOCKS the request until Meilisearch processes the task!
res.json({ success: true });
});
悪い理由: .waitTask() は Meilisearch を繰り返しポーリングし、リクエストハンドラーをブロックします。キューの深さによっては、タスクに数秒から数分かかることがあります。
バッチタスク管理とタスクステータスのポーリングについては、examples/core.md を参照してください。
パターン 6:インデックス設定の構成
設定を変更すると、インデックス全体が再作成されます。再インデックスを避けるため、ドキュメントを追加する前に設定を行ってください。
// Good Example -- Configure index before adding documents
const index = client.index("products");
// Step 1: Configure settings (triggers re-index)
await index
.updateSettings({
filterableAttributes: ["price", "categories", "brand", "inStock"],
sortableAttributes: ["price", "createdAt"],
searchableAttributes: ["name", "description", "brand"],
rankingRules: [
"words",
"typo",
"proximity",
"attribute",
"sort",
"exactness",
"price:asc",
],
typoTolerance: {
disableOnAttributes: ["sku", "barcode"],
},
synonyms: {
phone: ["smartphone", "mobile"],
laptop: ["notebook"],
},
})
.waitTask(); // OK in setup script
// Step 2: Add documents (indexes with correct settings)
await index.addDocuments(products, { primaryKey: "productId" });
良い理由: ドキュメントを追加する前に設定することで、二重の再インデックスを防いでいます。完全一致させるフィールドではタイプミス許容を無効にし、一般的な別名の同義語を定義しています。
すべての設定オプション、ストップワード、ページネーションの設定については、examples/settings.md を参照してください。
<decision_framework>
意思決定フレームワーク
どの検索方法を選ぶべきですか?
What kind of search do I need?
-- Single index, text query? -> index.search(query, options)
-- Multiple indexes, separate results? -> client.multiSearch({ queries })
-- Multiple indexes, merged results? -> client.multiSearch({ federation: {}, queries })
-- Browse/filter without text? -> index.search("", { filter, sort }) (placeholder search)
フィルターと検索のどちらを選ぶべきですか?
How should users find data?
-- Natural language, typo-tolerant? -> Use the `q` parameter (search)
-- Exact attribute matching? -> Use `filter` parameter
-- Both? -> Combine: search("query", { filter: "category = 'X'" })
-- Browsing without a query? -> Placeholder search: search("", { filter, sort })
どのページネーション方式を選ぶべきですか?
How should I paginate results?
-- Infinite scroll / load more? -> Use offset + limit (default)
-- Page numbers (page 1, 2, 3)? -> Use page + hitsPerPage
-- NOTE: Default maxTotalHits is 1000 -- increase in pagination settings if needed
どのタスク管理方式を選ぶべきですか?
How should I handle async operations?
-- Seed script / migration? -> .waitTask() is fine
-- Test setup? -> .waitTask() to ensure data is ready
-- API request handler? -> Fire-and-forget, return task UID to client
-- Need confirmation? -> Return taskUid, let client poll GET /tasks/:uid
</decision_framework>
<red_flags>
危険信号
優先度の高い問題:
- 事前に
filterableAttributes/sortableAttributesを設定せずにフィルタリングまたはソートすること――フィルターは結果を何も返さず、ソートは無視されます - 本番環境のリクエストハンドラーで
.waitTask()を使用すること――イベントループをブロックし、高負荷時にリクエストがタイムアウトします - クライアント側のコードでマスターキーを使用すること――管理者権限へのフルアクセスが公開されます。検索専用の API キーまたはテナントトークンを使用してください
- プライマリーキーを明示的に設定しないこと――Meilisearch は最初のドキュメントから自動推論し、誤ったフィールドを選ぶ可能性があるため、以降のインデックス作成がすべて失敗する原因になります
優先度が中程度の問題:
- ドキュメントを追加した後に設定すること――すべてのドキュメントの再インデックスが発生し、大規模なデータセットでは数分かかることがあります
- デフォルトの
maxTotalHits: 1000ページネーション制限を超えること――検索結果は警告なく 1000 件で上限に達します。より深いページネーションが必要な場合は、設定のpagination.maxTotalHitsで上限を増やしてください - フィルターで括弧を使わずに
AND/ORを使用すること――ANDはORより優先順位が高いため、予期しないフィルター結果になります - タスクの失敗を処理しないこと――失敗したタスクではインデックスは変更されませんが、タスクのステータスを確認しないとエラーがわかりません
注意点とエッジケース:
- 地理検索では
filterableAttributesに_geoを含める必要があります――_geoフィールドを含むドキュメントを追加するだけでは不十分で、この属性を明示的に指定する必要があります client.index("name")はインデックスを作成せず、存在も確認しません――ローカル参照を返すだけです。実際に作成するにはclient.createIndex("name")を使用してください- 空文字列での検索(
search(""))は有効な「プレースホルダー検索」です――フィルターに一致するすべてのドキュメントを返すため、ブラウジングやファセットナビゲーションに便利です - Meilisearch のタスクキューには約 10 GiB の上限があります――キューがいっぱいになると、新しい書き込み操作は
no_space_left_on_deviceで失敗します。完了済みのタスクは定期的に削除してください - 同義語はフィルターには適用されません――同義語として設定されていても、「phone」でフィルタリングして「smartphone」を含むドキュメントが一致することはありません
_geoフィールドの形式は厳密です。{ lat: number, lng: number }である必要があります――lngの代わりにlongitudeを使うと、invalid_document_geo_fieldエラーが発生します- 設定の変更(filterableAttributes など)もタスクとしてキューに追加されます――即時には反映されません。新しい設定を利用する前に、タスクの完了を待ってください
hitsPerPageとpageパラメーターはoffset/limitより優先されます――同じクエリで両方のページネーション方式を併用しないでください- デフォルトの
maxTotalHitsは 1000 です――offsetベースのページネーションでも、この設定を増やさなければ 1000 件目より後のドキュメントにはアクセスできません
</red_flags>
<critical_reminders>
重要な注意事項
すべてのコードは CLAUDE.md のプロジェクト規約(kebab-case、名前付きエクスポート、インポート順序、
import type、名前付き定数)に従う必要があります
(検索クエリで filter を使用する前に、インデックスで filterableAttributes を必ず設定してください。属性が filterableAttributes に含まれていない場合、フィルターは結果を何も返しません)
(検索クエリで sort を使用する前に、インデックスで sortableAttributes を必ず設定してください。設定されていない属性でのソートは無視されます)
(本番環境のリクエストハンドラーで .waitTask() を呼び出してはいけません。タスクが完了するまで Meilisearch をポーリングしてイベントループをブロックします。スクリプト、シード、テストでのみ使用してください)
(ドキュメントに id フィールドがない場合は、プライマリーキーを明示的に設定する必要があります。Meilisearch がプライマリーキーを自動推論するのは最初のドキュメント追加時のみで、誤った推論は後続のバッチ処理でインデックス作成エラーの原因になります)
これらのルールに従わないと、検索が警告なく失敗し、リクエストのタイムアウトやインデックス作成エラーが発生します。
</critical_reminders>