Markdown 文書のための再帰チャンキング入門 — Elasticsearch での実践と比較

BLOG

はじめに

RAG(Retrieval-Augmented Generation)や検索アプリケーションの構築において、Markdown文書の「チャンキング戦略」に頭を悩ませたことはありませんか?

従来、Markdown文書に対して単純な word(単語数)や sentence(文)単位でのチャンキングを行うと、見出しと本文が分離してしまったり、文脈が複数のブロックに分断されたりするケースが多々ありました。その結果、検索結果にノイズが含まれ、回答精度の低下を招く要因となっていました。

Elasticsearch 8.19.0 および 9.1.0 では、この課題を解決する新たな戦略として再帰チャンキング(Recursive Chunking)が追加されました。

本記事では、この再帰チャンキングの仕組みと、実際に有価証券報告書(Markdown形式)を用いたインデキシングおよび検索実験の結果をご紹介します。

※補足: 本ブログで使用したスクリプトやテストデータは、下記のリポジトリで公開しています。

再帰チャンキング (Recursive Chunking) とは

概略

再帰チャンキングは、定義されたセパレーター(区切り文字)のリストに基づいて、文書を階層的かつ再帰的に分割する手法です。
Markdownの見出し構造(#, ##, ###…)をセパレーターとして定義することで、文書の論理構造を保ったままチャンクを生成できます。

例:

"chunking_settings": {
  "strategy": "recursive",
  "max_chunk_size": 300,
  "separators": [
    "\n# ",
    "\n## ",
    "\n### ",
    "\n#### ",
    "\n##### ",
    "\n###### ",
    "\n^(?!\\s*$).*\\n-{1,}\\n",
    "\n^(?!\\s*$).*\\n={1,}\\n"
  ]
}

※ “separators” の指定は、 “separator_group” : “markdown” と書くことも可能ですが、
ここではカスタマイズ性を重視し、 “separators” を明示的に記述する形式で解説します。

分割のイメージ

この戦略を用いると、Markdown文書は以下のように構造的に分割されます。

元の Markdown 文書

# 1. 企業の概況

## 1.1. 主要な経営指標

### 1.1.1. 売上高の推移
(本文テキスト...)

### 1.1.2. 利益の推移
(本文テキスト...)

## 1.2. 沿革
(本文テキスト...)

# 2. 事業の内容
(本文テキスト...)

Recursive Chunkingによる分割結果

チャンク1

# 1. 企業の概況

## 1.1. 主要な経営指標

### 1.1.1. 売上高の推移
(本文テキスト...)

チャンク2

### 1.1.2. 利益の推移
(本文テキスト...)

チャンク3

## 1.2. 沿革
(本文テキスト...)

チャンク4

# 2. 事業の内容
(本文テキスト...)

※重要なポイント:

構造を無視した分割(例:1.1.2. の途中から始まり、1.2. の冒頭が含まれるなど)が発生しません。
見出しの階層が変わるタイミングで適切に分割されるため、各チャンクが「意味のあるまとまり」を持ちます。

参考URL

Recursive chunking in Elasticsearch for structured documents - Elasticsearch Labs
Learn how to configure recursive chunking in Elasticsearch with chunk size, separator groups, and custom separator lists...
Chunking strategies: Elasticsearch chunking & its strategies - Elasticsearch Labs
Learn the fundamentals of document chunking in Elasticsearch, compare different chunking strategies, and discover how yo...
Elasticsearch chunking: Set up chunking for inference endpoints - Elasticsearch Labs
Explore Elasticsearch chunking strategies, learn how Elasticsearch chunks text, and how to configure chunking settings f...
Inference integrations | Elastic Docs
Elasticsearch provides a machine learning inference API to create and manage inference endpoints that integrate with ser...

実践:インデキシング

実際に Elasticsearch を用いて、Markdown文書の取り込みとベクトル化を行います。

※使用するデータについて

テストデータとして、サイオス株式会社の2024年12月期の有価証券報告書を使用します。

本検証作業では、上記の PDF の内容を抜粋、改変して markdown 化したものを利用します。

1. 準備

2. Inference Endpoint の作成

ドキュメント取り込み時に「チャンク分割」と「密ベクトル生成」を行うための inference endpoint を作成します。
ここで strategy: “recursive” を指定します。

参考URL: https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-inference-put-elasticsearch

Kibana の Dev Tools (Console) から次のリクエストを発行します。

PUT _inference/text_embedding/e5_chunk_recursive
{
  "service": "elasticsearch",
      "service_settings": {
        "num_allocations": 1,
        "num_threads": 1,
        "model_id": ".multilingual-e5-small_linux-x86_64"
      },
      "chunking_settings": {
        "strategy": "recursive",
        "max_chunk_size": 300,
        "separators": [
          "\n# ",
          "\n## ",
          "\n### ",
          "\n#### ",
          "\n##### ",
          "\n###### "
        ]
      }
}

※今回のテストデータでは、— や === による区切りは使用していないので、separators から除外してシンプルにしています。

3. インデックスの作成

日本語検索に最適化するため、kuromoji と icu_normalizer を設定したインデックスを作成します。

PUT /ir_report_2024_chunk_recursive
{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 1,
      "refresh_interval": "3600s"
    },
    "analysis": {
      "char_filter": {
        "ja_normalizer": {
          "type": "icu_normalizer",
          "name": "nfkc_cf",
          "mode": "compose"
        }
      },
      "tokenizer": {
        "ja_kuromoji_tokenizer": {
          "mode": "search",
          "type": "kuromoji_tokenizer",
          "discard_compound_token": true
        }
      },
      "analyzer": {
        "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"
          ]
        }
      }
    }
  }
}

4. マッピングの追加

作成したエンドポイント (e5_chunk_recursive) を使用するようにマッピングを定義します。
semantic_text 型を利用することで、テキスト処理とベクトル化を自動化します。

PUT /ir_report_2024_chunk_recursive/_mappings
{
  "dynamic": false,
  "_source": {
    "excludes": [
      "content.text_embedding"
    ]
  },
  "properties": {
    "content": {
      "type": "text",
      "analyzer": "ja_kuromoji_index_analyzer",
      "search_analyzer": "ja_kuromoji_search_analyzer",
      "fields": {
        "text_embedding": {
          "type": "semantic_text",
          "inference_id": "e5_chunk_recursive"
        }
      }
    }
  }
}

5. ドキュメントの取り込み

Markdown化した有価証券報告書データを取り込みます。(以下は抜粋です)

POST /ir_report_2024_chunk_recursive/_doc
{
  "content": """
# 第1 【企業の概況】

## 1 【主要な経営指標等の推移】

### (1) 連結経営指標等

| 回次 | 第24期 | 第25期 | 第26期 | 第27期 | 第28期 |
|:--|:--|:--|:--|:--|:--:|
| 決算年月 | 2020年12月 | 2021年12月 | 2022年12月 | 2023年12月 | 2024年12月 |
| 売上高(千円) | 14,841,739 | 15,725,371 | 14,420,269 | 15,889,487 | 20,561,583 |

...

当連結会計年度におきましては、個別決算において関係会社株式の減損による特別損失の計上を行うことから、利
益剰余金が大幅に減少することになり、誠に遺憾ではございますが、期末配当を無配とさせていただきたいと存じま
す。
"""
}

全文は、下記を参照してください。

File not found ?? sios-elastic-tech/blogs
A sample code for blogs about elasticsearch. Contribute to sios-elastic-tech/blogs development by creating an account on...

6. データの反映

データを検索可能にするため、リフレッシュを行います。

POST /ir_report_2024_chunk_recursive/_refresh

実践:検索と結果検証

取り込んだデータに対してセマンティック検索を行い、再帰チャンキングの効果を確認します。

ケース1. 表データの検索

クエリ: “2024年度の有給休暇取得率”

GET /ir_report_2024_chunk_recursive/_search
{
  "_source": false,
  "query": {
    "semantic": {
      "field": "content.text_embedding",
      "query": "2024年度の有給休暇取得率"
    }
  },
  "highlight": {
    "fields": {
      "content.text_embedding": {
        "order": "score",
        "number_of_fragments": 1
      }
    }
  }
}

検索結果

"""
### (3) 指標及び目標

当社グループは、上記の「(2)①人材の多様性の確保を含む人材の育成に関する方針」及び「(2)②社内環境整備に
関する方針」について、次の指標を用いています。当該指標に関する目標及び実績は、次の通りです。

| 指標 | 2023年度実績 | 2024年度実績 | 目標 |
|:--|:--|:--|:--:|
| 管理職に占める女性労働者の割合 | 15.5% | 10.5% | 20%(2025年度) |
| 男性労働者の育児休業取得率 | 38.5% | 100.0% | 100%(2025年度) |
| 労働者の男女の賃金の差異 | 76.3% | 78.4% | 80%(2025年度) |
| 有給休暇取得率 | 74.9% | 69.8% | 80%(2025年度) |
| 離職率 | 6.4% | 7.7% | 5%以下(毎年) |
| 月平均所定外残業時間 | 14.3時間 | 14.1時間 | − |
| 障がい者雇用率 | 2.96% | 2.48% | − |

(注) 1.国内グループ会社(当社、サンプルテクノロジー株式会社)を対象に計算しています。

2.当社グループでは定年制を廃止しているため、離職率については、すべての退職者を含めて
計算しています。

3.目標の「−」は、設定がないことを示しています。
"""

考察

Markdownの表全体が一つのチャンクとして綺麗に取得できており、必要な文脈(表のヘッダーや注釈)が保持されています。
LLM にこのテキストを渡した場合でも、表構造を正しく解釈できると思われます。

比較:Sentence Chunking の場合

一方で、sentence 戦略を用いた場合、表の途中でチャンクが分割されてしまい、ヘッダー情報が欠落するケースが見られました。

Sentence Chunking での検索結果の抜粋:

"""(100-999人)では管理職を除く55歳以上のシニア世代がいきいきと働く企業として、3位に選出されました。

### (3) 指標及び目標

当社グループは、上記の「(2)①人材の多様性の確保を含む人材の育成に関する方針」及び「(2)②社内環境整備に
関する方針」について、次の指標を用いています。当該指標に関する目標及び実績は、次の通りです。

| 指標 | 2023年度実績 | 2024年度実績 | 目標 |
|:--|:--|:--|:--:|
| 管理職に占める女性労働者の割合 | 15.5% | 10.5% | 20%(2025年度) |
| 男性労働者の育児休業取得率 | 38.5% | 100.0% | 100%(2025年度) |
""",
            """| 労働者の男女の賃金の差異 | 76.3% | 78.4% | 80%(2025年度) |
| 有給休暇取得率 | 74.9% | 69.8% | 80%(2025年度) |
| 離職率 | 6.4% | 7.7% | 5%以下(毎年) |
| 月平均所定外残業時間 | 14.3時間 | 14.1時間 | − |
| 障がい者雇用率 | 2.96% | 2.48% | − |

(注) 1.国内グループ会社(当社、サンプルテクノロジー株式会社)を対象に計算しています。

2.当社グループでは定年制を廃止しているため、離職率については、すべての退職者を含めて
計算しています。

3.目標の「−」は、設定がないことを示しています。

## 3 【事業等のリスク】
"""

このようにヘッダーとデータ行が分断されると、数値だけがヒットしても
「それが何の数値か(2023年度の実績なのか、それとも、2024年度の実績なのか)」を正しく理解できず、
回答精度の低下に直結します。

ケース2. 階層が深いブロックの検索

クエリ: “当連結会計年度のアプリケーション事業の販売実績”

GET /ir_report_2024_chunk_recursive/_search
{
  "_source": false,
  "query": {
    "semantic": {
      "field": "content.text_embedding",
      "query": "当連結会計年度のアプリケーション事業の販売実績"
    }
  },
  "highlight": {
    "fields": {
      "content.text_embedding": {
        "order": "score",
        "number_of_fragments": 1
      }
    }
  }
}

検索結果

"""
##### (d) 販売実績

当連結会計年度の販売実績をセグメントごとに示すと、次のとおりであります。

| セグメントの名称 | 当連結会計年度(自2024年1月1日至2024年12月31日) | 前年同期比(%) |
|:--|:--|:--:|
| オープンシステム基盤事業(千円) | 14,573,839 | 147.1 |
| アプリケーション事業(千円) | 5,986,143 | 100.3 |
| 合計(千円) | 20,559,983 | 129.5 |

(注)1.セグメント間の内部売上高又は振替高を除いた外部顧客に対する売上高を記載しております。

2.最近2連結会計年度の主要な販売先及び当該販売実績の総販売実績に対する割合は次のとおりであります。

| 販売先 | 前連結会計年度(自2023年1月1日至2023年12月31日) |  | 当連結会計年度(自2024年1月1日至2024年12月31日) |  |
|:--|:--|:--|:--|:--:|
|  | 金額(千円) | 割合(%) | 金額(千円) | 割合(%) |
| 株式会社*** | 4,229,893 | 26.6 | 6,067,031 | 29.5 |
| 株式会社*** | 2,026,750 | 12.8 | 2,599,494 | 12.6 |

"""

ピンポイントで検索したいブロック(見出し (d) 販売実績 配下)のみがヒットしています。

比較:Word Chunking の場合

一方で、word 戦略を用いた場合、単純な単語数で区切るため、目的のブロックの前に直前のセクション
の残骸が含まれたり、取得したい情報が途中で切れたりする現象が発生しました。

Word Chunking での検索結果の抜粋:

"""(前年同期は157百万円の使用)となりました。

#### ③ 生産、受注及び販売の状況

##### (a) 生産実績

当連結会計年度の生産実績をセグメントごとに示すと、次のとおりであります。

| セグメントの名称 | 当連結会計年度(自2024年1月1日至2024年12月31日) | 前年同期比(%) |
|:--|:--|:--:|
| オープンシステム基盤事業(千円) | 894,022 | 128.9 |
| アプリケーション事業(千円) | 1,979,893 | 88.3 |
| 合計(千円) | 2,873,915 | 97.9 |

##### (b) 仕入実績

当連結会計年度の仕入実績をセグメントごとに示すと、次のとおりであります。

| セグメントの名称 | 当連結会計年度(自2024年1月1日至2024年12月31日) | 前年同期比(%) |
|:--|:--|:--:|
| オープンシステム基盤事業(千円) | 11,053,511 | 165.7 |
| アプリケーション事業(千円) | 1,303,180 | 123.3 |
| 合計(千円) | 12,356,691 | 159.9 |

##### (c) 受注実績

当連結会計年度の受注実績をセグメントごとに示すと、次のとおりであります。

| セグメントの名称 | 受注高(千円) | 前年同期比(%) | 受注残高(千円) | 前年同期比(%) |
|:--|:--|:--|:--|:--:|
| オープンシステム基盤事業 | 14,871,485 | 141.6 | 2,742,583 | 112.2 |
| アプリケーション事業 | 6,400,717 | 115.5 | 2,477,333 | 120.1 |
| 合計 | 21,272,202 | 132.6 | 5,219,917 | 115.8 |

##### (d) 販売実績

当連結会計年度の販売実績をセグメントごとに示すと、次のとおりであります。

| セグメントの名称 | 当連結会計年度(自2024年1月1日至2024年12月31日) | 前年同期比(%) |
|:--|:--|:--:|
| オープンシステム基盤事業(千円) | 14,573,839 | 147.1 |
| アプリケーション事業(千円) | 5,986,143 | 100.3 |
| 合計(千円) | 20,559,983 | 129.5 |

(注)1.セグメント間の内部売上高又は振替高を除いた外部顧客に対する売上高を記載しております。

2.最近2連結会計年度の主要な販売先及び当該
"""

採用時の重要な注意点

Elasticsearch の chunking_settings (Inference API) を使用してチャンキングを行った場合、
データ構造は以下のようになります。

  • content (text 型)
  • content.text_embedding (semantic_text 型)
  • 複数の密ベクトル (分割されたチャンクの数だけ生成される)

この構造下では、「チャンク単位でのキーワード検索 + ベクトル検索(ハイブリッド検索)」を行うことが困難です。
標準のキーワード検索(Match Queryなど)を行うと、ドキュメント全体(content フィールド)に対してマッチングが行われるため、ベクトル検索でヒットしたチャンクとは無関係の箇所にあるキーワードにも反応してしまいます。

もし、「チャンクごとに、厳密なキーワード検索とベクトル検索を組み合わせたい」 という要件がある場合は、Elasticsearch 側での自動チャンキング機能(Inference API の chunking)は使用せず、以下のアプローチを検討してください。

  1. LangChain などの外部ライブラリを使用して、アプリケーション側で事前に Markdown を分割する。
  2. 分割した各チャンクを、それぞれ独立した Elasticsearch ドキュメントとしてインデックスする。

こうすることで、1つのドキュメントに対して「1つのテキスト」と「1つのベクトル」が対応するシンプルな構造となり、チャンク単位でのハイブリッド検索が容易になります。

まとめ

検証の結果、Markdown 文書に対して Recursive Chunking(再帰チャンキング) を適用することで、以下のメリットが確認できました。

  • 文脈の保持: 見出しや表などの論理的なまとまりが維持される。
  • ノイズの削減: 無関係なセクションの混入を防ぎ、検索精度の向上に寄与する。
  • RAGへの最適化: LLMに渡すコンテキストとして、より意味の通った情報を提供できる。

Markdown形式のドキュメント(技術仕様書、社内Wiki、レポートなど)を検索対象とする場合、
Elasticsearchの再帰チャンキング(Recursive Chunking)は非常に強力な選択肢となります。ぜひ一度お試しください。