記事が52本になったところで、サイト内検索の効きを確かめようと思った。検索APIに「データが更新」と入れたら、返ってきたのは0件。

この6文字は、公開中の記事タイトル「構造化データが更新で壊れるCMSを、壊れない運用設計にする」にそのまま入っている。索引は張られている(/_emdash/api/search/stats は blog_posts 52件と返す)。記事も公開済み。それでも0件でした。

犯人は記事でも索引の張り忘れでもなく、本文をどこで切って索引にするかという設定です。ここを既定のまま日本語サイトに検索を付けると、たまに当たるけれど、たいてい当たらない検索ができあがります。

「検索が当たらない」の正体は、文を切る場所

全文検索は、本文を単語(トークン)に切ってから索引を作ります。英語なら簡単で、スペースが区切りになる。contact form は contact と form の2語。

日本語にはその区切りがない。お問い合わせフォームのスパムを減らす、という文には空白も記号もないので、切れ目が見つからないまま、ひとかたまりで索引に入ります。

自サイト(EmDash・公開52本・トークナイザは既定のまま)で、同じ検索APIに投げた結果です。

  • 「構造化」→ 6件。当たる
  • 「データが更新」→ 0件
  • 「フォーム」→ 14件。当たる
  • 「わせフォーム」→ 0件

当たった2つは、かたまりの先頭から始まる文字列。落ちた2つは、途中から始まる文字列。どちらも記事タイトルにそのまま含まれている点は変わりません。

読者が検索窓に打ち込むのは、たいてい途中の言葉です。「フォーム」と過不足なく打ってくれる人ばかりではない。この状態の検索窓は、置いてあるだけで実質は機能していません。

直し方は、切り方(トークナイザ)を替えること

SQLite の全文検索(FTS5)を土台にしたCMSなら、この切り方=トークナイザを選べます。自サイトも Cloudflare D1(中身はSQLite)で動かしています(EmDashをCloudflareにデプロイする)。

EmDash の既定は porter unicode61。英語の語形変化を吸収する(correction と corrected を同じ語として扱う)Porterステミング付きで、名前のとおり英語向けです。

日本語には trigram を指定する。3文字ずつ、1文字ずらしながら索引を作る方式なので、文の途中からの一致を拾えます。EmDash の公式ドキュメントも、日本語・中国語・タイ語のように語を空白で区切らない言語には trigram を挙げている。

トークナイザ比較。既定のporter unicode61は途中一致が落ち、trigramは途中一致を拾うが2文字以下は当たらない

ただし、どこから指定するかはバージョンで変わります。自サイトが動かしている 0.32 で確かめたら、切り替えのスイッチは見当たりませんでした(次の節)。

索引を張り直しても、記事側は触りません。既存サイトに後から入れる場合でも、原稿の修正は発生しない。

前提として、コレクションの supports に search があり、拾わせたいフィールドに searchable が立っている必要があります。自サイトはタイトル・要約・本文の3つを対象にしています。

{
  "slug": "blog_posts",
  "supports": ["drafts", "revisions", "search", "seo"],
  "fields": [
    { "slug": "title",   "type": "string",       "searchable": true },
    { "slug": "summary", "type": "text",         "searchable": true },
    { "slug": "content", "type": "portableText", "searchable": true }
  ]
}

どのフィールドを検索に載せるかは、そのままヘッドレスCMSのフィールド設計の話につながります。本文のリッチテキスト1本に全部詰め込んでいると、検索対象を絞ることもできません。

EmDash 0.32 に切り替えスイッチは無かった(2026年9月に追記)

この記事を公開したあと、自サイトの検索を実際に切り替えようとして手が止まりました。ドキュメントには tokenize の指定が載っているのに、動かしている 0.32 のコードにはその口が見当たりません。

  • 検索を有効化する POST /_emdash/api/search/enable が受け取るのは collection / enabled / weights の3つだけ。トークナイザを渡す口が無い(src/api/schemas/search.ts)
  • FTS5のテーブルは tokenize='porter unicode61' を直書きして作られる(src/search/fts-manager.ts)

APIからは切り替えられない、ということです。やるなら索引そのものをSQLで張り直す。Cloudflare D1(中身はSQLite)ならこうなります。

DROP TABLE IF EXISTS "_emdash_fts_blog_posts";
CREATE VIRTUAL TABLE "_emdash_fts_blog_posts" USING fts5(
  id UNINDEXED, locale UNINDEXED, title, summary, content,
  content='ec_blog_posts', content_rowid='rowid', tokenize='trigram'
);
INSERT INTO "_emdash_fts_blog_posts"(rowid, id, locale, title, summary, content)
  SELECT rowid, id, locale, title, summary, content FROM ec_blog_posts WHERE deleted_at IS NULL;

手元のDBを複製して試したところ(記事13本のローカル環境)、「フォーム」の一致が1件から3件に増えました。途中一致を拾い始めた分です。引き換えに、2文字の「予約」は2件から0件へ落ちた。冒頭の比較図のとおりのトレードオフが、そのまま数字に出ます。

張り直すときに引っかかるのが3つ。

  • 同期用のトリガーも作り直す ― テーブルを DROP するとトリガーも道連れで消えます。気づかずにいると、記事を更新しても索引に反映されません
  • 再構築APIを叩くと元に戻る ― /search/rebuild はテーブルを落として porter unicode61 で作り直す実装なので、手で入れた trigram は消えます
  • 有効化APIは上書きしない ― /search/enable の方は CREATE VIRTUAL TABLE IF NOT EXISTS なので、すでにある索引はそのまま残ります
EmDash 0.32で日本語検索を通す手順と注意(索引の確認、SQLで張り直す、トリガー再作成、rebuildで戻る)

CMS本体を上げるたびにここを見にいく必要がある、という話でもあります。自サイトは読者向けの検索窓を出していないので既定のまま動かしていますが、検索を機能として出すサイトなら、CMSを選ぶ段階でトークナイザを指定できるかを確認しておきたいところ。

trigram にも穴がある。2文字以下は当たらない

万能ではありません。SQLite の公式ドキュメントは、trigram について「3文字未満の部分文字列はどの行にも一致しない」と明記しています。3文字ずつで索引を作る仕組みなので、2文字では索引の単位に届かない。

日本語には2文字の語がふつうにあります。経費、人事、予約、求人。BtoBサイトほど踏みやすい。

しかも、既定のままなら当たっていた短い語が、切り替えると落ちます。先ほどの自サイトで「フォ」の2文字を投げると19件返ってきた。trigram に替えると、これは0件になる。途中一致を取る代わりに短い語を捨てる、というトレードオフです。

穴はUI側で吸収します。

  • 2文字以下が入力されたら「3文字以上で検索してください」と出す。0件の白い画面より親切
  • サジェスト(/_emdash/api/search/suggest)を併用し、打ち終わる前に候補を見せる
  • タグ・カテゴリの一覧導線を検索窓のそばに置く。短い語ほど、検索より一覧のほうが速い

形態素解析まで求めるなら外部の検索サービスという手もある。ただ記事数が数十から数百のサイトなら、trigram と導線の組み合わせで足りることが多いと感じています。

検索が当たらないときの切り分け4段階。索引にあるか、公開済みか、先頭一致だけか、3文字未満か

検索を付ける前に、決めておく3つ

実装を渡す前にここが決まっていると、そのまま組めます。

  1. 何を検索対象にするか。コレクションとフィールド。本文だけか、要約やタグも拾うか。対象を広げるほどノイズも増える
  2. 下書きを含めるか。EmDash の検索APIは既定で公開済みだけを返します。管理画面の中で探したいのか、公開サイトの読者に出したいのかで別物になる(下書きの扱いはヘッドレスCMSのプレビュー実装でも触れています)
  3. 順位と見せ方。weights でタイトルを本文より重くできる。マッチ箇所は <mark> タグ付きのスニペットで返るので、ハイライト表示は実装側で組む

決めごとをどの粒度で渡すかという点では、コーディング指示書の書き方と同じ話です。検索も例外ではなく、あとで決めるのがいちばん後ろに響きます。

検索を付ける前に決めておく3つ。検索の対象、下書きの扱い、順位と見せ方

検索が当たらない、と言われたとき、記事の書き方やCMSの乗り換えを疑う前に、索引の切り方を見てください。設定ひとつで直る場合があります。

この手の切り分けと修正だけを切り出して頼みたい制作会社の方は、お問い合わせからご相談ください。既存サイトの改修・高速化も同じ入口で受けています。

※本記事は2026年8月に公開し、2026年9月にEmDash 0.32での挙動を確かめて加筆しました。