FessのテストをJUnit 5へ移行

Fessのテストコードを JUnit 4 から JUnit 5(Jupiter)へ移行しました。4000以上のテストケースがあるため、かなりの作業量になりましたが、無事に完了しました。

移行の背景

JUnit 4は長らくJavaのテストフレームワークのデファクトスタンダードでしたが、JUnit 5では以下のような改善が行われています。

  • アノテーションの刷新(@Before@BeforeEach@After@AfterEach など)
  • より柔軟な拡張モデル
  • ネストしたテストのサポート
  • パラメータ化テストの改善

Fessでも最新のテスト環境を維持するため、移行を実施しました。

移行作業の内容

@AfterEachアノテーションの追加

JUnit 5では、テストメソッドの後処理に@AfterEachアノテーションが必要です。従来のtearDown()メソッドに対して、このアノテーションを追加しました。

import org.junit.jupiter.api.AfterEach;

public class SampleTest extends UnitFessTestCase {

    @AfterEach
    public void tearDown() throws Exception {
        // クリーンアップ処理
    }
}

この変更だけで6000行以上の修正が必要でした。テストクラスの数が多いため、すべてのtearDown()メソッドにアノテーションを追加し、importステートメントも更新しました。

  • PR #3012: fix(test): add @AfterEach annotation for JUnit 5 compatibility

テストカバレッジの向上

移行に合わせて、これまでテストが不足していた箇所にも新規テストを追加しました。

追加したテストの内容:

  • Service層: FessAppServiceTestでベースサービス機能をテスト
  • Storage関連: StorageItemTest、StorageTypeTestでストレージクラスをテスト
  • Pagerクラス: 24個の新規Pagerテスト(GroupPager、RolePager、UserPager、SchedulerPager、PathMapPager、KeyMatchPager、ElevateWordPager、FailureUrlPager、JobLogPager、FileConfigPager、WebConfigPager、RelatedContentPager、RelatedQueryPager、RoleTypePager、SearchLogPager、WebAuthPager、FileAuthPager、ReqHeaderPager、SynonymPager、KuromojiPager、StopwordsPager、ProtwordsPager、StemmerOverridePager、LabelTypePager)
  • Jobクラス: AllJobSchedulerTest、ScriptExecutorJobTestでジョブスケジューリングをテスト

これらの追加により、以下のカバレッジギャップを解消しました。

対象移行前カバレッジ移行後
Service層0%テスト追加
Pagerクラス22.6%大幅に向上
Storageクラス0%テスト追加
App Jobクラス0%テスト追加
  • PR #3011: test: add unit tests for improved test coverage

まとめ

4000以上のテストケースを持つFessのJUnit 5移行を完了しました。移行作業は主にアノテーションの置き換えとimport文の更新でしたが、テストクラスの数が多いため手間のかかる作業でした。また、移行を機にテストカバレッジも改善し、これまで手薄だった箇所にテストを追加しました。

FessにAIモード(RAGチャット機能)を追加

全文検索サーバーFessの次期バージョン15.5に、LLMを使ったRAG(Retrieval-Augmented Generation)チャット機能をAIモードとして追加しました。

企業の文書検索において、ChatGPTのような対話形式で回答を得たいというニーズは増えています。一般的なRAGの実装では、ベクトル検索(Embedding検索)を使って関連文書を取得しますが、数百万から数千万件規模のドキュメントをベクトル検索で運用するには、計算リソースやインフラコストの面でハードルが高いのが現状です。Fessでベクトル検索でRAGをしたい場合は、商用サポートに相談みたいな感じかな。

FessのAIモードでは、検索部分は従来どおりの全文検索(OpenSearch)を使い、検索結果をコンテキストとしてLLMに渡すことで、RAGを実現しています。これにより、既存のFess環境にLLMを追加するだけでRAGチャットが使えるようになります。

主な機能

マルチLLMプロバイダー対応

以下のLLMプロバイダーをサポートしています。

  • Ollama: ローカルで動作するLLM。APIキー不要で、プライベート環境での利用に最適
  • OpenAI (GPT-4, GPT-4o等): OpenAI APIを使用
  • Google Gemini: Google AI StudioのAPIを使用

デフォルトではOllamaが設定されており、ローカル環境で手軽に試すことができます。

ストリーミングレスポンス

REST APIでServer-Sent Events(SSE)によるストリーミングに対応しています。LLMからの応答をリアルタイムで表示できるため、ユーザー体験が向上します。

  • 同期API: /api/v1/chat
  • ストリーミングAPI: /api/v1/chat/stream

チャットセッション管理

会話履歴を保持するセッション管理機能を実装しています。

  • セッションタイムアウト: デフォルト30分(設定変更可)
  • レートリミット: デフォルト10リクエスト/分(設定変更可)

チャットUI

Web UIにチャット画面を追加しました。検索結果を基にした対話形式での情報探索が可能です。

アーキテクチャ

AIモードのワークフローは以下のとおりです。

  1. ユーザーからの質問を受け取る
  2. Fessの全文検索で関連文書を検索
  3. 検索結果をコンテキストとしてLLMに渡す
  4. LLMが回答を生成してユーザーに返す

LLMクライアントは抽象化されており、LlmClientインターフェースを実装することで、他のLLMプロバイダーも追加できます。

設定

fess_config.propertiesで各種設定が可能です。

  • LLMプロバイダーの選択
  • APIキー
  • モデル名
  • レートリミット
  • セッションタイムアウト

まとめ

Fess 15.5のAIモードは、全文検索とLLMを組み合わせることで、現実的なコストでRAGを実現する機能です。大規模なベクトルDBを構築せずとも、既存のFess環境を活かしてAIチャット機能を追加できます。

現時点では実験的な機能として提供しますが、フィードバックをいただきながら改善していく予定です。

fess-ds-microsoft365プラグインの権限設定について

FessでMicrosoft 365のコンテンツをクロールするためのプラグイン「fess-ds-microsoft365」を利用する際、Entra IDでどのMicrosoft Graph API権限を設定すればよいかを詳しく解説します。DataStoreごとに必要な権限が異なるため、使用する機能に応じて最小限の権限を設定することをお勧めします。

権限一覧(クイックリファレンス)

まず、各DataStoreで必要な権限を一覧で示します。

DataStore必須権限条件付き権限
OneDriveDataStoreFiles.Read.AllUser.Read.All, Group.Read.All, Sites.Read.All
OneNoteDataStoreNotes.Read.AllUser.Read.All, Group.Read.All, Sites.Read.All
TeamsDataStoreTeam.ReadBasic.All, Group.Read.All, Channel.ReadBasic.All, ChannelMessage.Read.All, ChannelMember.Read.All, User.Read.AllChat.Read.All, Files.Read.All
SharePointDocLibDataStoreFiles.Read.All, Sites.Read.All
SharePointListDataStoreSites.Read.All
SharePointPageDataStoreSites.Read.All

SharePoint系のDataStoreでは、site_idを指定する場合、Sites.Read.Allの代わりにSites.Selectedを使用できます。

OneDriveDataStore

ユーザー、グループ、SharePointサイトのOneDriveファイルをクロールします。

必要な権限

権限必須/条件付き条件
Files.Read.All必須ドライブとファイルへのアクセスに必要
User.Read.All条件付きuser_drive_crawler=trueの場合(デフォルト: true)
Group.Read.All条件付きgroup_drive_crawler=trueの場合(デフォルト: true)
Sites.Read.All条件付きshared_documents_drive_crawler=trueの場合(デフォルト: true)

使用されるGraph APIエンドポイント

エンドポイント用途条件
GET /usersライセンス付きユーザーの取得user_drive_crawler=true
GET /groupsMicrosoft 365グループの取得group_drive_crawler=true
GET /users/{id}/driveユーザーのドライブを取得user_drive_crawler=true
GET /groups/{id}/driveグループのドライブを取得group_drive_crawler=true
GET /sitesすべてのサイトを取得shared_documents_drive_crawler=true
GET /sites/{id}/sites子サイトを取得(再帰的)shared_documents_drive_crawler=true
GET /sites/{id}/drivesサイトのドライブを取得shared_documents_drive_crawler=true
GET /drives/{id}/items/{id}/childrenドライブアイテムの一覧常時
GET /drives/{id}/items/{id}/contentファイルコンテンツの取得常時
GET /drives/{id}/items/{id}/permissionsアイテムの権限を取得常時

最小権限の設定

ユーザーのOneDriveのみをクロールする場合:

Files.Read.All, User.Read.All

グループ共有ドキュメントとSharePoint共有ドキュメントが不要な場合は、パラメータを以下のように設定します:

user_drive_crawler=true
group_drive_crawler=false
shared_documents_drive_crawler=false

OneNoteDataStore

OneNoteのノートブック、セクション、ページをクロールします。

必要な権限

権限必須/条件付き条件
Notes.Read.All必須OneNoteコンテンツへのアクセスに必要
User.Read.All条件付きuser_note_crawler=trueの場合(デフォルト: true)
Group.Read.All条件付きgroup_note_crawler=trueの場合(デフォルト: true)
Sites.Read.All条件付きsite_note_crawler=trueの場合(デフォルト: true)

使用されるGraph APIエンドポイント

エンドポイント用途条件
GET /usersライセンス付きユーザーの取得user_note_crawler=true
GET /groupsMicrosoft 365グループの取得group_note_crawler=true
GET /sites/rootルートサイトの取得site_note_crawler=true
GET /users/{id}/onenote/notebooksユーザーのノートブックを取得user_note_crawler=true
GET /groups/{id}/onenote/notebooksグループのノートブックを取得group_note_crawler=true
GET /sites/{id}/onenote/notebooksサイトのノートブックを取得site_note_crawler=true
GET /onenote/notebooks/{id}/sectionsノートブックのセクションを取得常時
GET /onenote/sections/{id}/pagesセクションのページを取得常時
GET /onenote/pages/{id}/contentページのコンテンツを取得常時

TeamsDataStore

Teamsのチャンネル、メッセージ、チャットをクロールします。Teamsは他のDataStoreと比べて必要な権限が多くなります。

必要な権限

権限必須/条件付き条件
Team.ReadBasic.All必須チーム情報へのアクセスに必要
Group.Read.All必須TeamsはMicrosoft 365グループに基づく
Channel.ReadBasic.All必須チャンネル情報へのアクセスに必要
ChannelMessage.Read.All必須チャンネルメッセージの読み取りに必要
ChannelMember.Read.All必須チャンネルメンバー情報(権限用)に必要
User.Read.All必須ユーザータイプの解決に必要
Chat.Read.All条件付きchat_idを指定している場合
Files.Read.All条件付きappend_attachment=trueの場合

使用されるGraph APIエンドポイント

エンドポイント用途条件
GET /teamsすべてのチームを取得team_idを指定していない場合
GET /groups/{id}特定のチームを取得team_idを指定した場合
GET /teams/{id}/channelsチャンネルを取得channel_idを指定していない場合
GET /teams/{id}/channels/{id}特定のチャンネルを取得channel_idを指定した場合
GET /teams/{id}/channels/{id}/messagesチャンネルメッセージを取得常時
GET /teams/{id}/channels/{id}/messages/{id}/repliesリプライメッセージを取得ignore_replies=falseの場合(デフォルト)
GET /teams/{id}/channels/{id}/membersチャンネルメンバーを取得権限用
GET /chats/{id}/messagesチャットメッセージを取得chat_idを指定した場合
GET /chats/{id}/membersチャットメンバーを取得chat_idを指定した場合
GET /shares/{id}/driveItem/content添付ファイルのコンテンツを取得append_attachment=trueの場合

注意点

Teamsでチャンネルメッセージのみをクロールする場合(チャットは不要、添付ファイルも不要)の最小権限は以下の通りです:

Team.ReadBasic.All, Group.Read.All, Channel.ReadBasic.All, ChannelMessage.Read.All, ChannelMember.Read.All, User.Read.All

SharePointDocLibDataStore

SharePointドキュメントライブラリとそのファイルをクロールします。

必要な権限

権限必須/条件付き条件
Files.Read.All必須ドキュメントライブラリとファイルへのアクセスに必要
Sites.Read.All必須site_idを指定しない場合(すべてのサイトを列挙)
Sites.Selected必須site_idを指定した場合(Sites.Read.Allの代替)

Files.Read.Allに加えて、Sites.Read.AllまたはSites.Selectedのいずれかが必要です。

使用されるGraph APIエンドポイント

エンドポイント用途条件
GET /sitesすべてのサイトを取得site_idを指定していない場合
GET /sites/{id}/sites子サイトを取得(再帰的)site_idを指定していない場合
GET /sites/{id}特定のサイトを取得site_idを指定した場合
GET /sites/{id}/drivesドキュメントライブラリを取得常時
GET /drives/{id}/items/root/childrenドライブアイテムの一覧常時
GET /drives/{id}/items/{id}/contentファイルコンテンツの取得常時
GET /drives/{id}/items/root/permissionsドライブの権限を取得常時

SharePointListDataStore

SharePointリストとそのアイテムをクロールします。

必要な権限

権限必須/条件付き条件
Sites.Read.All必須site_idを指定しない場合(すべてのサイトを列挙)
Sites.Selected必須site_idを指定した場合(Sites.Read.Allの代替)

使用されるGraph APIエンドポイント

エンドポイント用途条件
GET /sitesすべてのサイトを取得site_idを指定していない場合
GET /sites/{id}特定のサイトを取得site_idを指定した場合
GET /sites/{id}/listsリストを取得常時
GET /sites/{id}/lists/{id}特定のリストを取得list_idを指定した場合
GET /sites/{id}/lists/{id}/itemsリストアイテムを取得常時
GET /sites/{id}/lists/{id}/items/{id}アイテムの再取得(フォールバック)フィールドが空の場合
GET /sites/{id}/permissionsサイトの権限を取得権限用

SharePointPageDataStore

SharePointページ(サイトページ、ニュースページ、Wikiページなど)をクロールします。

必要な権限

権限必須/条件付き条件
Sites.Read.All必須site_idを指定しない場合(すべてのサイトを列挙)
Sites.Selected必須site_idを指定した場合(Sites.Read.Allの代替)

使用されるGraph APIエンドポイント

エンドポイント用途条件
GET /sitesすべてのサイトを取得site_idを指定していない場合
GET /sites/{id}特定のサイトを取得site_idを指定した場合
GET /sites/{id}/pagesサイトページを取得常時
GET /sites/{id}/pages/{id}ページのコンテンツを取得常時
GET /sites/{id}/permissionsサイトの権限を取得権限用

Sites.Selected権限の設定方法

SharePoint系のDataStore(SharePointDocLib、SharePointList、SharePointPage)でsite_idを指定する場合、Sites.Read.Allの代わりにSites.Selectedを使用できます。これにより、特定のサイトにのみアクセスを限定できます。

設定手順

  1. Azure Portalでアプリ登録にSites.Selected権限を付与
  2. Microsoft Graph PowerShellまたはAPIを使用して、特定サイトへのアクセス権を付与
  3. 各対象サイトへのアクセスを明示的に許可

参考: https://learn.microsoft.com/en-us/graph/permissions-reference#sitesselected

全権限一覧

権限用途使用するDataStore
User.Read.Allユーザー情報、ライセンス確認OneDrive, OneNote, Teams
Group.Read.AllMicrosoft 365グループの取得OneDrive, OneNote, Teams
Files.Read.Allファイルとドライブの読み取りOneDrive, SharePointDocLib, Teams(添付ファイル)
Notes.Read.AllOneNoteコンテンツの読み取りOneNote
Sites.Read.AllすべてのSharePointサイトの列挙OneDrive, OneNote, SharePointDocLib, SharePointList, SharePointPage
Sites.Selected特定サイトのみへのアクセスSharePointDocLib, SharePointList, SharePointPage(site_id指定時)
Team.ReadBasic.Allチームの基本情報Teams
Channel.ReadBasic.Allチャンネルの基本情報Teams
ChannelMessage.Read.Allチャンネルメッセージの読み取りTeams
ChannelMember.Read.Allチャンネルメンバーの読み取りTeams
Chat.Read.Allチャットメッセージの読み取りTeams(chat_id指定時)

最小権限の考え方

アプリケーションに付与する権限は、必要最小限に抑えることが推奨されます。

  1. OneDriveDataStore: 不要なクローラーを無効化
  2. user_drive_crawler=falseでUser.Read.Allを省略
  3. group_drive_crawler=falseでGroup.Read.Allを省略
  4. shared_documents_drive_crawler=falseでSites.Read.Allを省略

  5. OneNoteDataStore: OneDriveと同様のパターン

  6. user_note_crawler=falsegroup_note_crawler=falsesite_note_crawler=falseを必要に応じて設定

  7. SharePoint系DataStore: site_idを指定

  8. Sites.Read.Allの代わりにSites.Selectedを使用
  9. Azure ADでサイトごとの追加設定が必要

  10. TeamsDataStore: オプション機能をスキップ

  11. chat_idを指定しなければChat.Read.Allは不要
  12. append_attachment=falseでFiles.Read.Allは不要

まとめ

fess-ds-microsoft365プラグインでは、使用するDataStoreと設定パラメータによって必要な権限が異なります。本ガイドを参考に、使用する機能に応じた最小限の権限を設定してください。Entra IDの管理者に権限申請する際にも、「このDataStoreを使うからこの権限が必要」と具体的に説明できるようになります。