はじめに
NAS内のファイル数が数十万規模になると、ファイル名検索では目的のデータを見つけにくくなり、ディレクトリー構造の複雑化で検索効率も低下します。
このような環境では全文検索が有効です。
本シリーズでは、NAS内のファイルを高速かつ容易に検索するため、オープンソースの 全文検索サーバー Fess を用いて検索サーバーを構築する手順を紹介します。
検索サーバーのホストとしてRaspberry Pi 5を採用し、低消費電力で常時稼働可能な検索ノードの設置を目標としました。
本記事は全3回のうち 第3回 Fess 構成 です。
シリーズ一覧
- Raspberry Pi × Fess で全文検索環境構築 (1) Pi 5 セットアップ
- Raspberry Pi × Fess で全文検索環境構築 (2) Fess インストール
- Raspberry Pi × Fess で全文検索環境構築 (3) Fess 構成
前提
- Ollamaが使用可能であること
- Fess、OpenSearch、Nginxが構成済みであること
Fess構成
管理者パスワードの変更
Webブラウザーから https://<FESS>/admin にアクセスし、ユーザー名 admin、パスワード admin でログインします。

全般の設定
管理画面から システム > 全般 に移動します。
Raspberry Pi 5のCPUコア数 4 にあわせてクロール時の総スレッド数を 4 にします。そのため、同時クローラー数を 1、クロール設定(後述)内のスレッド数を 4 に設定します。
| カテゴリー | 項目 | 設定値 |
|---|---|---|
| システム | サムネイルの表示 | 有効 |
| クローラー | 同時クローラー設定 | 1 |
プラグインの設定
システム > プラグインに移動して インストール をクリックします。

fess-llm-ollama-15.8.0 を選択して インストール をクリックします。

管理画面からログアウトします。
Fessを再起動します。
$ sudo systemctl restart fess
クロール設定の作成
クロール設定
Webブラウザーから https://<FESS>/admin にアクセスし、ユーザー admin でログインします。
クローラー > ファイルシステムに移動して 新規作成 をクリックします。

クロールに必要な値を入力して設定を保存します。
設定項目の説明は以下にあります。
ファイルクロール (ファイルサーバー検索)
設定項目のうち、スレッド数を 4 に変更するとクロール時のスレッド数が 4 になります。
認証設定
NASへのアクセスにユーザー名、パスワードが必要な場合ファイル認証設定を追加します。
クローラー > ファイル認証に移動して 新規作成 をクリックします。

認証に必要な情報を入力して設定を保存します。
設定項目の説明は以下にあります。
ファイル認証
クロールの実行
クロールを手動開始して動作を確認します。
システム > スケジューラ で Default Crawler を開きます。

サムネイル生成のための追加設定
クロール対象を smb:// で指定の場合、サムネイル作成に失敗します。これは generate-thumbnail スクリプトが file:// で始まるファイルのサムネイル作成を前提としていることによります。
また、unoconvコマンドを前提の generate-thumbnail コマンドが、unoconv を使用できないRaspberry Piでは動作しない問題があります。
smb:// で始まるファイルのサムネイル作成可能化
generate-thumbnail スクリプトに file:// でファイルのパスを指定可能にするため、クロール対象のNASフォルダーをマウントします。
マウント・ポイントを作成します。
$ sudo mkdir -p /mnt/nas/<SHARED_FOLDER>
NAS接続ユーザー名とパスワードを設定ファイルに記述します。
$ sudo nano /etc/nas-credentials
username=<USERNAME>
password=<PASSWORD>
設定ファイルの権限を変更します。
$ sudo chown root:root /etc/nas-credentials
$ sudo chmod 600 /etc/nas-credentials
Raspberry Pi起動時にNASを自動マウントするように設定を追加します。
$ sudo nano /etc/fstab
fstab への追記例は以下のとおりです。
<NAS_SHARED_FOLDER_PATH> /mnt/nas/<SHARED_FOLDER> cifs credentials=/etc/nas-credentials,iocharset=utf8,uid=fess,gid=fess,ro,file_mode=0555,dir_mode=0555,noauto,x-systemd.automount,x-systemd.mount-timeout=10 0 0
マウント設定を反映します。
$ sudo systemctl daemon-reload
$ sudo systemctl restart remote-fs.target
$ sudo mount -a
generate-thuubnail の変更
Fess提供の generate-thumbnail に以下の変更を追加します。
- サムネイル作成対象のファイル名が
smb://で始まる場合、file://に変換 -
unoconv使用の処理をsofficeまたはlibreofficeに変更
変更前にバックアップを作成し、generate-thumbnail を編集します。
$ sudo cp -p /usr/share/fess/bin/generate-thumbnail /usr/share/fess/bin/generate-thumbnail.default
$ sudo nano /usr/share/fess/bin/generate-thumbnail
変更後の /usr/share/fess/bin/generate-thumbnail 全量を表示するにはクリック
9行目、10行目の XXXXXXXX は環境にあわせて変更します。#!/bin/bash
cmd_type=$1
url=$2
output_file=$3
mimetype=${4:-}
image_size=100x100
if [[ "$url" =~ ^smb://XXXXXXXX ]]; then
target_file=$(echo "$url" | sed -E 's|smb://XXXXXXXX[^/]+/|/mnt/nas/|')
target_file=$(python3 -c "import sys, urllib.parse; print(urllib.parse.unquote(sys.argv[1]))" "$target_file")
else
target_file=$(echo "$url" | sed -e "s#^file:/*#/#g")
fi
check_command() {
cmd=$1
cmd_path=$(command -v "${cmd}")
if [[ ! -e "${cmd_path}" ]] ; then
echo "${cmd} does not exist."
exit 1
fi
}
get_imagemagick_cmd() {
if command -v magick >/dev/null 2>&1; then
echo "magick"
elif command -v convert >/dev/null 2>&1; then
echo "convert"
else
echo ""
fi
}
if [[ x"$HOME" = "x/root" ]] || [[ x"$HOME" = "x/var/root" ]] ; then
HOME=/var/lib/fess
fi
if [[ x"${cmd_type}" = "xmsoffice" ]] ; then
im_cmd=$(get_imagemagick_cmd)
if [[ -z "${im_cmd}" ]] ; then
echo "ImageMagick (convert or magick) does not exist."
exit 1
fi
check_command pdftoppm
if command -v soffice >/dev/null 2>&1; then
lo_cmd="soffice"
elif command -v libreoffice >/dev/null 2>&1; then
lo_cmd="libreoffice"
elif command -v unoconv >/dev/null 2>&1; then
lo_cmd="unoconv"
else
echo "Neither LibreOffice nor unoconv exists."
exit 1
fi
tmp_dir="/tmp/fess_msoffice_$$"
mkdir -p "${tmp_dir}"
tmp_pdf_file="${tmp_dir}/converted.pdf"
if [[ "${lo_cmd}" = "unoconv" ]]; then
unoconv -e PageRange=1-1 -o "${tmp_pdf_file}" -f pdf "${target_file}"
else
"${lo_cmd}" --headless --convert-to pdf --outdir "${tmp_dir}" "${target_file}" >/dev/null 2>&1
generated_pdf=$(ls "${tmp_dir}"/*.pdf 2>/dev/null | head -n 1)
if [[ -n "${generated_pdf}" ]]; then
mv "${generated_pdf}" "${tmp_pdf_file}"
fi
fi
if [[ ! -f "${tmp_pdf_file}" ]] ; then
rm -rf "${tmp_dir}"
echo "Office to PDF conversion failed."
exit 1
fi
tmp_png_prefix="${tmp_dir}/thumb"
pdftoppm -png -singlefile -r 150 "${tmp_pdf_file}" "${tmp_png_prefix}"
tmp_png_file="${tmp_png_prefix}.png"
if [[ ! -f "${tmp_png_file}" ]] ; then
rm -rf "${tmp_dir}"
echo "pdftoppm does not work."
exit 1
fi
${im_cmd} "${tmp_png_file}" -thumbnail ${image_size} "${output_file}"
rm -rf "${tmp_dir}"
elif [[ x"${cmd_type}" = "xpdf" ]] ; then
im_cmd=$(get_imagemagick_cmd)
if [[ -z "${im_cmd}" ]] ; then
echo "ImageMagick (convert or magick) does not exist."
exit 1
fi
check_command pdftoppm
tmp_png_prefix="/tmp/fess_thumb_$$"
pdftoppm -png -singlefile -r 150 "${target_file}" "${tmp_png_prefix}"
tmp_png_file="${tmp_png_prefix}.png"
if [[ ! -f "${tmp_png_file}" ]] ; then
echo "pdftoppm does not work."
exit 1
fi
${im_cmd} "${tmp_png_file}" -thumbnail ${image_size} "${output_file}"
rm -f "${tmp_png_prefix}"*png
elif [[ x"${cmd_type}" = "xps" ]] ; then
im_cmd=$(get_imagemagick_cmd)
if [[ -z "${im_cmd}" ]] ; then
echo "ImageMagick (convert or magick) does not exist."
exit 1
fi
check_command pdftoppm
check_command ps2pdf
tmp_pdf_file=/tmp/thumbnail.pdf.$$
ps2pdf "${target_file}" ${tmp_pdf_file}
if [[ ! -f ${tmp_pdf_file} ]] ; then
echo "ps2pdf does not work."
exit 1
fi
tmp_png_prefix=/tmp/thumbnail.png.$$
pdftoppm -png -singlefile ${tmp_pdf_file} ${tmp_png_prefix}
tmp_png_file="${tmp_png_prefix}.png"
rm -f ${tmp_pdf_file}
if [[ ! -f ${tmp_png_file} ]] ; then
echo "pdftoppm does not work."
exit 1
fi
${im_cmd} "${tmp_png_file}" -thumbnail ${image_size} "${output_file}"
rm -f ${tmp_png_prefix}*png
elif [[ x"${cmd_type}" = "xsvg" ]] ; then
check_command rsvg-convert
rsvg-convert -w 100 -h 100 --keep-aspect-ratio "${target_file}" -o "${output_file}"
elif [[ x"${cmd_type}" = "ximage" ]] ; then
im_cmd=$(get_imagemagick_cmd)
if [[ -z "${im_cmd}" ]] ; then
echo "ImageMagick (convert or magick) does not exist."
exit 1
fi
format_hint=""
case "${mimetype}" in
"image/gif") format_hint="gif:" ;;
"image/tiff") format_hint="tiff:" ;;
"image/png") format_hint="png:" ;;
"image/jpeg") format_hint="jpeg:" ;;
"image/bmp"|"image/x-windows-bmp"|"image/x-ms-bmp") format_hint="bmp:" ;;
"image/vnd.adobe.photoshop"|"image/photoshop"|"application/x-photoshop"|"application/photoshop") format_hint="psd:" ;;
esac
${im_cmd} "${format_hint}${target_file}" -thumbnail ${image_size} "${output_file}"
elif [[ x"${cmd_type}" = "x" ]] ; then
echo "No filetype."
exit 1
else
echo "Unsupported type: ${cmd_type}"
exit 1
fi
if [[ ! -f ${output_file} ]] ; then
echo "Thumbnail is not created."
exit 1
fi
検索結果
通常の検索、AI検索結果の例を以下に示します。
結論
NAS内のファイルに対する索引作成により、検索にかかる時間は従来のファイル名検索と比べて大幅に短縮されました。
また、全文検索によりファイル内容まで検索可能になり、目的の情報へ迅速に到達できるようになりました。
さらにLLMと組み合わせたAI検索は自然言語での検索が可能となり情報検索の質が向上しました。
Raspberry Piを使用した低消費電力の検索サーバーは常時稼働に適しており、NAS内の情報資産を効率的に活用できる環境が整いました。
一方でNAS全体の差分検出には長時間を要しており、現在は全量クロールがボトルネックとなっています。差分ファイルのみの効率よくクロールできる仕組みづくりが今後の課題です。




