fess-llm-*プラグイン

全文検索サーバーFessのAIモード(RAGチャット機能)について、大規模なリファクタリングを行いました。LLMプロバイダーの実装をコアから切り出してfess-llm-*プラグインとして独立させたほか、セキュリティ強化、設定の柔軟性向上、信頼性改善など多くの改善を加えています。

LLMクライアントのプラグイン化

これまでGemini、Ollama、OpenAIの各LLMクライアントはFessコアに組み込まれていましたが、プラグインアーキテクチャに切り出しました。

変更内容

  • PluginHelperLLM("fess-llm")プラグインタイプを追加
  • AbstractLlmClientregister()メソッドを追加し、LlmClientManagerへの自動登録を実現
  • LlmClientManagerclientListCopyOnWriteArrayListに変更し、スレッドセーフに
  • コアからGeminiLlmClientOllamaLlmClientOpenAiLlmClientとそのテストクラスを削除

これにより、コアから約6,600行が削減され、LLMプロバイダーの追加・更新がFess本体のリリースに依存しなくなりました。新しいLLMプロバイダーを追加する場合は、fess-llm-*プラグインとして実装するだけで利用できます。

パラメータ設定の統一

各LLMプロバイダーで散在していたパラメータ設定を、統一的な設定方式に変更しました。

  • getTemperature()getMaxTokens()等の個別メソッドを廃止し、getConfigPrefix()で設定プレフィックスを返す方式に
  • {configPrefix}.{promptType}.{paramName}のパターンでプロンプトタイプごとにtemperature、max.tokens、thinking.budgetを設定可能に
  • LlmChatRequestextraParamsフィールドを追加し、プロバイダー固有のパラメータ(OpenAIのreasoning_effort等)にも対応

たとえば、意図検出には低いtemperatureを、回答生成には高いトークン上限を設定するといった細かいチューニングが、コード変更なしで可能になりました。

セキュリティ強化

RAGチャットパイプラインに対して、複数のセキュリティ対策を追加しました。

  • メッセージ長制限: rag.chat.message.max.length(デフォルト4000文字)で入力長を制限
  • プロンプトインジェクション対策: ユーザー入力を<user_input>デリミタで囲み、タグエスケープを適用
  • クエリバリデーション: LLMが生成したクエリに対して、1000文字超や危険なLuceneパターン(*:*)を拒否
  • セッションオーナーシップ検証: セッションクリア時にuserId所有権を検証し、他ユーザーのセッション操作を防止

エラーハンドリングの改善

LlmExceptionに構造化されたエラーコードを導入しました。

  • rate_limitauth_errorservice_unavailableunknownmodel_not_foundtimeoutの6種類
  • HTTPステータスコードからエラーコードへのマッピング(resolveErrorCode()
  • チャットUIでエラーコードに応じたローカライズ済みメッセージを表示(21言語対応)
  • 推論モデル(o1、o3、DeepSeek R1等)でトークンを使い切った場合のフォールバック処理を追加

並行制御

LLMリクエストにセマフォベースの並行制御を追加しました。

  • rag.llm.{provider}.max.concurrent.requests(デフォルト5)で同時リクエスト数を制限
  • rag.llm.{provider}.concurrency.wait.timeout(デフォルト30秒)でタイムアウトを設定
  • 上限超過時はERROR_RATE_LIMITを返却

コンテキスト管理の改善

LLMに渡すコンテキストの管理を改善しました。

  • 履歴バジェット管理: addHistoryWithBudget()で会話履歴の総文字数をrag.chat.total.context.max.chars以内に制御
  • アシスタントメッセージ設定: rag.chat.history.assistant.contentで履歴中のアシスタントメッセージの形式を選択可能(fullsource_titlessource_titles_and_urlstruncatednoneの5モード)
  • ハイライト設定: RAGチャット用のハイライトでHTMLタグを除去し、フラグメントサイズと数を設定可能に
  • プロンプトタイプ別コンテキストサイズ: プロンプトの種類ごとにデフォルトのコンテキストサイズを設定可能に

管理画面の改善

管理画面の一般設定にRAG LLMプロバイダーの選択UIを追加しました。

  • 登録済みのLLMクライアントからプロバイダーを選択可能
  • rag.llm.name設定値の検証
  • RAG機能が有効かつLLMクライアントが登録されている場合のみ表示

その他の改善

  • 国際化: チャットのウェルカムタイトルを全16言語で簡潔なタグラインに更新(例: JA「聞けば、見つかる。」、EN「Ask and Discover.」)
  • レート制限設定の削除: チャットのレート制限設定(rag.chat.rate.limit.*)をFessConfigから削除(並行制御に移行)
  • 履歴付き意図検出: 直近の会話履歴を考慮した意図検出で、コンテキストに基づいた検索クエリを生成
  • ドキュメント順序の保持: 検索スコア順でドキュメントの順序を維持

まとめ

今回のリファクタリングにより、FessのAIモードはプラグインベースの拡張可能なアーキテクチャになりました。LLMプロバイダーの追加・更新がFess本体から独立し、セキュリティや信頼性も大幅に向上しています。新しいLLMプロバイダーに対応する場合は、fess-llm-*プラグインを作成してインストールするだけで利用可能です。

関連リンク

FessクローラーのURLからURIへの置き換えで対応できないパターンへのフォールバック対応

Fessのクローラーでは、クロール対象のHTMLページからリンクURLを抽出する際に、java.net.URIを使って相対URLを絶対URLに解決しています。しかし、URI.resolve()はRFC準拠の厳密なパースを行うため、スペースなどの特殊文字を含む相対パスを正しく処理できないケースがありました。今回、この問題に対するフォールバック処理をPR #3056で追加しました。

問題

FessXpathTransformerでHTMLのアンカータグなどからURLを抽出する際、URI.resolve()で相対URLを絶対URLに変換しています。通常の相対パスであれば問題なく動作しますが、以下のようなケースではURIがスキームを検出できず、URLの解決に失敗していました。

  • スペースを含む相対パス(例: page 2.html
  • スペースを含む絶対パス(例: /path with space/page.html
  • スペースを含むプロトコル相対URL(例: //cdn.example.com/a b.js
  • スペースを含む親ディレクトリ参照(例: ../page 2.html

実際のWebサイトでは、こうしたURLは珍しくないため、クロール時にリンクが取りこぼされる原因になっていました。

対応内容

FessXpathTransformeraddChildUrlFromTagAttributeメソッドに、既存の絶対URL判定(スキーム検出)で処理できなかった場合のフォールバック処理を追加しました。ベースURIのスキームとオーソリティを利用して、以下の4パターンの相対URLを手動で絶対URLに変換します。

プロトコル相対URL(//で始まる場合)

// 例: //cdn.example.com/a b.js → http://cdn.example.com/a%20b.js
fallbackUrl = scheme + ":" + urlValue;

クエリ・フラグメントのみ(?または#で始まる場合)

// 例: ?q=test → http://example.com/page.html?q=test
fallbackUrl = uri.toString() + urlValue;

ルート絶対パス(/で始まる場合)

// 例: /path with space/page.html → http://example.com/path%20with%20space/page.html
// /../の正規化も行う

相対パス(上記以外)

// 例: ../page 2.html → http://example.com/dir/page%202.html
// ベースURIのパスから親ディレクトリを算出して結合

いずれのパターンでも、/../がルートを超える場合のパス正規化処理を行い、最終的にencodeUrlでパーセントエンコーディングを適用します。

テスト

6つのテストケースを追加して、各パターンの動作を検証しています。

  • page 2.htmlhttp://example.com/dir/page%202.html
  • /path with space/page.htmlhttp://example.com/path%20with%20space/page.html
  • //cdn.example.com/a b.jshttp://cdn.example.com/a%20b.js
  • ../page 2.html(深い階層から) → http://example.com/dir/page%202.html
  • /../page 2.htmlhttp://example.com/page%202.html
  • ../page 2.html(ルートから) → http://example.com/page%202.html

まとめ

今回の修正は、既存のURI.resolve()で処理できるURLには一切影響を与えず、これまで取りこぼしていたスペース等の特殊文字を含む相対URLを正しく解決できるようになりました。実際のWebサイトでは、CMSが生成するURLにスペースが含まれることもあるため、クロールの網羅性向上に寄与する修正です。

fess-crawlerに一太郎ドキュメントのMIMEタイプ検出を追加

fess-crawlerに、ジャストシステムの一太郎ドキュメントのMIMEタイプ検出機能を追加しました。これにより、Fessのクローラーが一太郎ファイルを正しく識別し、コンテンツを抽出できるようになります。

一太郎のファイル形式

一太郎はジャストシステムが開発した日本語ワープロソフトで、日本の官公庁や企業で広く利用されています。一太郎のファイルはOLE2複合ドキュメント形式を使用しており、バージョンによって複数の拡張子が存在します。

対応する拡張子

今回のMIMEタイプ application/x-js-taro に対応する拡張子は以下の通りです。

拡張子説明
.jtd一太郎 基本文書
.jtt一太郎 テンプレート
.jtdc一太郎 基本文書(ZIP圧縮)
.jttc一太郎 テンプレート(ZIP圧縮)
.jfw一太郎7形式 基本文書
.jvw一太郎7形式 テンプレート
.jsw一太郎ver.4形式 基本文書
.jaw一太郎ver.5形式 基本文書
.jtw一太郎ver.5形式 テンプレート
.jbw一太郎ver.6形式 基本文書
.juw一太郎ver.6形式 テンプレート

MIMEタイプの登録

tika-mimetypes.xmlにMIMEタイプ定義を追加しています。マジックバイト(DOC\x00)とglobパターンの両方で一太郎ファイルを識別できるようにしました。一太郎はOLE2複合ドキュメント形式を使用しているため、application/x-tika-msofficeのサブクラスとして登録しています。

<mime-type type="application/x-js-taro">
  <_comment>JustSystems Ichitaro Document</_comment>
  <magic priority="50">
    <match value="DOC\x00" type="string" offset="0"/>
  </magic>
  <glob pattern="*.jtd"/>
  <glob pattern="*.jtt"/>
  <!-- 他の拡張子も同様に登録 -->
  <sub-class-of type="application/x-tika-msoffice"/>
</mime-type>

Extractorの設定

extractor.xmlapplication/x-js-taroTikaExtractorにマッピングし、一太郎ファイルからのコンテンツ抽出を有効にしています。HWP(Hangul Word Processor)など他のOLE2ベースのフォーマットと同じパターンに従った設定です。

テスト

一太郎ver.4、ver.5、ver.6、一太郎7、一太郎2016の各バージョンのテストリソースファイルを用意し、すべての拡張子に対してMIMEタイプが正しく検出されることを確認するテストケースを追加しています。

変更の詳細はPR #141を参照してください。