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

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

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

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

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

## 記載していること
- CharacterTextSplitterの基本的な説明
- いくつかの設定パターンで動かしてみた結果
- できなかったこと

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

### パラメータの説明
- **chunk_size**: チャンクの最大サイズを指定します。このサイズに基づいてテキストが分割されます。
- **separator**: テキストを分割するためのセパレータを指定します。通常、句点や改行などが使われます。
- **is_separator_regex**: セパレータが正規表現かどうかを指定します。Trueにすると、指定したセパレータが正規表現として解釈されます。
- **chunk_overlap**: チャンク間のオーバーラップを指定します。これにより、前のチャンクと次のチャンクの一部が重複するようになります。


![‎RAGの基本的な仕組み.‎002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3833753/e11cac04-6815-66b2-9d6c-eb8dd49e52ec.jpeg)


### 実装例
以下は、CharacterTextSplitterの基本的な使い方の例です。

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

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

chunk_size = 10
chunk_overlap = 2

char_text_splitter = CharacterTextSplitter(
    chunk_size=chunk_size,
    chunk_overlap=chunk_overlap,
    separator='。'
)

documents = char_text_splitter.split_text(source_text)
```

## 動かしてみる

### ドキュメントの準備
このようなテキストファイルを用意します。
```sample.txt
あいうえお。
かきくけこ。さしすせそ。
たちつてと。なにぬねの。はひふへほ。
まみむめも。やゆよ。らりるれろわをん。
```

### ①チャンクサイズのみを指定してみる
```python
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import CharacterTextSplitter

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

# CharacterTextSplitterを設定
char_text_splitter = CharacterTextSplitter(
    chunk_size=5,
    chunk_overlap=0
)

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

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

exit(0)
```

CharacterTextSplitterによる分割結果: 
**あれ？思ったように分割されていない・・・** 
```
Chunk 1 (Length: 58):
あいうえお。
かきくけこ。さしすせそ。
たちつてと。なにぬねの。はひふへほ。
まみむめも。やゆよ。らりるれろわをん。
```

### ②チャンクサイズとセパレータを設定してみる

セパレータに空白文字('')を設定することで、chunk_sizeで分割されました。

```python
# CharacterTextSplitterを設定
char_text_splitter = CharacterTextSplitter(
    chunk_size=5,
    chunk_overlap=0,
    separator=''
)
```

CharacterTextSplitterによる分割結果: 
**改行は１文字とカウントされています。** 
```
Chunk 1 (Length: 5):
あいうえお

Chunk 2 (Length: 5):
。
かきく

Chunk 3 (Length: 5):
けこ。さし

Chunk 4 (Length: 4):
すせそ。

Chunk 5 (Length: 5):
たちつてと

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

Chunk 7 (Length: 5):
の。はひふ

Chunk 8 (Length: 5):
へほ。
ま

Chunk 9 (Length: 5):
みむめも。

Chunk 10 (Length: 5):
やゆよ。ら

Chunk 11 (Length: 5):
りるれろわ

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

### ③オーバーラップを設定してみる

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

```python
# CharacterTextSplitterを設定
char_text_splitter = CharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=2,
    separator=''
)
```

CharacterTextSplitterによる分割結果: 
**2文字オーバラップしているのが分かります。** 
```
Chunk 1 (Length: 10):
あいうえお。
かきく

Chunk 2 (Length: 10):
きくけこ。さしすせそ

Chunk 3 (Length: 10):
せそ。
たちつてと。

Chunk 4 (Length: 10):
と。なにぬねの。はひ

Chunk 5 (Length: 10):
はひふへほ。
まみむ

Chunk 6 (Length: 10):
みむめも。やゆよ。ら

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


### ④セパレータを正規表現にしてみる

セパレータに句点(。)と改行(\n)を設定します。
また、分かりやすくするために、テキストファイルの内容を変更します。
```sample.txt
あいうえお。かきくけこさしすせそたちつてと。
なにぬねの。はひふへほ、まみむめも、やゆよ。らりるれろわをん。
```
```python
# CharacterTextSplitterを設定
char_text_splitter = CharacterTextSplitter(
    chunk_size=10,
    chunk_overlap=2,
    is_separator_regex=True,
    separator='[。|\\n]'
)
```

CharacterTextSplitterによる分割結果: 
**2つのセパレータで分割していることが分かります。** 
**ですが、チャンクサイズが10を超えています、また、オーバーラップしていません。** 

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

Chunk 2 (Length: 15):
かきくけこさしすせそたちつてと

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

Chunk 4 (Length: 15):
はひふへほ、まみむめも、やゆよ

Chunk 5 (Length: 8):
らりるれろわをん
```

## 仕様を調べてみる（ChatGPT）

チャンクサイズ、オーバーラップ、セパレータ の優先度は、テキストの分割において以下のように処理されるとのこと。 

###	1.	セパレータによる分割
- 最初に、指定されたセパレータ（文字や正規表現）でテキスト全体を分割します。
- ここでのセパレータは、テキストをどこで区切るかを決定するもので、チャンクサイズに関係なく先に処理されます。
- 例えば、セパレータとして句点（。）や改行（\n）を指定した場合、それを基にテキストを区切ってリストにします。
###	2.	チャンクサイズの調整
- セパレータで分割した後、各チャンクが設定された chunk_size（最大チャンクサイズ）を超える場合、それを更に細かく分割します。
- 逆に、chunk_size よりも小さなチャンクであれば、そのまま次の処理へと進みます。
###	3.	オーバーラップの適用
- 最終的に、チャンク間で重複させる文字数を chunk_overlap で指定します。
- 例えば、前のチャンクの最後の 2 文字が次のチャンクの最初に重なるように設定する場合、chunk_overlap=2 とします。

## できなかったこと（考察）
**④セパレータを正規表現にしてみる**
では、想定していた動作の確認を取ることができませんでした。

**②チャンクサイズとセパレータを設定してみる**
では、セパレータに空白文字('')を設定することで、chunk_sizeで分割されるようになりました。

同じような設定をすればいいのかもしれませんが、正規表現で空白文字('')を設定ができないことから、この設定は難しいと考えています。

## まとめ
RAG（Retrieval-Augmented Generation）においては、文脈や意味の単位でテキストを分割することが、精度向上につながります。そのため、特定の文字数で区切る方法は、利用シーンが少ないのかもしれません。

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