LangChain: チャンクにmetadataを追加する

# 0. はじめに
みなさん、こんにちは。これまでの記事で

1. テキストファイルをベクトル化し、ベクトルストアへ保存する方法

https://qiita.com/mashmoeiar11/items/cf4d6573e3aa50a725c0

2. 作成したベクトルストアを用いて専門情報を検索 (RetrievalQA) する方法

https://qiita.com/mashmoeiar11/items/214984400e3452615ea5

について簡単に紹介しました。前回はベクトルストアをつくる際に`page_content`のみを使いました。しかし、これだとチャンクの数が増えるにつれてベクトル検索精度が下がっていきます。今回は、ベクトルストア作成にメタデータ情報も追加して、ベクトル検索の精度を維持・向上する方法について紹介したいと思います。


# 1. 前回のおさらい

```python
from langchain.document_loaders import TextLoader, DirectoryLoader
from langchain.text_splitter import CharacterTextSplitter

    # Document Loaderテキストファイルをロード
    loader = DirectoryLoader(
        "./input/", 
        glob="**/*.txt", 
        loader_cls=TextLoader, 
        loader_kwargs={'autodetect_encoding': True}
    )
    data = loader.load()

    # Text Splitterの初期化
    text_splitter = CharacterTextSplitter(
        separator='\n\n',
        chunk_size=300,
        chunk_overlap=0,
        length_function=len
    )
```
まずは`./input/`に格納されたテキストファイルをロードします。次にText Splitterを初期化します。ここまでは前回と同じです。

# 2. メタデータの追加

さて今回は、`page_content`だけでなく`metadata`もdocumentに追加します。
```python
    # 全てのデータを結合してTextSplitterに入力
    texts = [doc.page_content for doc in data]   
    metadatas = [doc.metadata for doc in data]   
    documents = text_splitter.create_documents(texts, metadatas)
```

それでは実際に、`TextSplitter`クラスの`create_documents`をみてみます。メタデータもオプションではありますが、引数にとれることがわかります。型は`List[dict]`になっています。
```python
def create_documents(
        self, texts: List[str], metadatas: Optional[List[dict]] = None
    ) -> List[Document]:
        """Create documents from a list of texts."""
        _metadatas = metadatas or [{}] * len(texts)
        documents = []
        for i, text in enumerate(texts):
            index = -1
            for chunk in self.split_text(text):
                metadata = copy.deepcopy(_metadatas[i])
                if self._add_start_index:
                    index = text.find(chunk, index + 1)
                    metadata["start_index"] = index
                new_doc = Document(page_content=chunk, metadata=metadata)
                documents.append(new_doc)
        return documents
```

https://api.python.langchain.com/en/latest/_modules/langchain/text_splitter.html#TextSplitter

`metadata`の中身はというと、これは`TextLoader`クラスの`load`関数をみるとわかりますが、`metadata = {"source": self.file_path}`の形になっているのがわかります。

```python
def load(self) -> List[Document]:
        """Load from file path."""
        text = ""
        try:
            with open(self.file_path, encoding=self.encoding) as f:
                text = f.read()
        except UnicodeDecodeError as e:
            if self.autodetect_encoding:
                detected_encodings = detect_file_encodings(self.file_path)
                for encoding in detected_encodings:
                    logger.debug(f"Trying encoding: {encoding.encoding}")
                    try:
                        with open(self.file_path, encoding=encoding.encoding) as f:
                            text = f.read()
                        break
                    except UnicodeDecodeError:
                        continue
            else:
                raise RuntimeError(f"Error loading {self.file_path}") from e
        except Exception as e:
            raise RuntimeError(f"Error loading {self.file_path}") from e

        metadata = {"source": self.file_path}
        return [Document(page_content=text, metadata=metadata)]
```

https://api.python.langchain.com/en/latest/_modules/langchain/document_loaders/text.html#TextLoader

一方、`CSVLoader`では、`metadata = {"source": source, "row": i}`の形になっています。`source`はファイルパスで、`row`ごとに`page_content`が定義されています。`row`で情報が区切られていれば使いやすいです。ちなみlangchainのバージョンが0.0.312以降であれば、`CSVLoader`では以下のような感じで書けば、`column`ごとに、自由にメタデータを追加定義できるようになりました。とても便利です!

```python
csv_loader = CSVLoader(
    file_path="your_csv_file_path.csv",
    metadata_columns=["column_name1", "column_name2"]
)
documents = csv_loader.load()
```

https://api.python.langchain.com/en/latest/_modules/langchain/document_loaders/csv_loader.html#CSVLoader

# 3. チャンクの確認

それでは実際に、文書を出力してみます。
```text
....
Document(page_content='悟空はドラゴンボールによって生き返るまでの間、あの世の界王の下で修業し、
仲間と共に地球に強襲したサイヤ人の戦士・ナッパとベジータを迎え撃つ。悟空は修行により増した力でナッパ
を一蹴し、ベジータと決闘。仲間の協力もあり、何とか辛勝し撤退させるが、多くの仲間を失うとともに、
ピッコロの戦死により彼と一心同体であった神も死亡し、地球のドラゴンボールも消滅する。', 
metadata={'source': 'input\\dragonball_story.txt'}), 
....
Document(page_content='更なるパワーアップを遂げただけでなく、セブンスリーのコピー能力までも得たモロの前に
ベジータが敗れ、悟空や悟飯らも応戦するが全員が瀕死の状態にまで追いつめられる。そこに天界に帰ったはずの天使の
メルスが現れる。メルスは下界の出来事に深く干渉すると天使は消滅してしまうことを自覚した上でモロと戦う。その間
に悟空は回復のために駆け付けたデンデに回復してもらう。メルスは天使としての実力を使いモロを追い詰めると同時
に、コピー能力を担うモロの身体の水晶部位の破壊に成功する。しかしメルスの身体は次第に透け始め、最期に自身が愛
した銀河を守る意思を悟空に託し消滅する。', 
metadata={'source': 'input\\dragonball_super_story.txt'})
....
```
今、二つのチャンクには、メタデータとしてファイル名が入っています。それぞれドラゴンボールとドラゴンボール超のあらすじの一部ですが、ドラゴンボールを知らない人には、どちらがドラゴンボール超なのかわからないと思います。しかし、メタデータのおかげで、それぞれがどちらのあらすじの一部なのかがわかるようになるわけです。これは文書検索を行うLLMにとっても同じです。

メタデータを文字列に変換して、ヘッダーとして追加することもできます。

```python
title_map = {
    "input\\dragonball_story.txt": "###ドラゴンボール",
    "input\\dragonball_super_story.txt": "###ドラゴンボール超",
}

for document in documents:
    header = document.metadata.get('source', None)
    title_name = title_map.get(header, "Unknown Title")
    document.page_content = title_name + '\n' + document.page_content
```
これをテキストに焼き直して出力します。
```python
with open("./output/text_chunks.txt", "w", encoding="utf-8") as file:
    for text in documents:
        file.write(text.page_content)
         file.write('\n--------------------------------------\n')
```
```
....
--------------------------------------
###ドラゴンボール
悟空はドラゴンボールによって生き返るまでの間、あの世の界王の下で修業し、仲間と共に地球に強襲したサイヤ人の戦
士・ナッパとベジータを迎え撃つ。悟空は修行により増した力でナッパを一蹴し、ベジータと決闘。仲間の協力もあり
、何とか辛勝し撤退させるが、多くの仲間を失うとともに、ピッコロの戦死により彼と一心同体であった神も死亡し、地
球のドラゴンボールも消滅する。
-------------------------------------- 
....
--------------------------------------
###ドラゴンボール超
更なるパワーアップを遂げただけでなく、セブンスリーのコピー能力までも得たモロの前にベジータが敗れ、悟空や悟飯
らも応戦するが全員が瀕死の状態にまで追いつめられる。そこに天界に帰ったはずの天使のメルスが現れる。メルスは下
界の出来事に深く干渉すると天使は消滅してしまうことを自覚した上でモロと戦う。その間に悟空は回復のために駆け付
けたデンデに回復してもらう。メルスは天使としての実力を使いモロを追い詰めると同時に、コピー能力を担うモロの身
体の水晶部位の破壊に成功する。しかしメルスの身体は次第に透け始め、最期に自身が愛した銀河を守る意思を悟空に託
し消滅する。
--------------------------------------
....
```

# 4. まとめ

いかがでしたでしょうか。チャンクにメタデータを追加することで検索精度の維持向上が期待できます。データベースのスケーラビリティを担保するための工夫として是非メタデータを有効活用してください！


# 参考文献

https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%A9%E3%82%B4%E3%83%B3%E3%83%9C%E3%83%BC%E3%83%AB

https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%A9%E3%82%B4%E3%83%B3%E3%83%9C%E3%83%BC%E3%83%AB%E8%B6%85