症状
ログに search_api_db の警告が大量に溜まっていました。
An overlong word (more than 50 characters) was encountered while indexing: %word. Since database search servers currently cannot index words of more than 50 characters, the word was truncated for indexing. If this should not be a single word, make sure the “Tokenizer” processor is enabled and configured correctly for index %index.
「50文字を超える単語があった。データベース検索サーバーは50文字を超える単語を索引できないので、索引用に切り詰めた」。警告文の後半は「これが1単語でないなら Tokenizer プロセッサを有効にして正しく設定せよ」と、対処法まで書いてくれています。
問題は「切り詰めた」の部分です。これは単なる警告ではなく、本文の51文字目以降が索引から落ちているという報告でした。検索しても本文中の語で当たらない、という実害が裏で起きていたわけです。
なぜ日本語だけで起きるか
上限は search_api_db のバックエンド実装にハードコードされた定数です。
// search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php
protected const TOKEN_LENGTH_MAX = 50;
この値はそのまま索引テーブルの word カラムの長さにも使われています。英語なら単語がスペースで区切られているので、1単語が50文字を超えることはまずありません。日本語の本文には区切り文字がないので、一文がそのまま1単語として渡ります。数百文字の段落が1トークンになり、50文字で切られる。
つまりこれは「日本語を入れた時にだけ顔を出す」タイプの罠です。
一度目の対策は効かなかった
警告文の指示通り、Tokenizer プロセッサを有効にしました。設定画面には「Simple CJK handling」というチェックボックスがあり、これが CJK(漢字・ひらがな・カタカナ)を n-gram に分割してくれるものです。有効にして再インデックスしました。
警告は消えませんでした。
設定は入っている。CJK handling も有効になっている。それでも一文が1トークンのまま切り詰められている。
真因: プロセッサは weight の昇順で走る
Search API のプロセッサには weight があり、昇順で実行されます。この時の構成はこうなっていました。
| プロセッサ | weight |
|---|---|
| transliteration | -10 |
| tokenizer | (transliteration より後) |
transliteration が先に走っていました。これが何をするかというと、テキストをラテン文字へ変換します。日本語に対して何が起きるか、実際に叩いてみます。
$t = \Drupal::transliteration();
$t->transliterate('会社', 'ja'); // huishe
$t->transliterate('案件', 'ja'); // anjian
$t->transliterate('譲渡', 'ja'); // rangdu
$t->transliterate('事業', 'ja'); // shiye
$t->transliterate('日本語', 'ja'); // ribenyu
会社 が huishe。案件 が anjian。これは日本語の読み(kaisha、anken)ではなく、中国語のピンインです。しかも第2引数に ja を明示しても変わりません。
なぜ langcode に ja を渡しても効かないのか
Drupal core が同梱している変換テーブルを見ると理由が分かります。
// core/lib/Drupal/Component/Transliteration/data/x4f.php
// 「Generic transliteration data for the PhpTransliteration class」
$base[0x1A] = 'hui'; // 会 U+4F1A
// core/lib/Drupal/Component/Transliteration/data/x79.php
$base[0x3E] = 'she'; // 社 U+793E
漢字はコードポイント単位で汎用テーブルに登録されていて、その読みが中国語です。日本語の漢字と中国語の漢字は Unicode 上で同じコードポイントを共有しているので、コードポイントだけを見るテーブルでは区別できません。
そして言語別の上書きテーブルは、core には5つしかありません。
core/lib/Drupal/Component/Transliteration/data/
da.php de.php eo.php kg.php uk.php
ja.php は存在しません。だから 'ja' を渡しても上書きするものがなく、汎用テーブル(=中国語読み)にそのまま落ちます。
ピンイン化されると CJK 分割が「黙って」効かなくなる
ここが繋がる部分です。Tokenizer の CJK 分割は、Unicode の CJK 範囲に対する正規表現で実装されています。
// search_api/src/Plugin/search_api/processor/Tokenizer.php
$text = preg_replace_callback('/[' . $this->getPregClassCjk() . ']+/u', [$this, 'expandCjk'], $text);
getPregClassCjk() が返すのは \x{3040}-\x{309F}(ひらがな)、\x{30A1}-\x{318E}(カタカナ)、\x{4E00}-\x{9FCF}(漢字)といったCJK のコードポイント範囲です。マッチした部分だけが expandCjk() に渡され、n-gram に分割されます。
transliteration が先に走ると、テキストは huisheanjian... のような ASCII の羅列になっています。CJK のコードポイントは1つも残っていない。だから preg_replace_callback は何にもマッチせず、expandCjk() は一度も呼ばれません。
エラーも警告も出ません。CJK 分割は「対象が無かった」として静かに何もしないだけです。結果、ローマ字化された長い列が1トークンとして残り、また50文字で切り詰められる。設定画面のチェックボックスは有効なままなので、設定を見ている限り原因に辿り着けません。
対策: tokenizer を transliteration より前に置く
weight を変えて、CJK 分割が transliteration より先に走るようにします。
processor_settings:
ignorecase:
weights: { preprocess_index: -20, preprocess_query: -20 }
all_fields: true
html_filter:
weights: { preprocess_index: -15 }
all_fields: true
tokenizer:
weights: { preprocess_index: -12, preprocess_query: -12 }
all_fields: true
overlap_cjk: 1
minimum_word_size: 2
transliteration:
weights: { preprocess_index: -10, preprocess_query: -10 }
all_fields: true
tokenizer を -12、transliteration を -10。この順序なら、まず CJK が n-gram(minimum_word_size: 2 なので2文字ずつ)へ分割され、その短いトークンに対して transliteration が走ります。
3点補足します。
preprocess_index と preprocess_query の両方に設定する。 索引側だけ直しても、検索クエリ側が別の順序で処理されると当たりません。
html_filter も入れる。 本文が full_html の場合、タグがそのまま索引されます。こちらは索引時のみ(preprocess_index)で十分です。
expandCjk() は minimum_word_size 以下の長さの語を分割しない。 実装がこうなっています。
if ($length <= $min) {
return ' ' . $str . ' ';
}
minimum_word_size: 2 なら2文字の語はそのまま通ります。ここを大きくしすぎると短い語が n-gram にならず取りこぼすので、日本語では2が扱いやすいです。
反映手順: config を入れただけでは直らない
設定を反映しても、既存の索引は古いままです。作り直しが必要です。
drush cim -y
drush search-api:clear <index_id>
drush search-api:index <index_id>
search-api:clear を挟まずに index だけ叩くと、既に索引済みのアイテムは skip されるので何も変わりません。再インデックス後にログを見て、search_api_db の overlong 警告が新規に出ないことを確認します。古い警告が残っていると判断を誤るので、確認前に掃除しておくと分かりやすいです。
drush watchdog:delete --type=search_api_db -y
本番でも同じで、デプロイスクリプトが config を取り込んでも再インデックスはしてくれません。clear と index は明示的に実行する必要があります。ここを忘れると「設定は入っているのに検索結果が変わらない」で、もう一周悩むことになります。
まとめ
search_api_dbの overlong 警告は「切り詰めた」=索引からの脱落報告。 単なる警告ではなく検索漏れの実害が出ている- 上限50文字は定数
TOKEN_LENGTH_MAX。 区切り文字のない日本語では一文が1トークンになるので簡単に超える - Search API のプロセッサは weight の昇順実行。
tokenizerがtransliterationより後だと、CJK 分割が効かない - Drupal の transliteration は日本語の漢字を中国語ピンインにする。
会社→huishe。core にja.phpの上書きテーブルが無いため、langcode にjaを渡しても変わらない - ローマ字化後は CJK のコードポイントが残らないので、CJK 分割は黙って何もしない。 エラーが出ないため設定画面からは原因が見えない
tokenizer: -12/transliteration: -10の順に置く。preprocess_indexとpreprocess_queryの両方に設定する- config を反映しても既存の索引は古いまま。
search-api:clearしてからsearch-api:indexする。デプロイ自動化に含まれていないことが多い
この記事は、事業・Webサイトの売買プラットフォーム RIKKA M&A を開発する中で得た知見です。RIKKA M&A は GitHub リポジトリを解析する技術デューデリジェンスを備え、Drupal 11 / PHP / LLM で構築・運用しています。

