LangChainのText Splittersでドキュメントを分割しよう（RecursiveCharacterTextSplitter）

## はじめに

RAG（Retrieval-Augmented Generation）は、情報を効率的に取得し、それを基に応答を生成する手法です。このプロセスにおいて、大きなドキュメントを適切に分割し、関連する情報を迅速に取り出すことが非常に重要です。特に、テキストの分割方法は、検索効率や回答精度の向上に寄与するため、適切なツールの選択がカギとなります。
ここでは、LangChainライブラリに用意されているテキストを簡単に分割するための「Text Splitters」の中から、RecursiveCharacterTextSplitterの基本的な使い方とその実際の動作について詳しく見ていきます。

こちらも参考にして下さい

https://qiita.com/Kahiro/items/3bb75812e5767f12b005

https://qiita.com/Kahiro/items/da45f689de828366f9fb

## 筆者のレベル

- RAGについて基本的な理解を持っている
- RAGを実装することで、さらに深く学びたいと考えている
- プログラミング経験はあまりないが、RAGを実装してみたい
- 実際にコードを作成して動かすことで、理解を深めていきたい

## 記載していること

- RecursiveCharacterTextSplitterの基本的な説明
- いくつかの設定パターンで動かしてみた結果

## RecursiveCharacterTextSplitterの特徴

RecursiveCharacterTextSplitterは、再帰的に文字単位でテキストを分割する機能を持つText Splitterです。このツールを使用することで、文字列処理において、より効果的に情報を扱うことができます。以下に、RecursiveCharacterTextSplitterの主要なパラメータを説明します。

## パラメータの説明

- **chunk_size**: チャンクの最大サイズを指定します。このサイズに基づいてテキストが分割されます。
- **chunk_overlap**: チャンク間のオーバーラップを指定します。これにより、前のチャンクと次のチャンクの一部が重複するようになります。
- **separators**: 複数のセパレータを指定して、順に分割していきます。句点や改行などを含むリストで設定されます。
- **keep_separator**:テキストを分割した際にセパレータをどのように扱うかを決定します。以下のオプションがあります。

    **`True` または `"end"`**
   - **説明**: 各チャンクの最後にセパレータを保持します。

    **`"start"`**
   - **説明**: 各チャンクの最初にセパレータを保持します。

    **`False`**
   - **説明**: セパレータを削除します。

## 実装例

以下は、RecursiveCharacterTextSplitterの基本的な使い方の例です。

``` python
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

source_text = "あいうえお、かきくけこさしすせそ。"

chunk_size = 10
chunk_overlap = 2

recursive_char_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap,
    separators=['。']
)

documents = recursive_char_text_splitter.split_text(source_text)
```

## 動かしてみる

### ドキュメントの準備

このようなテキストファイルを用意します。

``` sample.txt
あいうえお。
かきくけこ。さしすせそ。
たちつてと。なにぬねの。はひふへほ。
まみむめも。やゆよ。らりるれろわをん。
```

### ①chunk_sizeとseparatorsを設定してみる

``` python
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# テキストをロード
source_document = TextLoader('sample.txt').load()
source_text = source_document[0].page_content

# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=5,
    chunk_overlap=0,
    separators=['。']
)

# 分割を実行
char_documents = recursive_char_text_splitter.split_text(source_text)

# 分割結果を表示
print('\nRecursiveCharacterTextSplitterによる分割結果: \n')
for i, chunk in enumerate(char_documents):
    print(f"Chunk {i+1} (Length: {len(chunk)}):\n{chunk}\n")
```

RecursiveCharacterTextSplitterによる分割結果:
セパレータは、チャンクの最初に保持されています。

```
Chunk 1 (Length: 5):
あいうえお

Chunk 2 (Length: 7):
。
かきくけこ

Chunk 3 (Length: 6):
。さしすせそ

Chunk 4 (Length: 7):
。
たちつてと

Chunk 5 (Length: 6):
。なにぬねの

Chunk 6 (Length: 6):
。はひふへほ

Chunk 7 (Length: 7):
。
まみむめも

Chunk 8 (Length: 4):
。やゆよ

Chunk 9 (Length: 9):
。らりるれろわをん

Chunk 10 (Length: 1):
。
```

### ②keep_separatorを設定してみる

``` python
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=5,
    chunk_overlap=0,
    separators=['。'],
    keep_separator="end"
)
```

RecursiveCharacterTextSplitterによる分割結果:
セパレータはチャンクの最後に保持されています。

```
Chunk 1 (Length: 6):
あいうえお。

Chunk 2 (Length: 7):

かきくけこ。

Chunk 3 (Length: 6):
さしすせそ。

Chunk 4 (Length: 7):

たちつてと。

Chunk 5 (Length: 6):
なにぬねの。

Chunk 6 (Length: 6):
はひふへほ。

Chunk 7 (Length: 7):

まみむめも。

Chunk 8 (Length: 4):
やゆよ。

Chunk 9 (Length: 9):
らりるれろわをん。
```

### ③chunk_overlapを設定してみる

chunk_sizeは10、chunk_overlapは2を設定してみます。

``` python
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=2,
    separators=['。'],
    keep_separator="end"
)
```

RecursiveCharacterTextSplitterによる分割結果:
オーバラップされていない・・・

```
Chunk 1 (Length: 6):
あいうえお。

Chunk 2 (Length: 6):
かきくけこ。

Chunk 3 (Length: 6):
さしすせそ。

Chunk 4 (Length: 6):
たちつてと。

Chunk 5 (Length: 6):
なにぬねの。

Chunk 6 (Length: 6):
はひふへほ。

Chunk 7 (Length: 6):
まみむめも。

Chunk 8 (Length: 4):
やゆよ。

Chunk 9 (Length: 9):
らりるれろわをん。
```

### ④chunk_sizeを変更してみる

セパレータをカウントしない長さ(chunk_size=5)に、チャンクサイズを設定します。

``` python
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=5,
    chunk_overlap=2,
    separators=['。'],
    keep_separator="end"
)
```

RecursiveCharacterTextSplitterによる分割結果:
chunk_size=5よりも大きなサイズで分割されてしまいます。

```
Chunk 1 (Length: 6):
あいうえお。

Chunk 2 (Length: 7):

かきくけこ。

Chunk 3 (Length: 6):
さしすせそ。

Chunk 4 (Length: 7):

たちつてと。

Chunk 5 (Length: 6):
なにぬねの。

Chunk 6 (Length: 6):
はひふへほ。

Chunk 7 (Length: 7):

まみむめも。

Chunk 8 (Length: 4):
やゆよ。

Chunk 9 (Length: 9):
らりるれろわをん。
```

### ⑤separatorsに空白文字('')を設定してみる

CharacterTextSplitterにおいて、セパレータに空白文字('')を設定しないと、適切にカウントされていないことを確認していたので、それを参考に空白文字('')を設定してみました。

``` python
# RecursiveCharacterTextSplitterを設定
recursive_char_text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=5,
    chunk_overlap=2,
    separators=['。',''],
    keep_separator="end"
)
```
RecursiveCharacterTextSplitterによる分割結果:
一見、chunk_size、chunk_overlapともに、思った通りに分割されているように見えますが、オーバーラップしていないチャンクもあります。

```
Chunk 1 (Length: 5):
あいうえお

Chunk 2 (Length: 3):
えお。

Chunk 3 (Length: 4):
かきくけ

Chunk 4 (Length: 4):
くけこ。

Chunk 5 (Length: 5):
さしすせそ

Chunk 6 (Length: 3):
せそ。

Chunk 7 (Length: 4):
たちつて

Chunk 8 (Length: 4):
つてと。

Chunk 9 (Length: 5):
なにぬねの

Chunk 10 (Length: 3):
ねの。

Chunk 11 (Length: 5):
はひふへほ

Chunk 12 (Length: 3):
へほ。

Chunk 13 (Length: 4):
まみむめ

Chunk 14 (Length: 4):
むめも。

Chunk 15 (Length: 4):
やゆよ。

Chunk 16 (Length: 5):
らりるれろ

Chunk 17 (Length: 5):
れろわをん

Chunk 18 (Length: 3):
をん。
```

## 考察
チャンクサイズに分割する際に、指定した分割サイズまでにセパレータが存在しなければ、オーバーラップが機能するように見えます。
(必ずオーバーラップして欲しいのだが・・・)

## まとめ

RAG（Retrieval-Augmented Generation）においては、文脈や意味の単位でテキストを分割することが、精度向上につながります。RecursiveCharacterTextSplitterを用いることで、指定したセパレータやチャンクサイズに基づいて、再帰的にテキストを効率よく分割できます。
ですが、思った通りに分割するには、もう少し工夫がいるのかもしれません。

## 参考文献
- [LangChain APIリファレンス](https://python.langchain.com/api_reference/index.html)