3
7

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

Last updated at Posted at 2023-10-21

0. はじめに

みなさん、こんにちは。これまでの記事で

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

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

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

1. 前回のおさらい

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に追加します。

    # 全てのデータを結合して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]になっています。

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

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

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)]

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

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

3. チャンクの確認

それでは実際に、文書を出力してみます。

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

今、二つのチャンクには、メタデータとしてファイル名が入っています。それぞれドラゴンボールとドラゴンボール超のあらすじの一部ですが、ドラゴンボールを知らない人には、どちらがドラゴンボール超なのかわからないと思います。しかし、メタデータのおかげで、それぞれがどちらのあらすじの一部なのかがわかるようになるわけです。これは文書検索を行うLLMにとっても同じです。

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

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

これをテキストに焼き直して出力します。

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. まとめ

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

参考文献

3
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
7