はじめに
2026年10月15日のLakebaseのリリースノートで、Lakebase Searchが中国語・日本語・韓国語の全文検索に対応しました。キーワード検索の単語分割を担う lakebase_tokenizer 拡張機能に、形態素解析で文を単語に分ける機能が追加されています。
8月に書いたLakebase Searchの記事では、日本語の本文に to_tsvector('english', ...) をかけると文全体が1語になり、Postgres で検索しても1件もマッチしないことを確認しました。その記事では、UC側で形態素解析して分かち書きした列を同期する、という対応方針を書いて締めくくっていました。
今回の対応で、この前処理なしに、Lakebaseの中だけで日本語のBM25検索ができるようになっています。前回と同じ文で比べるところから、複合語の扱いの選び方まで書いていきます。
何が変わったのか
PostgreSQL標準の全文検索は、空白や記号を手がかりに文を単語に分けます。日本語は単語の間に空白を入れないので、文が分割されずに丸ごと1語として扱われていました。
lakebase_tokenizer 0.2.0では、形態素解析で文を単語に分ける tokenizer_morph テンプレートが追加されました。ドキュメントによると、解析にはLinderaを使い、日本語の辞書にはIPADICが同梱されています。
拡張機能を入れると、次の3つのテキスト検索設定が最初から作られます。辞書を自分で作らなくても、to_tsvector にそのまま渡せます。
| 設定 | 言語 | 複合語の扱い |
|---|---|---|
tokenizer_chinese |
中国語 | mixed |
tokenizer_japanese |
日本語 | discard |
tokenizer_korean |
韓国語 | discard |
出力は標準の tsvector なので、GINインデックスにも、lakebase_text の lakebase_bm25 インデックスにも使えます。
準備
検証は、前回の記事で作ったLakebase Search有効化済みのプロジェクトで、LakebaseのSQL Editorから行いました。プロジェクトでLakebase Searchを有効にする手順は前回の記事に書いています。
ここが最初のハマりどころです。Lakebase Searchを有効にしていないプロジェクトで lakebase_text をインストールしようとすると、次のエラーになります。私は最初、別のプロジェクトに接続したまま実行してこのエラーに当たりました。
ERROR: [lakebase_text] lakebase_text must be loaded via shared_preload_libraries. (SQLSTATE XX000)
lakebase_text はPostgresの起動時に読み込まれている必要がある拡張機能で、この読み込みはプロジェクトの設定でLakebase Searchを有効にしたときに構成されます。SQLからは変えられないので、エラーが出たらプロジェクトの設定を確認してください。
有効化済みのプロジェクトで、拡張機能をインストールします。lakebase_tokenizer はドキュメントどおり tokenizer_ext スキーマに入れます。
-- BM25 検索の拡張機能
CREATE EXTENSION IF NOT EXISTS lakebase_text;
-- 単語分割の拡張機能。tokenizer_ext スキーマに入れる
CREATE SCHEMA IF NOT EXISTS tokenizer_ext;
CREATE EXTENSION IF NOT EXISTS lakebase_tokenizer WITH SCHEMA tokenizer_ext;
-- 0.1.x を入れていた場合は 0.2.0 以降に上げる。既存の設定やインデックスはそのまま使える
ALTER EXTENSION lakebase_tokenizer UPDATE;
バージョンを確認します。
SELECT extname, extversion
FROM pg_extension
WHERE extname IN ('lakebase_text', 'lakebase_tokenizer');
| extname | extversion |
|---|---|
| lakebase_text | 0.1.3 |
| lakebase_tokenizer | 0.2.1 |
lakebase_tokenizer は0.2.1が入りました。日本語に対応したのは0.2.0からです。
以降のSQLでは、日本語の設定を tokenizer_ext.tokenizer_japanese とスキーマ名付きで書きます。拡張機能を tokenizer_ext スキーマに入れたので、設定もこのスキーマの中に作られているためです。
前回と同じ文で比べる
前回の記事で1語になってしまった文を、3つの設定で分割して並べます。
SELECT 'english' AS config,
to_tsvector('english', 'LakebaseはDatabricksが提供するマネージドPostgresです') AS tsv
UNION ALL
SELECT 'simple',
to_tsvector('simple', 'LakebaseはDatabricksが提供するマネージドPostgresです')
UNION ALL
SELECT 'tokenizer_japanese',
to_tsvector('tokenizer_ext.tokenizer_japanese', 'LakebaseはDatabricksが提供するマネージドPostgresです');
| config | tsv |
|---|---|
| english | 'lakebaseはdatabricksが提供するマネージドpostgresです':1 |
| simple | 'lakebaseはdatabricksが提供するマネージドpostgresです':1 |
| tokenizer_japanese | 'databrick':2 'lakebas':1 'postgr':5 'マネージド':4 '提供':3 |
english と simple は前回と同じく、文全体が1語です。tokenizer_japanese では5つの語に分かれました。
Postgres で検索してマッチするかも確かめます。
SELECT 'english' AS config,
to_tsvector('english', 'LakebaseはDatabricksが提供するマネージドPostgresです')
@@ plainto_tsquery('english', 'Postgres') AS matched
UNION ALL
SELECT 'tokenizer_japanese',
to_tsvector('tokenizer_ext.tokenizer_japanese', 'LakebaseはDatabricksが提供するマネージドPostgresです')
@@ plainto_tsquery('tokenizer_ext.tokenizer_japanese', 'Postgres');
| config | matched |
|---|---|
| english | false |
| tokenizer_japanese | true |
前回マッチしなかった Postgres が、tokenizer_japanese ではマッチしました。
分割結果の読み方
tsvector の中身を見ると、いくつか気になる点があります。
- 「は」「が」「です」のような助詞・助動詞は落ちています。ドキュメントによると、
tokenizer_japaneseは助詞、助動詞、接続詞などを取り除きます - 英単語の語尾が削られています(
lakebas、databrick、postgr)。組み込みの設定では英語のステミングが有効になっているためです。ステミングがかかるのは英字だけで構成された語に限られ、日本語の語には影響しません - 検索語にも同じ処理がかかるので、
Postgresで検索すればpostgr同士で一致します。tsvectorを直接見たときに戸惑わないよう、この点だけ覚えておくと良さそうです
BM25で日本語の文書を検索する
次に、実際に文書を入れてBM25で検索します。
テーブルとインデックスを作る
検証用のスキーマ ja_fts_demo を作り、tokenizer_japanese で tsvector を作る生成列を持つテーブルを用意します。
DROP SCHEMA IF EXISTS ja_fts_demo CASCADE;
CREATE SCHEMA ja_fts_demo;
CREATE TABLE ja_fts_demo.ja_docs (
id BIGSERIAL PRIMARY KEY,
body TEXT NOT NULL,
-- 本文を入れると、日本語の設定で分割した tsvector が自動で入る
search_vector TSVECTOR GENERATED ALWAYS AS (
to_tsvector('tokenizer_ext.tokenizer_japanese', body)
) STORED
);
文書を6件入れます。
INSERT INTO ja_fts_demo.ja_docs (body) VALUES
('LakebaseはDatabricksが提供するマネージドPostgresです'),
('ベクトル検索では意味の近い文書を見つけます'),
('キーワード検索はBM25で文書をランク付けします'),
('検索した結果をエージェントに渡しました'),
('Unity Catalogでテーブルの権限を管理します'),
('関西国際空港へのアクセスは電車が便利です');
BM25インデックスは、データを入れたあとに作ります。BM25のスコア計算に使う統計は、インデックスを作るときに計算されるためです。
CREATE INDEX ja_docs_bm25 ON ja_fts_demo.ja_docs USING lakebase_bm25 (search_vector);
文書がどう分割されたか
検索の前に、各文書の分割結果を見ておきます。
SELECT id, body, search_vector
FROM ja_fts_demo.ja_docs
ORDER BY id;
| id | body | search_vector |
|---|---|---|
| 1 | LakebaseはDatabricksが提供するマネージドPostgresです | 'databrick':2 'lakebas':1 'postgr':5 'マネージド':4 '提供':3 |
| 2 | ベクトル検索では意味の近い文書を見つけます | 'ベクトル':1 '意味':3 '文書':5 '検索':2 '見つける':6 '近い':4 |
| 3 | キーワード検索はBM25で文書をランク付けします | '25':4 'bm':3 'キーワード':1 'ランク':6 '付け':7 '文書':5 '検索':2 |
| 4 | 検索した結果をエージェントに渡しました | 'エージェント':3 '検索':1 '渡す':4 '結果':2 |
| 5 | Unity Catalogでテーブルの権限を管理します | 'catalog':2 'uniti':1 'テーブル':3 '権限':4 '管理':5 |
| 6 | 関西国際空港へのアクセスは電車が便利です | 'アクセス':4 '便利':6 '国際':2 '空港':3 '関西':1 '電車':5 |
活用している語は基本形に戻っています。「見つけます」は 見つける、「渡しました」は 渡す です。「検索した」は 検索 だけが残りました。
BM25 が bm と 25 に分かれているのは、PostgreSQL標準のパーサーが英字と数字の境目で区切るためです。日本語対応とは別の、もともとの挙動です。
日本語のクエリで検索する
BM25のスコアは、<@> 演算子と to_bm25query 関数で計算します。クエリ側も同じ tokenizer_japanese で分割します。
SELECT id, body,
search_vector <@> to_bm25query(
-- クエリも文書と同じ設定で分割する
to_tsvector('tokenizer_ext.tokenizer_japanese', '検索'),
-- インデックス名はスキーマ名付きで渡す
'ja_fts_demo.ja_docs_bm25'
) AS score
FROM ja_fts_demo.ja_docs
ORDER BY score
LIMIT 3;
スコアは負の値で、小さいほど関連が高い順です。to_bm25query の2つ目の引数にはインデックス名を文字列で渡します。前回の記事でつまずいた点ですが、public 以外のスキーマにインデックスを作った場合は、スキーマ名を付けないと見つかりません。
4つのクエリで検索した結果です。マッチしなかった行(スコア0)は省いています。
| クエリ | ヒットした文書(スコア順) |
|---|---|
Postgres |
id=1 (-1.5999) |
検索 |
id=4 (-0.7802)、id=2 (-0.6683)、id=3 (-0.6236) |
文書のランク付け |
id=3 (-3.6979)、id=2 (-0.9927) |
空港 |
id=6 (-1.4852) |
検索 では、「検索」を含む3件がヒットしました。どの文書にも「検索」は1回ずつ出てきますが、いちばん短い文書4が1位です。BM25は、同じ語が同じ回数出てくる場合、短い文書ほどスコアを高くするためです。
文書のランク付け は、クエリが「文書」「ランク」「付け」の3語に分かれます。3語すべてを含む文書3が大きく差をつけて1位で、「文書」だけを含む文書2が2位でした。単語に分かれたことで、語の重なりの多さがスコアに反映されています。
空港 では、「関西国際空港」を含む文書6がヒットしました。複合語の一部だけで検索してもヒットするのは、組み込みの tokenizer_japanese が複合語を細かく分ける設定になっているためです。次の節で、この設定を変えてみます。
複合語の扱いを選ぶ
tokenizer_morph には、複合語の扱いを決める Mode オプションがあります。日本語で選べるのは次の2つです。
-
discard: 複合語を構成要素に分けて出力する。組み込みのtokenizer_japaneseはこちら -
none: 辞書にある長い複合語をそのまま1語として出力する
none の設定は組み込みにはないので、tokenizer_morph テンプレートから辞書と設定を作ります。
-- none モードの辞書
CREATE TEXT SEARCH DICTIONARY ja_fts_demo.ja_none_dict (
TEMPLATE = tokenizer_ext.tokenizer_morph,
Language = 'japanese',
Mode = 'none',
-- 組み込みの設定と同じく、英字だけの語には英語のステミングをかける
Stemmer = 'english'
);
-- simple をコピーして、トークンの種類ごとに上の辞書を割り当てる
CREATE TEXT SEARCH CONFIGURATION ja_fts_demo.ja_none_cfg (COPY = pg_catalog.simple);
ALTER TEXT SEARCH CONFIGURATION ja_fts_demo.ja_none_cfg
ALTER MAPPING FOR asciiword, word, numword, hword_asciipart, hword_part, hword_numpart
WITH ja_fts_demo.ja_none_dict;
同じ文を2つの設定で分割します。
SELECT 'discard' AS mode,
to_tsvector('tokenizer_ext.tokenizer_japanese', '関西国際空港へのアクセスは電車が便利です') AS tsv
UNION ALL
SELECT 'none',
to_tsvector('ja_fts_demo.ja_none_cfg', '関西国際空港へのアクセスは電車が便利です');
| mode | tsv |
|---|---|
| discard | 'アクセス':4 '便利':6 '国際':2 '空港':3 '関西':1 '電車':5 |
| none | 'アクセス':2 '便利':4 '関西国際空港':1 '電車':3 |
none では「関西国際空港」が1語のまま残りました。
検索語ごとに、マッチするかを比べます。
SELECT q AS query,
to_tsvector('tokenizer_ext.tokenizer_japanese', '関西国際空港へのアクセスは電車が便利です')
@@ plainto_tsquery('tokenizer_ext.tokenizer_japanese', q) AS discard_match,
to_tsvector('ja_fts_demo.ja_none_cfg', '関西国際空港へのアクセスは電車が便利です')
@@ plainto_tsquery('ja_fts_demo.ja_none_cfg', q) AS none_match
FROM unnest(ARRAY['空港', '関西国際空港', '国際']) AS q;
| query | discard_match | none_match |
|---|---|---|
| 空港 | true | false |
| 関西国際空港 | true | true |
| 国際 | true | false |
discard は、複合語の一部である 空港 や 国際 でもマッチします。関西国際空港 で検索した場合は、クエリ側も「関西」「国際」「空港」に分かれ、文書がその3語をすべて含むのでマッチします。
none は、関西国際空港 という複合語の全体で検索したときだけマッチします。
語の一部でも拾いたいなら discard、固有名詞のような複合語を1つの単位として扱いたいなら none、という選び方になります。どちらにしても、文書とクエリで同じ設定を使う必要があります。
まとめ
Lakebase Searchの日本語対応を試してみて分かったことをまとめます。
lakebase_tokenizer0.2.0以降の組み込み設定tokenizer_japaneseをto_tsvectorに渡すだけで、日本語の文が単語に分かれる。前回の記事で必要だった、UC側での分かち書きの前処理はいらない- 前回1語になっていた文では、
Postgresでのマッチがfalseからtrueに変わった - 助詞・助動詞は取り除かれ、活用している語は基本形に戻る
- 組み込みの設定は英字の語に英語のステミングをかけるので、
tsvectorにはpostgrのような語尾の削られた形で入る。検索語にも同じ処理がかかるので、検索には影響しない - 生成列とBM25インデックスに組み込むと、日本語のクエリでBM25のスコア順に検索できる。インデックス名はスキーマ名付きで
to_bm25queryに渡す - 複合語の扱いは
Modeで選べる。組み込みのdiscardは語の一部でもマッチし、noneは複合語の全体でだけマッチする - Lakebase Searchを有効にしていないプロジェクトでは、
lakebase_textのインストールでshared_preload_librariesのエラーになる
一番の収穫は、前回「続編で扱います」と書いた日本語のキーワード検索が、Postgresの中のSQLだけで完結するようになったことでした。分かち書きのパイプラインを別に持たなくてよいので、アプリが直接INSERTするデータにもそのまま日本語の検索が効きます。日本語でハイブリッド検索を組むときは、まず tokenizer_japanese で分割結果を眺めて、複合語の扱いが検索の意図に合うかを確かめるところから始めるのが良さそうです。
参考リンク
- Lakebase リリースノート
- lakebase_tokenizer
- lakebase_text
- Lakebase Search
- Lakebase Search (ベータ) を試してみた: Databricks AI Searchとどう使い分けるのか
