Fess 15.7のStaticテーマでセマンティック検索を分かりやすく可視化する

Fessでセマンティック(ベクトル)検索を手軽に試せるように、docker-semanticsearchを大きく刷新しました。Fess 15.7とOpenSearch 3.7を組み合わせ、docker compose upだけで、BM25(キーワード検索)とベクトル検索を組み合わせたハイブリッド検索が動く環境をそのまま立ち上げられるようにしています。

今回のポイントは、Fess 15.7で追加された静的テーマ(Static Theme)の仕組みを活用し、検索結果の見た目を分かりやすくしたことです。デフォルトで採用している「SemanticLens」テーマでは、各検索結果がキーワード検索・セマンティック検索・ハイブリッドのどれによってヒットしたのかをバッジで表示します。これにより、セマンティック検索が実際にどう効いているのかが一目で分かるようになりました。

セマンティック検索とハイブリッド検索

Fessのセマンティック検索は、埋め込みモデルで文章をベクトル化し、意味の近さで検索するベクトル検索です。従来のBM25によるキーワード検索と組み合わせ、RRF(Reciprocal Rank Fusion)で両者のランキングを統合することで、表記ゆれや言い換えに強いハイブリッド検索を実現します。docker-semanticsearchでは、多言語対応の埋め込みモデルparaphrase-multilingual-MiniLM-L12-v2(384次元)をデフォルトで使用します。

必要な環境

  • Docker および Git
  • Linuxの場合はOpenSearch向けにvm.max_map_countを262144以上に設定(Docker Desktopでは自動設定)

利用する主なコンポーネントは以下のとおりです。

  • Fess 15.7.0
  • OpenSearch 3.7.0(fess-opensearch)
  • fess-webapp-semantic-search 15.7.0

セットアップと起動

リポジトリをクローンし、セットアップスクリプトを実行してから起動します。

git clone https://github.com/codelibs/docker-semanticsearch.git
cd docker-semanticsearch
bash ./bin/setup.sh
docker compose up -d

bin/setup.shは、データディレクトリの作成、system.propertiesの初期生成、そして静的テーマの同期を行います。初回起動時はOpenSearchが埋め込みモデルをダウンロードするため、準備が整うまで数分かかります。進捗は次のコマンドで確認できます。

docker compose logs -f init-semantic init-fess-index
docker compose ps -a

fess01がhealthyになり、セットアップ用のコンテナが正常終了すれば準備完了です。検索画面はhttp://localhost:8080/、管理画面はhttp://localhost:8080/admin(初期アカウントはadmin / admin)でアクセスできます。あとは管理画面のクローラー設定でクロール対象を登録してクロールを実行すれば、取り込んだドキュメントが自動的にベクトル化され、検索できるようになります。

Staticテーマで検索結果を可視化する

今回の刷新で一番大きいのが、検索画面の作り方を変えたことです。以前は独自のJSPテーマで検索画面を組んでいましたが、Fess 15.7で追加された静的テーマ(HTML/CSS/JavaScriptだけで検索画面を構成できる仕組み)に切り替えました。テーマはfess-themesリポジトリからbin/setup.shが同期します。

利用するテーマは.envTHEME_NAMEで指定でき、デフォルトはsemanticlens(SemanticLens)です。SemanticLensは、各検索結果に「その結果がどの検索器(keyword / semantic / hybrid)によって生成されたか」を示すバッジと凡例を表示します。ハイブリッド検索では複数の検索器のランキングが統合されるため、通常のテーマでは「なぜこの結果が出てきたのか」までは分かりません。SemanticLensを使うと、セマンティック検索で拾えた結果とキーワード検索でヒットした結果を見分けられるので、動作の理解やチューニングに役立ちます。

SemanticLens以外にも、以下の静的テーマが選べます。

  • semanticlens(デフォルト。検索器バッジを表示)
  • nomadkit
  • docsearch
  • docuforge
  • rawblock
  • voicebox

テーマを切り替えるときは.envTHEME_NAMEを変更してからbin/setup.shを実行します。なお、bin/setup.shは既存のsystem.propertiesを上書きしないため、すでに起動済みの環境でテーマを変える場合は、data/fess/opt/fess/system.propertiestheme.defaultを直接書き換える(または管理画面のAdmin > Generalで変更する)必要があります。

セマンティック検索は自動でセットアップされる

以前のdocker-semanticsearchは、モデルの登録や設定値の手貼り、再インデックスといった手順をすべて手動で行う必要がありました。今回のバージョンでは、これらをコンテナ起動時に自動化しています。

  • セマンティック検索プラグインは、FessのFESS_PLUGINS環境変数で自動的にインストールされます。
  • init-semanticコンテナが、OpenSearchに埋め込みモデルを登録・デプロイし、チャンク分割と埋め込みを行うニューラル取り込みパイプライン(neural_pipeline)を作成します。
  • 生成されたモデルIDは、Fess起動時にJVMのシステムプロパティとして自動的に注入されます。
  • init-fess-indexコンテナが、ドキュメントインデックスにベクトル用のマッピングを付与するための再インデックスを一度だけ実行します。

これらはいずれも冪等に作られているので、docker compose upを実行するだけで、管理画面での手作業なしにハイブリッド検索が使える状態になります。

まとめ

docker-semanticsearchをFess 15.7 + OpenSearch 3.7に対応させ、docker compose upだけでセマンティック検索(ハイブリッド検索)を試せるようにしました。さらに、Fess 15.7のStaticテーマ「SemanticLens」を採用することで、どの検索器で結果がヒットしたのかがバッジで分かるようになり、セマンティック検索の挙動を見た目でつかめるようになっています。

詳細やソースコードはdocker-semanticsearchを参照してください。

Recotem 2.0.0のリリース

レコメンドシステム構築ツール Recotem の 2.0.0 をリリースしました。

2.0 は 1.x からの完全な作り直しです。これまでの Django / Vue / Celery とデータベース・メッセージブローカーで構成された Web アプリケーションを廃止し、pip でインストールできる単一の Python パッケージと 1 つの Docker イメージにまとめました。レシピファイル(YAML)を 1 つ書き、コマンドを実行するだけでレコメンドモデルを作成でき、作成したモデルは Recotem が提供する Docker でそのままレコメンド API として立ち上げられます。

Recotem 2.0 で変わったこと

  • レシピ駆動のワークフロー: 1 つの YAML レシピがそのまま 1 モデル・1 エンドポイントに対応します(1 recipe = 1 model = 1 endpoint)。
  • 2 つの CLI コマンド: recotem train <recipe.yaml> で学習し、recotem serve --recipes <dir> で配信します。
  • FastAPI による配信: /v1/recipes/{name}:recommend などのエンドポイントでレコメンドを返します。アーティファクトの更新を検知してホットスワップします。
  • 署名付きアーティファクト: 学習と配信は HMAC 署名付きのアーティファクトファイルのみでやり取りするため、別々のマシンで動かせます。共有データベースもメッセージブローカーも不要です。
  • 多様なデータソース: csv / parquet / bigquery / sql(PostgreSQL / MySQL / SQLite)に対応しています。
  • Optuna によるハイパーパラメータ探索: irspack のアルゴリズムに対して自動でチューニングします。

なお Python 3.12 以上が必要です。1.x との互換性はなく、モデルは再学習が必要です。

インストール

pip install recotem

または Docker イメージを利用します。

docker pull ghcr.io/codelibs/recotem:2.0.0

レシピファイルでモデルを定義する

モデルは 1 つの YAML レシピで定義します。データソース、カラムの対応、前処理、学習アルゴリズム、出力先をまとめて記述します。以下はチュートリアル用のレシピ例です。

name: purchase_log

source:
  # csv, parquet, bigquery(またはプラグイン名)を選択
  type: csv
  path: https://raw.githubusercontent.com/codelibs/recotem/refs/tags/v1.0.0/frontend/e2e/test_data/purchase_log.csv
  # ネットワーク経由のパスでは sha256 が必須
  sha256: 945fc769205a5976d38c5783500ae473afbb04608043b703951a699993c8f8be
  dtype:
    user_id: str
    item_id: str

schema:
  user_column: user_id
  item_column: item_id

cleansing:
  drop_null_ids: true
  dedup: keep_last
  min_rows: 100
  min_users: 10
  min_items: 10

training:
  # IALS, CosineKNN, TopPop, RP3beta, DenseSLIM, TruncatedSVD, BPRFM から選択し Optuna が最適化
  algorithms: [IALS, TopPop]
  # ndcg, map, recall, hit から選択
  metric: ndcg
  cutoff: 10
  n_trials: 10
  parallelism: 1
  split:
    scheme: random
    heldout_ratio: 0.2
    seed: 42

output:
  path: ./artifacts/purchase_log.recotem
  versioning: append_sha

training.algorithms に複数のアルゴリズムを指定すると、Optuna が n_trials の範囲で組み合わせを探索し、metric で最良のモデルを選びます。

コマンドでモデルを作成する

まず署名用と API 用のキーを生成します。

recotem keygen --type signing --kid dev
recotem keygen --type api --kid dev

生成したキーを環境変数に設定します。

export RECOTEM_SIGNING_KEYS="dev:<signing から得た平文>"
export RECOTEM_API_KEYS="dev:sha256:<api から得たハッシュ>"
export RECOTEM_API_PLAINTEXT="<api から得た平文>"

レシピの内容を検証してから学習を実行します。

recotem validate examples/tutorial-purchase-log/recipe.yaml
recotem train examples/tutorial-purchase-log/recipe.yaml

学習が完了すると、output.path に署名付きのアーティファクトファイルが出力されます。これがそのままレコメンドモデルの実体になります。

Docker でレコメンドAPIを立ち上げる

作成したモデルは、Recotem が提供する Docker でレコメンド API として配信できます。レシピを置いたディレクトリを指定して serve するだけです。

recotem serve --recipes examples/tutorial-purchase-log/

付属の compose.yaml を使えば、学習と配信を Docker だけで完結できます。

docker compose run --rm train
docker compose up -d serve

立ち上がった API には HTTP でリクエストします。ユーザーへのレコメンドは次のように取得します。

curl -sX POST http://localhost:8080/v1/recipes/purchase_log:recommend \
  -H "X-API-Key: $RECOTEM_API_PLAINTEXT" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "1", "limit": 5}' | jq .

アイテムに関連するアイテムのレコメンドも取得できます。

curl -sX POST http://localhost:8080/v1/recipes/purchase_log:recommend-related \
  -H "X-API-Key: $RECOTEM_API_PLAINTEXT" \
  -H "Content-Type: application/json" \
  -d '{"seed_items": ["<item_id>"], "limit": 5}' | jq .

このほか :recommend-batch によるバッチ推論や、Prometheus 向けの /metrics エンドポイントにも対応しています。Helm チャートや Kubernetes 用のマニフェストも同梱しているため、コンテナ環境へそのままデプロイできます。

Claude Code でレコメンドモデルを作成する

2.0 ではレシピが単なる YAML ファイルになり、学習も配信もコマンド 1 つで実行できるようになりました。この設計により、Claude Code のような AI コーディングエージェントを使ったモデル作成も可能になります。

例えば、手元のデータのカラム構成を Claude Code に伝えれば、適切な sourceschema を持つレシピファイルを生成してもらえます。そのまま recotem validaterecotem train を実行してモデルを作成し、評価結果を見ながらアルゴリズムや n_trials を調整する、といった一連の流れをエージェントに任せられます。レコメンドモデルの構築に必要な作業が「ファイルを書く」「コマンドを実行する」に集約されたことで、AI エージェントとの相性が大きく向上しました。

1.x からの移行

2.0 へは自動移行のパスがありません。次の点に注意してください。

  1. モデルは再学習する: 1.x のモデルは 2.0 の署名付きアーティファクト形式と互換性がありません。
  2. データベース・メッセージブローカーは不要: 2.0 はステートレスで、永続化されるのは署名付きアーティファクトファイルだけです。
  3. API クライアントを更新する: /predict/{name} は廃止され、POST /v1/recipes/{name}:recommend に変わりました。
  4. キーを生成する: recotem keygen で署名用・API 用のキーを生成し、RECOTEM_SIGNING_KEYS / RECOTEM_API_KEYS に設定します。

まとめ

Recotem 2.0.0 は、レシピファイルとコマンドでレコメンドモデルを手軽に作成し、Docker でそのまま API として配信できるツールに生まれ変わりました。重厚な Web アプリケーションから、軽量で AI エージェントとも組み合わせやすいレコメンド基盤へと進化しています。ぜひお試しください。

関連リンク

Fess 15.7.0のリリース

Fess 15.7.0をリリースしました。今回は、ヘッドレス用途やSPA(シングルページアプリケーション)フロントエンドを意識した新しい/api/v2 REST APIと、HTML/CSS/JavaScriptだけで検索画面を作れる静的テーマの仕組みが目玉になっています。マイルストーンを見てもらうと分かりますが、フロントエンドとAPIまわりを大きく作り直した、ボリュームのあるリリースになりました。

まず一番大きいのが、/api/v2に統一したREST APIです。これまで機能ごとにバラバラだったREST managerを、一貫したJSONエンベロープ(statusschemadata / error)にまとめました。検索やスクロール(NDJSON)、関連クエリ・関連コンテンツ、ドキュメントキャッシュ、お気に入り、クリックログ、認証(ログイン / ログアウト / me / パスワード変更)、CSRFトークン、ログインのレート制限、UI設定、そしてチャット(POSTおよびSSEストリーミング)まで、ひととおりカバーしています。OpenAPIの定義も同梱していて、入力値の上限チェック、状態を変更するリクエストへのCSRF必須化、IP単位・ユーザー単位のレート制限といったセキュリティ面の強化も合わせて入れています。

このAPI刷新にともなって、従来のv1 JSON検索API・チャットAPIは削除されました。もし既存のv1検索APIに依存している場合は、新しく用意したfess-webapp-v1-apiプラグインをインストールすると、これまでどおりの検索APIを使えるようになります。アップグレード時はこの点に注意してください。

次に、静的テーマ(Static Theme System)の仕組みを追加しました。検索画面を、JSPやActionを経由せずにWeb層から直接配信できる、自己完結したHTML/CSS/JavaScriptのバンドルとして提供できるようになっています。テーマはtheme.ymlのマニフェストでバリデーションされ、事前圧縮したアセットやキャッシュヘッダーにも対応します。ZIPアップロードでインストールでき、パストラバーサルやzip-bomb、サイズ上限といったセキュリティガードもしっかり入れてあります。管理画面には新しく「Admin → Theme」を追加して、アップロード・有効化・デフォルト設定・削除がそこから行えるようにしました。

その静的テーマを実際に動かすリファレンス実装として、Bootstrap 5ベースのテーマも同梱しています。素のES2022モジュールで作られていて、ホーム、ファセット付きの検索、詳細検索、キャッシュ表示、AIチャット、プロフィール、ヘルプといった既存の検索UIを、16言語で再現しています。そのまま使ってもいいですし、独自の検索フロントエンドを作るときの出発点としても使えるようにしています。

検索エンジン側では、OpenSearch 3.7に対応しました。バンドルしているkopfプラグインも15.7.0に合わせて更新しています。

セキュリティ周りもまとめて強化しています。状態を変更するv2リクエストにはCSRFを無条件で必須化し、ログインのスロットリングは(クライアントIP、ユーザー名)の単位に、匿名チャットのスロットリングはクライアントIP単位にスコープを切るようにしました。認証情報付きのCORSは許可リストに載せたオリジンだけに制限し、関連コンテンツのプレースホルダーでは検索クエリをエンコードして反射型XSSを防いでいます。セッションCookieにSecure属性を付けるためのsession.cookie.secureオプションも追加しました。このほか、匿名のヘルスレスポンスからcluster_nameを出さないようにしたり、v2レスポンスをデフォルトでCache-Control: no-storeにしたり、クリックのquery_idの妥当性を検証してランクの値を範囲内に収めたりと、細かいところを地道に固めています。

AI Search Mode(RAGチャット)も改善しました。大きなドキュメントから回答を生成するときに、ハイライトされたパッセージを使うようにしています。スマートサマリーは「Minimal Trail」として作り直し、フェーズごとに履歴を持つ形に見直しました。LLMのリトライ・待機・フォールバック・警告といった状態や、検索ヒット件数をブラウザに通知できるようにし、RAGのインテント処理では途中で切れたLLMレスポンスをフォールバックとして扱うようにしています。

クロール・インデックス周りでは、失敗URLとして扱うステータスコードのデフォルトにHTTP 403と410を追加しました。あわせてFessCrawlerThreadのホットパスを引き締め、API・クローラー・LLMのコード全体で正規表現パターンを事前コンパイルするようにして、処理を軽くしています。

そのほか、不具合修正として、デフォルトのGoogle Cloud Storageエンドポイントを使ったときにストレージ機能が正しく有効化されるよう直しています。

今回もコード修正からリリースノート作成、多言語ドキュメントの更新まで、Claude Codeにかなり助けてもらいました。/api/v2への統一と静的テーマの仕組みで、フロントエンドの自由度がぐっと上がっているので、ぜひ新しいFessを使ってみてください。