FSCrawler を使って検索用ストレージサイズの概算見積りを行う

BLOG

ファイルサーバー等の検索システム構築において、事前に必要なストレージ容量を試算する際の手順を解説します。

今回は、FSCrawler (*1) を使って検索用ストレージサイズの概算見積りを行ってみたいと思います。

(*1) Elastic の正式な製品ではありませんが、ファイルを検索できるよう Elasticsearch に登録してくれるオープンソースです。

1. 検証の概要

本記事では、サンプルファイルを FSCrawler で Elasticsearch に取り込み、作成されたインデックスの物理ストレージサイズを取得することで、 ストレージ容量の見積り倍率(元のファイルサイズに対するインデックスサイズの割合)を算出する手順を解説します。

2. 環境

  • Windows 11
  • Elasticsearch 9.5.2 (Self-Managed, Trial License)
  • Elastic Cloud (セマンティック検索用)
  • FSCrawler 3.0
  • Java 25.0.4.1

3. FSCrawler の初期化と設定

下記を参考にして FSCrawler をインストールします。

FSCrawler の初期設定を行います。

参考URL: https://fscrawler.readthedocs.io/en/fscrawler-3.0/user/getting_started.html

set JAVA_HOME=jdk-25.0.4.1のインストールフォルダ
set FS_JAVA_OPTS=-Xmx2g -Xms2g

bin\fscrawler.bat --setup

C:\Users\username\.fscrawler\fscrawler\_settings.yaml ファイルが生成されるので、これを編集します。

(下記はあくまでもサンプルです。適宜修正してください。)

name: "test"
fs:
  url: "C:/tmp/es"
  update_rate: "15m"
  includes:
    - "**/*.pdf"
    - "**/*.docx"
    - "**/*.xlsx"
    - "**/*.pptx"
  excludes:
    - "**/*.bak"
  hash_algorithm: "SHA-256"
  raw_metadata: true
  ocr:
    enabled: false
elasticsearch:
  urls:
    - "https://127.0.0.1:9200"
  index: "test_docs"
  index_folder: "test_folder"
  api_key: "YOUR_API_KEY"
  ca_certificate: "ca.crtファイルのパス"
  #pipeline: "my_pipeline"
  semantic_search: "true"

  • 検索対象のフォルダを C:\tmp\es としておきます。
  • 今回は、*.pdf, *.docx, *.xlsx, *.pptx ファイルを検索対象とします。
  • セマンティック検索:あり としておきます。
  • API_KEYは、Kibana 上で発行しておきます。

※セマンティック検索を行う場合、EIS の設定を行っておくなど、Elasticsearch 側での事前準備が必要です。

4. 検索用ファイルの配置

C:\tmp\es フォルダ配下に検索対象となるテスト用ファイル(例: 合計 1 GB の PDF や Office 文書)を配置します。

ここでは、東京都防災ホームページ からダウンロードした 「東京都くらし防災」(全ページ) (13.5MBのpdf) を C:\tmp\es\pdf\kb2023-tokyo-all.pdf として配置します。

5. FSCrawlerの実行

FSCrawlerを実行してインデックス化を行います。

set JAVA_HOME=jdk-25.0.4.1のインストールフォルダ
set FS_JAVA_OPTS=-Xmx2g -Xms2g

bin\fscrawler.bat

FSCrawlerの実行に成功すると、C:\Users\username\.fscrawler\test\_checkpoint.json ファイルが作成されます。

また、Elasticsearch の test_docs インデックスにドキュメントが登録されます。

6. 登録確認

Kibana の DevTool から下記のクエリを実行してみます(キーワード検索)。

GET /test_docs/_search
{
  "query": {
    "match": {
      "content": "寝室での注意事項"
    }
  }
}

ドキュメントが返却されます。

続いて、下記のリクエストも実行してみます(セマンティック検索)。

GET /test_docs/_search
{
  "query": {
    "match": {
      "content_semantic": "寝室での注意事項"
    }
  },
  "highlight": {
    "fields": {
      "content_semantic": {
        "number_of_fragments": 2,
        "order": "score"
      }
    }
  }
}

こちらもドキュメントが返却されます。

7. インデックスサイズの確認

Kibana の Stack Management / Index Management の画面から test_docs インデックスのページを表示します。

プライマリインデックスのサイズが 568.65 KB, レプリカと合わせると 1.11 MB であることがわかります。

8. セマンティック検索:なしで再計測

content フィールドに対するセマンティック検索用データ(ベクトルデータ)を生成しないようにして再計測してみます。

また、content フィールドの analyzer をデフォルトの standard ではなく、日本語用の analyzer を使うよう設定しておきます。

※ Elasticsearchに事前に analysis-icu, analysis-kuromoji のインストールが必要です。

登録先のインデックスを test2_docs とします。

下記のリクエストを Dev Tool から発行して test2_doc インデックスを作成しておきます。

PUT /test2_docs/
{
  "settings": {
    "index": {
      "number_of_replicas": 1
    },
    "analysis": {
      "char_filter": {
        "windows_separator": {
          "type": "mapping",
          "mappings": [
            """\\ => /"""
          ]
        },
        "ja_normalizer": {
          "type": "icu_normalizer",
          "name": "nfkc_cf",
          "mode": "compose"
        }
      },
      "tokenizer": {
        "fscrawler_path": {
          "type": "path_hierarchy"
        },
        "ja_kuromoji_tokenizer": {
          "mode": "search",
          "type": "kuromoji_tokenizer",
          "discard_compound_token": true,
          "user_dictionary_rules": [
          ]
        }
      },
      "filter": {
        "ja_search_synonym": {
          "type": "synonym_graph",
          "lenient": false,
          "updateable": false,
          "expand": true,
          "synonyms": [
          ]
        }
      },
      "analyzer": {
        "fscrawler_path": {
          "char_filter": [
            "windows_separator"
          ],
          "tokenizer": "fscrawler_path"
        },
        "ja_kuromoji_index_analyzer": {
          "type": "custom",
          "char_filter": [
            "ja_normalizer",
            "kuromoji_iteration_mark"
          ],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "cjk_width",
            "ja_stop",
            "kuromoji_number",
            "kuromoji_stemmer"
          ]
        },
        "ja_kuromoji_search_analyzer": {
          "type": "custom",
          "char_filter": [
            "ja_normalizer",
            "kuromoji_iteration_mark"
          ],
          "tokenizer": "ja_kuromoji_tokenizer",
          "filter": [
            "kuromoji_baseform",
            "kuromoji_part_of_speech",
            "cjk_width",
            "ja_stop",
            "kuromoji_number",
            "kuromoji_stemmer",
            "ja_search_synonym"
          ]
        }
      }
    }
  },
  "mappings": {
    "dynamic_templates": [
      {
        "raw_as_text": {
          "path_match": "meta.raw.*",
          "mapping": {
            "fields": {
              "keyword": {
                "ignore_above": 256,
                "type": "keyword"
              }
            },
            "type": "text"
          }
        }
      }
    ],
    "properties": {
      "attachment": {
        "type": "binary"
      },
      "attributes": {
        "properties": {
          "acl": {
            "properties": {
              "flags": {
                "type": "keyword"
              },
              "permissions": {
                "type": "keyword"
              },
              "principal": {
                "type": "keyword"
              },
              "type": {
                "type": "keyword"
              }
            }
          },
          "group": {
            "type": "keyword"
          },
          "owner": {
            "type": "keyword"
          }
        }
      },
      "content": {
        "type": "text",
        "analyzer": "ja_kuromoji_index_analyzer",
       "search_analyzer": "ja_kuromoji_search_analyzer"
      },
      "file": {
        "properties": {
          "checksum": {
            "type": "keyword"
          },
          "content_type": {
            "type": "keyword"
          },
          "created": {
            "type": "date",
            "format": "date_optional_time"
          },
          "extension": {
            "type": "keyword"
          },
          "filename": {
            "type": "keyword",
            "store": true
          },
          "filesize": {
            "type": "long"
          },
          "indexed_chars": {
            "type": "long"
          },
          "indexing_date": {
            "type": "date",
            "format": "date_optional_time"
          },
          "last_accessed": {
            "type": "date",
            "format": "date_optional_time"
          },
          "last_modified": {
            "type": "date",
            "format": "date_optional_time"
          },
          "url": {
            "type": "keyword",
            "index": false
          }
        }
      },
      "meta": {
        "properties": {
          "altitude": {
            "type": "text"
          },
          "author": {
            "type": "text"
          },
          "comments": {
            "type": "text"
          },
          "contributor": {
            "type": "text"
          },
          "coverage": {
            "type": "text"
          },
          "created": {
            "type": "date",
            "format": "date_optional_time"
          },
          "creator_tool": {
            "type": "keyword"
          },
          "date": {
            "type": "date",
            "format": "date_optional_time"
          },
          "description": {
            "type": "text"
          },
          "format": {
            "type": "text"
          },
          "identifier": {
            "type": "text"
          },
          "keywords": {
            "type": "text"
          },
          "language": {
            "type": "keyword"
          },
          "latitude": {
            "type": "text"
          },
          "longitude": {
            "type": "text"
          },
          "metadata_date": {
            "type": "date",
            "format": "date_optional_time"
          },
          "modifier": {
            "type": "text"
          },
          "print_date": {
            "type": "date",
            "format": "date_optional_time"
          },
          "publisher": {
            "type": "text"
          },
          "rating": {
            "type": "byte"
          },
          "relation": {
            "type": "text"
          },
          "rights": {
            "type": "text"
          },
          "source": {
            "type": "text"
          },
          "title": {
            "type": "text"
          },
          "type": {
            "type": "text"
          }
        }
      },
      "path": {
        "properties": {
          "real": {
            "type": "keyword",
            "fields": {
              "fulltext": {
                "type": "text"
              },
              "tree": {
                "type": "text",
                "analyzer": "fscrawler_path",
                "fielddata": true
              }
            }
          },
          "root": {
            "type": "keyword"
          },
          "virtual": {
            "type": "keyword",
            "fields": {
              "fulltext": {
                "type": "text"
              },
              "tree": {
                "type": "text",
                "analyzer": "fscrawler_path",
                "fielddata": true
              }
            }
          }
        }
      }
    }
  }
}

先ほど実行していた fscrawler.bat を Ctrl+C で停止させます。

C:\Users\username\.fscrawler\fscrawler\_settings.yaml ファイルを修正します。

  • name : test -> test2
  • index : test_docs -> test2_docs
  • index_folder : test_folder -> test2_folder
  • semantic_search : true -> false
name: "test2"
fs:
  url: "C:/tmp/es"
  update_rate: "15m"
  includes:
    - "**/*.pdf"
    - "**/*.docx"
    - "**/*.xlsx"
    - "**/*.pptx"
  excludes:
    - "**/*.bak"
  hash_algorithm: "SHA-256"
  raw_metadata: true
  ocr:
    enabled: false
elasticsearch:
  urls:
    - "https://127.0.0.1:9200"
  index: "test2_docs"
  index_folder: "test2_folder"
  api_key: "YOUR_API_KEY"
  ca_certificate: "ca.crtファイルのパス"
  #pipeline: "my_pipeline"
  semantic_search: "false"

fscrawler.bat を再実行します。

set JAVA_HOME=jdk-25.0.4.1のインストールフォルダ
set FS_JAVA_OPTS=-Xmx2g -Xms2g

bin\fscrawler.bat

test2_docs インデックスへドキュメントが登録されます。

Kibana の Stack Management / Index Management の画面から test2_docs インデックスのページを表示します。

プライマリインデックスのサイズが 223.02 KB, レプリカと合わせると 446.04 KB となっています。

9. セマンティック検索:「あり」と「なし」のストレージサイズの比較

条件元ファイルのサイズプライマリインデックスのサイズインデックスサイズ/元ファイルサイズの比率
セマンティック検索あり13870.63 KB568.65 KB4.1%
セマンティック検索なし13870.63 KB223.02 KB1.6%

セマンティック検索用のベクトルデータを生成しない分、ストレージサイズが減少したことがわかります。

※この結果は、入力ファイルにより変動するので、実際に使用するファイルで試すことをお勧めします。

10. 概算見積りの計算ポイントと影響要因

実際の容量見積もりを行う際は、以下の要素を考慮して計算式を組み立てます。

  • テキスト抽出比率: PDF や Office ファイル内のテキストデータ割合に依存します。スキャン画像主体の PDF ではインデックスサイズが小さくなり、テキスト主体の文書では大きくなります。
  • レプリカ数の乗算: 本番環境で冗長化のためにレプリカ数を 1 に設定する場合、ストレージ要件は プライマリサイズ × 2 になります。

要件に近い形でサンプルデータのインデックス登録を行い、ストレージサイズを取得します。

サンプルデータで算出した倍率を全体のファイルサーバー容量に掛け合わせることで、Elasticsearch 用ストレージの概算見積もりが可能となります。

想定ストレージサイズ = 対象ファイルの総容量 x 検証で得られたインデックス比率 x (1 + レプリカ数)

※注意 FSCrawler は、基本的に 1 ファイル = 1 ドキュメントとしてインデックスに登録します。

11. まとめ

今回は FSCrawler を活用し、サンプルファイルを用いて Elasticsearch の検索用ストレージサイズを概算見積もりする手順をご紹介しました。

今回の検証における主なポイントは以下の通りです。

  • セマンティック検索(ベクトルデータ)の影響: セマンティック検索を有効にすると、テキストデータに加えてベクトルデータが保持されるため、ストレージサイズが大きくなります(今回の検証では元ファイル比で 1.6% から 4.1% に増加)。
  • 実ファイルを用いた事前検証の重要性: PDF や Office 文書内のテキスト抽出比率によってサイズが変動するため、本番環境に近いサンプルファイルで試算することが精度向上の鍵となります。
  • 冗長化構成(レプリカ数)の考慮: プライマリサイズだけでなく、本番環境のレプリカ設定(例: レプリカ 1 の場合はプライマリ × 2)を忘れずに計算式へ組み込む必要があります。

検索システムの新規構築や移行におけるクラスタ設計・キャパシティプランニングの際、ぜひ本記事の手順を参考に試算してみてください。

12. 参考URL