Algoliaでサイト内検索を爆速化|InstantSearch・インデックス設計・料金まで
Algoliaのサイト内検索をClaude Codeで実装。InstantSearchのUI、安全なインデックス設計、secured APIキー、料金の見積もりを、僕の漏洩ヒヤリ体験つきで解説。
検索ボックスにキーワードを打って、結果が出るまで一拍待つ。あの「一拍」がイヤで、僕はサイト内検索をAlgoliaに載せ替えました。
ところが移行作業の途中、レコードをそのままコピーする雑なスクリプトを書いて、危うく下書き記事の本文を全部インデックスに突っ込むところでした。投入ボタンを押す寸前で気づいて、背中が冷たくなったのを覚えています。UIに出していなくても、検索APIからは丸見えになるんです。
Algoliaは「速い」だけが取り柄じゃない。むしろ怖いのは速さより、何をインデックスに入れるかの設計ミスです。今日はそのへんを、実際に動くコードと一緒に共有します。
この記事の要点
- Algoliaは検索専用インデックスにデータを送り、ミリ秒で候補を返すSaaS。
LIKEや全文検索で限界が来たサイトの「次の一手」 - 一番大事なのはインデックス設計。DBの行を丸ごと送らず、公開してよい属性だけに削る。これをサボると検索が情報漏洩の入口になる
- 管理キーは絶対にブラウザに出さない。ユーザーごとに範囲を絞るならサーバーで secured API key を生成する
- UIは InstantSearch のウィジェットを組むだけ。検索ボックス・絞り込み・ページネーション・ハイライトが数十行で揃う
- 料金は検索リクエスト数とレコード数で決まる従量制。小規模なら無料枠で十分回る。Insightsでログを見て育てるのが本番
そもそもAlgoliaは何が速いのか
普通の全文検索は、ユーザーが入力するたびにDBへ問い合わせます。データが増えるほど遅くなるし、表記ゆれやタイプミスの許容を自前で作り込むと、保守がどんどん重くなる。
Algoliaは発想が逆です。あらかじめ検索専用のインデックスを作っておいて、検索のたびにそのインデックスだけを引く。DBの負荷とは切り離されているので、レコードが数万件あってもレスポンスはミリ秒単位で安定します。タイプミスの許容(postgreでPostgreSQLが出る)も、語順の入れ替えも、ハイライト表示も、設定だけで効きます。
サイト内検索そのものの選び方、つまり「LIKE・DBの全文検索・あいまい検索・外部SaaSのどれを選ぶか」はサイト内検索の作り方にまとめました。この記事はその4択のうち「外部SaaSに寄せると決めた人」が、Algoliaで具体的に何をどう作るかに絞ります。
ちなみに「DBの検索が遅い」段階なら、まずパフォーマンス最適化で索引やクエリを見直すほうが安い場合もあります。Algoliaは便利ですが、課金が増える要素でもあるので、必要になってから入れるくらいで十分です。
Claude Codeに任せると何が変わるか
Algoliaの公式ドキュメントは充実しています。だから「コードを書く」だけなら、正直コピペでもなんとかなる。
Claude Codeを使う本当の価値は、既存のDBスキーマ、画面、権限、ログを読ませたうえで、検索レコードの形・インデックス設定・同期処理・UI・レビュー観点を一気通貫でそろえられる点にあります。僕の場合、「このスキーマから公開してよい属性だけ抜いて、Algoliaのレコード型を作って」と頼んで、漏れやすい属性を先に洗い出してもらいました。
ただし丸投げは禁物です。後で書く「やらかし」のとおり、雑に頼むとAIも雑なレコードを作ります。「何を入れないか」を人間が決めてから渡すのがコツです。
なおこの記事のコードは、2026年6月時点のAlgolia JavaScript API Client v5を前提にしています。v5では古いinitIndexパターンではなく、client.saveObjectsやclient.searchSingleIndexのように、クライアントにindexNameを渡す形へ移りました。最新の更新点は公式のJavaScript API Client v5で確認してください。
使いどころを3つに分ける
最初にユースケースを分けると、インデックス設計で迷わなくなります。僕は最初これを飛ばして、全部を1つのインデックスに詰めて後悔しました。
| ユースケース | 典型データ | 重要な設定 | 注意点 |
|---|---|---|---|
| ドキュメント検索 | 記事、見出し、本文、タグ | searchableAttributes、同義語、ハイライト | 下書きや社内メモを混ぜない |
| EC・教材カタログ | 商品名、カテゴリ、価格、在庫、人気度 | facets、customRanking、Insights | 価格・在庫の同期遅延を監視する |
| 社内ナレッジ検索 | FAQ、チケット、設計メモ | secured API key、filters、権限フィールド | 個人情報と秘密情報を入れない |
検索UIだけを急いで作るのではなく、「誰に、どの範囲を、どの順番で見せるか」を先に決める。これだけで後戻りが激減します。
レコード設計は検索用に削る(ここが本丸)
冒頭の僕のヒヤリは、まさにここの手抜きが原因でした。
Algoliaに送るレコードは、DBの行を丸ごとコピーしてはいけません。検索結果に必要な公開情報と、ランキング・絞り込みに必要な最小限の属性だけにします。email、住所、決済ID、内部メモ、未公開本文、APIレスポンスの生データは入れない。UIで表示していなくても、検索APIから取れてしまうからです。
{
"objectID": "article_ja_claude-code-algolia-search",
"title": "Algoliaでサイト内検索を爆速化",
"summary": "インデックス設計、検索UI、料金、レビューまでをまとめた実装ガイド",
"content": "公開済み本文から抽出した検索対象テキスト",
"locale": "ja",
"section": "blog",
"category": "use-cases",
"tags": ["Algolia", "InstantSearch", "サイト内検索"],
"visibility": "public",
"allowedTeams": [],
"slug": "claude-code-algolia-search",
"url": "/blog/claude-code-algolia-search",
"publishedAt": "2025-11-15",
"updatedAt": "2026-06-07",
"updatedAtTimestamp": 1780272000,
"popularity": 42,
"conversionScore": 7,
"readingMinutes": 12,
"thumbnail": "/images/hero/hero-090.png"
}
objectIDは安定させます。URL変更やタイトル変更のたびにIDが変わると、クリック分析とランキング改善の履歴が切れてしまう。公開記事ならarticle_ja_slug、商品ならproduct_12345のように、元データの主キーとロケールを組み合わせると扱いやすいです。
インデックス設定と投入スクリプト
ここからが動くコードです。npm install algoliasearch@5 dotenvを済ませ、.envにALGOLIA_APP_IDとALGOLIA_ADMIN_KEYを置いて実行します。管理キーは必ずサーバー側だけで使ってください。
// scripts/index-articles.ts
import "dotenv/config";
import { algoliasearch } from "algoliasearch";
type SearchRecord = {
objectID: string;
title: string;
summary: string;
content: string;
locale: "ja" | "en";
section: "blog" | "docs" | "product";
category: string;
tags: string[];
visibility: "public" | "restricted";
allowedTeams: string[];
slug: string;
url: string;
publishedAt: string;
updatedAt: string;
updatedAtTimestamp: number;
popularity: number;
conversionScore: number;
readingMinutes: number;
thumbnail: string;
};
const appId = process.env.ALGOLIA_APP_ID;
const adminKey = process.env.ALGOLIA_ADMIN_KEY;
const indexName = process.env.ALGOLIA_INDEX_NAME ?? "claudecodelab_articles";
if (!appId || !adminKey) {
throw new Error("ALGOLIA_APP_ID and ALGOLIA_ADMIN_KEY are required");
}
const client = algoliasearch(appId, adminKey);
// 公開してよい属性だけを手で組み立てる。DBの行を丸ごと渡さないのが鉄則
const records: SearchRecord[] = [
{
objectID: "article_ja_claude-code-algolia-search",
title: "Algoliaでサイト内検索を爆速化",
summary: "インデックス設計から料金・レビューまでを扱う実践記事",
content: "公開本文から抽出した検索対象テキストだけを入れます。",
locale: "ja",
section: "blog",
category: "use-cases",
tags: ["Algolia", "InstantSearch", "サイト内検索"],
visibility: "public",
allowedTeams: [],
slug: "claude-code-algolia-search",
url: "/blog/claude-code-algolia-search",
publishedAt: "2025-11-15",
updatedAt: "2026-06-07",
updatedAtTimestamp: 1780272000,
popularity: 42,
conversionScore: 7,
readingMinutes: 12,
thumbnail: "/images/hero/hero-090.png"
}
];
// 検索の効き方を決める設定。searchableAttributesは上にある属性ほど強く評価される
await client.setSettings({
indexName,
indexSettings: {
searchableAttributes: [
"unordered(title)",
"unordered(summary)",
"content",
"tags",
"category"
],
attributesForFaceting: [
"filterOnly(visibility)",
"filterOnly(locale)",
"filterOnly(allowedTeams)",
"searchable(category)",
"searchable(tags)",
"section"
],
customRanking: [
"desc(conversionScore)",
"desc(popularity)",
"desc(updatedAtTimestamp)"
],
attributesToRetrieve: [
"title",
"summary",
"locale",
"section",
"category",
"tags",
"url",
"updatedAt",
"thumbnail"
],
attributesToHighlight: ["title", "summary", "content"],
typoTolerance: true,
removeWordsIfNoResults: "lastWords"
}
});
// 表記ゆれを吸収する同義語。証拠が出てから足すのが基本
await client.saveSynonyms({
indexName,
synonymHit: [
{
objectID: "claude-code-names",
type: "synonym",
synonyms: ["Claude Code", "claude code", "クロードコード"]
},
{
objectID: "search-ja",
type: "synonym",
synonyms: ["検索", "全文検索", "サイト内検索"]
}
],
clearExistingSynonyms: true
});
const { taskID } = await client.saveObjects({
indexName,
objects: records
});
// 反映を待ってから検証する。待たずに「検索できない」と判断すると無駄な修正が増える
await client.waitForTask({ indexName, taskID });
console.log(`Indexed ${records.length} records into ${indexName}`);
attributesForFacetingは絞り込み対象です。visibilityやallowedTeamsのような権限に使う属性はfilterOnlyにして、ユーザーが検索候補として拾わないようにします。customRankingは同点だったときのタイブレーク。コンバージョン・人気・更新日の順で並べ替えると、古くて読まれない記事が上に来にくくなります。
検索APIとsecured API key
ここがキーの扱いで一番事故りやすいところです。
公開検索だけならブラウザに search-only key を置けます。でも管理キー、書き込みキー、Analytics管理キーは絶対にブラウザへ出しません。ログインユーザーごとに検索範囲を絞る場合は、サーバー側で secured API key を生成します。これは親キーから派生する仮想キーで、制約をユーザー側で外せないようにするためにサーバーで作るのがポイントです。詳しくは公式のAPI keysを見てください。
// app/api/search-key/route.ts
import { algoliasearch } from "algoliasearch";
import { NextResponse } from "next/server";
const appId = process.env.ALGOLIA_APP_ID!;
const searchKey = process.env.ALGOLIA_SEARCH_KEY!;
const indexName = process.env.ALGOLIA_INDEX_NAME ?? "claudecodelab_articles";
export async function GET() {
// 本来はセッションから取得する。ここでは例として固定値
const user = { id: "user_123", teamIds: ["training"] };
const client = algoliasearch(appId, searchKey);
// filtersはサーバーで埋め込む。クライアントからは書き換えられない
const securedApiKey = client.generateSecuredApiKey({
parentApiKey: searchKey,
restrictions: {
restrictIndices: indexName,
filters: `visibility:public OR allowedTeams:${user.teamIds[0]}`,
userToken: user.id,
validUntil: Math.floor(Date.now() / 1000) + 60 * 30
}
});
return NextResponse.json({ appId, indexName, apiKey: securedApiKey });
}
サーバー経由で検索結果だけ返したい場合は、次のようなエンドポイントにします。入力を短く制限し、clickAnalyticsを有効にしてqueryIDを返すと、後でクリック計測に使えます。検索APIの基本はSearch an indexが公式リファレンスです。
// app/api/search/route.ts
import { algoliasearch } from "algoliasearch";
import { NextRequest, NextResponse } from "next/server";
const client = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_SEARCH_KEY!
);
const indexName = process.env.ALGOLIA_INDEX_NAME ?? "claudecodelab_articles";
export async function GET(request: NextRequest) {
// 入力は80文字で打ち切る。長すぎるクエリやいたずらを抑える
const query = request.nextUrl.searchParams.get("q")?.slice(0, 80) ?? "";
const locale = request.nextUrl.searchParams.get("locale") ?? "ja";
const result = await client.searchSingleIndex({
indexName,
searchParams: {
query,
filters: `visibility:public AND locale:${locale}`,
hitsPerPage: 10,
attributesToRetrieve: ["title", "summary", "url", "category", "tags"],
clickAnalytics: true
}
});
return NextResponse.json({
hits: result.hits,
queryID: result.queryID,
nbHits: result.nbHits
});
}
InstantSearchでUIを作る
検索UIは、Algoliaのウィジェットを組み合わせると驚くほど短く作れます。InstantSearch.jsは、検索ボックス・絞り込み・ページネーション・ハイライトなどを組み合わせる公式のUIライブラリです。Reactならreact-instantsearchを使います。
// components/ArticleSearch.tsx
"use client";
import { liteClient as algoliasearch } from "algoliasearch/lite";
import {
Configure,
Highlight,
Hits,
InstantSearch,
Pagination,
RefinementList,
SearchBox,
Stats
} from "react-instantsearch";
type HitProps = {
hit: {
objectID: string;
title: string;
summary: string;
url: string;
category: string;
tags: string[];
updatedAt: string;
};
};
// ブラウザに置けるのは search-only key だけ
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
);
function HitCard({ hit }: HitProps) {
return (
<article className="rounded border p-4">
<a href={hit.url} className="font-bold">
{/* Highlightが一致語を太字にしてくれる */}
<Highlight attribute="title" hit={hit} />
</a>
<p className="mt-2 text-sm text-gray-600">
<Highlight attribute="summary" hit={hit} />
</p>
<p className="mt-2 text-xs text-gray-500">
{hit.category} ・ {hit.updatedAt}
</p>
</article>
);
}
export function ArticleSearch() {
return (
<InstantSearch searchClient={searchClient} indexName="claudecodelab_articles">
{/* filtersでUIからも公開記事だけに絞る。サーバー側のキー制約と二重で守る */}
<Configure
hitsPerPage={8}
filters="visibility:public AND locale:ja"
clickAnalytics
/>
<SearchBox placeholder="Claude Codeの記事を検索" />
<Stats />
<div className="mt-6 grid gap-6 md:grid-cols-[220px_1fr]">
<aside>
<h2 className="text-sm font-bold">カテゴリ</h2>
<RefinementList attribute="category" searchable />
<h2 className="mt-4 text-sm font-bold">タグ</h2>
<RefinementList attribute="tags" searchable />
</aside>
<main>
<Hits hitComponent={HitCard} />
<Pagination className="mt-6" />
</main>
</div>
</InstantSearch>
);
}
これだけで、入力するたびに候補が絞り込まれ、カテゴリやタグで facets 絞り込みができ、一致語がハイライトされるUIが完成します。デバウンスや途中キャンセルといった面倒な処理は、InstantSearchが内側で吸収してくれます。自前で全文検索UIを書いた経験があると、この短さに拍子抜けするはずです。
気になる料金の話
Algoliaを入れるか迷う一番の理由は、たぶん料金ですよね。僕も最初そこで止まりました。
Algoliaの課金は、ざっくり「検索リクエスト数」と「インデックスのレコード数」の従量制です(プラン詳細は変わるので必ず公式のPricingで最新を確認してください)。個人ブログや小規模サイトなら無料枠の範囲で回ることが多く、僕の検証サイトも今のところ無料枠に収まっています。
料金を膨らませない実装のコツは3つあります。1つ目、検索リクエストはユーザーのキー入力1打ごとに飛ぶので、デバウンスを効かせて無駄打ちを減らす(InstantSearchはこれをやってくれます)。2つ目、レコード数を増やさないために、下書きや古い不要データをインデックスに入れない。3つ目、不要になったオブジェクトは消す。この3つを意識するだけで、想定外の請求はだいぶ防げます。
逆に言うと、何でもかんでもインデックスに突っ込む雑な設計は、漏洩リスクだけでなく料金面でも損です。「削る設計」はコスト最適化でもあるわけです。
僕がやらかした失敗3つ
正直に書きます。最初のAlgolia導入は事故りかけました。
ひとつ目は、冒頭にも書いたDBの全カラムをそのまま投入しようとしたこと。検索に不要な秘密情報を入れると、UIで表示していなくても検索APIから取得されます。attributesToRetrieveを絞り、投入前の変換で不要属性を削るようにしてから、夜もぐっすり眠れるようになりました。
ふたつ目は、NEXT_PUBLIC_を付けた環境変数に管理キーを入れかけたこと。NEXT_PUBLIC_はブラウザに丸ごと出ます。ブラウザに置けるのは search-only key か、サーバーで生成した secured API key だけ。これを知らずに管理キーを置いていたら、誰でもインデックスを書き換えられる状態でした。
みっつ目は、waitForTaskを待たずに検証したこと。投入直後はまだ反映前なので、「検索できない」と勘違いして設定を何度もいじり回しました。反映を待ってから確認する、これだけで無駄な修正が消えました。
Claude Codeで検索を「育てる」
検索は作って終わりじゃありません。検索語、0件検索、クリック位置、コンバージョンを見て、ランキングや同義語を直す運用が本番です。clickAnalyticsを有効にした検索結果にはqueryIDが含まれるので、クリックイベントと結びつけます。
// lib/search-insights.ts
import aa from "search-insights";
aa("init", {
appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
apiKey: process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!,
useCookie: true
});
// 検索結果のクリックをAlgoliaに送る。queryIDで「どの検索の何位か」が紐づく
export function trackSearchClick(params: {
indexName: string;
objectID: string;
queryID: string;
position: number;
}) {
aa("clickedObjectIDsAfterSearch", {
eventName: "Article Clicked",
index: params.indexName,
queryID: params.queryID,
objectIDs: [params.objectID],
positions: [params.position]
});
}
ためたログは、そのままClaude Codeに渡すより、目的と評価基準を明示したプロンプトで渡すとレビューの粒度がそろいます。僕はこんなプロンプトを週次で回しています。harnessのような専門語は「エージェントの足場」と言い換えを指定すると、出力がブレません。
あなたはClaudeCodeLabの検索品質レビュアーです。
Algoliaの検索ログ、0件検索、上位10件、クリック率を見て、
次の形式で改善案を出してください。
前提:
- 対象読者はClaude Code初心者から実務導入担当者
- private fieldsはインデックスに追加しない
- 変更案はAlgolia settings、synonyms、record content、UIの4分類に分ける
出力:
| query | 問題 | 原因 | 変更案 | リスク | 優先度 |
特に見てほしい点:
- 期待記事が3位以内に入っているか
- 同義語で解決すべきか、本文やタイトルを直すべきか
- facetが多すぎて初心者を迷わせていないか
この流れにしてから、0件検索の改善・同義語の追加・記事の手直しを「同じ週次タスク」にまとめられるようになりました。
よくある質問
Q. AlgoliaとDBの全文検索、どっちを使えばいい? 数万件以下で権限が単純なら、まずDBの全文検索で十分です。表記ゆれ・日本語精度・絞り込み・クリック分析まで欲しくなったらAlgoliaに寄せる。判断軸の全体像はサイト内検索の作り方にまとめてあります。
Q. 料金はどのくらいかかる? 検索リクエスト数とレコード数による従量制です。小規模サイトなら無料枠で回ることが多いです。最新の条件は公式のPricingで必ず確認してください。デバウンスと不要レコード削除でコストはかなり抑えられます。
Q. 管理キーをフロントに置くと何が危ない?
インデックスの書き換え・削除が誰でもできてしまいます。ブラウザに置けるのは search-only key か、サーバー生成の secured API key だけ。NEXT_PUBLIC_を付ける環境変数には絶対に管理キーを入れないでください。
Q. ログインユーザーごとに検索範囲を変えたい。
サーバーで secured API key を生成し、filtersに権限条件を埋め込みます。クライアントからは制約を外せないので、社内ナレッジ検索でも安全に範囲を絞れます。
Q. InstantSearchを使わず自前UIでもいい? できますが、デバウンス・途中キャンセル・ハイライト・0件表示を自分で実装することになります。最初はInstantSearchで体験を固め、足りない部分だけ差し替えるのが早いです。
実際に試した結果
冒頭のヒヤリ以来、僕がAlgoliaで最初にやるのは「UIを作ること」ではなく「インデックスに入れない属性を決めること」になりました。
レコード設計とattributesToRetrieveを先に絞った構成ほど、UI実装後の手戻りが少ない。これは何サイトか作ってみての実感です。そしてClaude Codeに検索ログを渡してレビューさせる流れは、0件検索の改善・同義語の追加・記事の手直しを同じ週次作業にまとめられるので、記事の品質改善とも相性が良かった。
速いAlgoliaに飛びつく前に、削る設計を済ませる。遠回りに見えて、漏洩も料金も事故らない一番速い道だ、というのが今の結論です。
検索導線の設計やClaude Codeの実務導入をまとめて相談したい方は研修・相談、手を動かす教材が欲しい方は教材一覧もどうぞ。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。