LangChainを用いて大量ファイルをロードするVectorDBを作ってみた(8)

![sky.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/d5fea432-0541-945a-fef6-0b7c5ad8e7fc.jpeg)
# はじめに
前回は、XML形式のファイルから必要なエレメントの情報のみVectorDBに格納する方法を試してみました。

https://qiita.com/ogi_kimura/items/4bbf2c987856a9435280

大量ファイルをVectorDBに取り込んだ場合、データ量が多すぎて「ぼやけてしまう」せいか、生成AIから返ってくる回答精度が悪くなることもわかってきました。
今回は、前回作成した属性を追加したVectorDBを基に、生成AIから精度の高い回答をもらえるようにしてみます。

# VectorDBの内容
今回は、`filter`を使ってVectorDBのデータを絞り込んだうえで、生成AIに質問を投げる方法を考えたいと思います。それにより「ぼやける」ことが無くなるのではないかと考えました。

前回記事の内容から、Chromaのデータベース`embedding_metadata`というテーブルに以下の属性が追加されており、その中にデータが入っています。
「DB Browser for SQLite」を用いて実際にデータの中身を見ていきたいと思います。

| 属性名 | 内容 | 備考 |
|:-|:-|:-|
| name  | ファイルの名前  | 拡張子は含まれない|
| source  | ファイルパス  | ファイル名・拡張子が含まれる|
| tag  | タグ名  | 名前空間名を含む、前回の記事で属性追加|
| title  | 特許のタイトル  |前回の記事で属性追加 |
| entry_name  | 特許を申請した人・組織の名前  | 前回の記事で属性追加|
| patent_citation_text  | 特許請求番号  | 前回の記事で属性追加|
| chroma:document  | ドキュメント内容  | Chromaのデフォルト|

#### tag
以下の様に`embedding_metadata`のkeyカラムが`tag`のものについて、名前空間（{}でくくられている）とタグ名がセットで入っていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/b8c67c9b-fec8-f440-65ee-df95538a1f12.png)


#### title
以下の様に`embedding_metadata`のkeyカラムが`title`のものについて、特許名称が入っていることがわかります。
'title'の`string_value`値が設定されているレコード数が少ないため、クエリーを使って表示することにします。

```sql:
select * from embedding_metadata where key="title" and string_value <> ""
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/751f1182-dba7-e013-f079-52e917bc9c27.png)


#### entry_name
以下の様に`embedding_metadata`のkeyカラムが`entry_name`のものについて、特許を請求した人もしくは組織の名前が設定されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/866f0fe9-83d0-736c-8742-57b8cc53e38d.png)

#### patent_citation_text
以下の様に`embedding_metadata`のkeyカラムが`patent_citation_text`のものについて、特許を請求したときの番号が設定されていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/cac58e86-8fda-3f12-0702-992178fdabd7.png)

# 生成AI対話画面(chainlit)
次に`chainlit`の画面表示を修正します。
`chainlit`については私がほとんど理解しておらず、ネットのサンプルコードを色々調べることにしました。ただ、「特許番号テキストボックス」と「メッセージ欄」の２つを同時に設ける例がどこにもありませんでした。
今回は仕方がないので、「メッセージ欄」１つで２つの機能を持たせることにしました。具体的には、入力欄の10桁目まではファイル名として、それ以降を質問内容にすることとしました。
例としては

```text:生成AIに投げる質問の例
0007350061この特許の概要を説明して。
```
上記の例では、メッセージ文字列の10桁（0007350061）でXMLファイルを特定し、それ以降（この特許の概要を説明して。）で質問をするという具合になります。

では実際にソースコードを記述していきます。
少し余談なのですが、前回の記事でVectorDBにデータをロードするプログラムを作成しましたが、そこではembeddingモデルを`text-embedding-3-small`にしていました。ただ、回答精度について悪くなることが分ってきました。そのため`text-embedding-ada-002`に戻しています。

```python:chroma_chainlit.py
import chainlit as cl
from langchain.chat_models import ChatOpenAI
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import PromptTemplate
from langchain.schema import HumanMessage
from langchain.vectorstores import Chroma
from qdrant_client.http.models import Filter, FieldCondition, MatchValue

embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
chat = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)


prompt = PromptTemplate(template="""文章を元に質問に答えてください。 

文章: 
{document}

質問: {query}
""", input_variables=["document", "query"])

database = Chroma(
    persist_directory="C:\\Users\\ogiki\\vectorDB\\local_chroma", 
    embedding_function=embeddings
)

@cl.on_chat_start
async def on_chat_start():
    await cl.Message(content="準備ができました！メッセージを入力してください！").send()

@cl.on_message
async def on_message(input_message):
    print("入力されたメッセージ: " + input_message)
    name = input_message[:10]
    documents = database.similarity_search_with_score(input_message[10:], k=3, filter={"name":name})
    documents_string = ""
    for document in documents:
        print("---------------document.metadata---------------")
        print(document[0].metadata)
        print(document[1])
        documents_string += f"""
            ---------------------------
            {document[0].page_content}
            """
    print("---------------documents_string---------------")
    print(input_message[10:])
    print(documents_string)
    result = chat([
        HumanMessage(content=prompt.format(document=documents_string,
                                           query=input_message[10:]))
    ])
    await cl.Message(content=result.content).send()
```

ソースコードでは、メッセージの10桁までを`name`に格納します。
```python:
name = input_message[:10]
```
それからメッセージの10桁目を`name`として`filter`をかけるようにしました。
```python:
documents = database.similarity_search_with_score(input_message[10:], k=3, filter={"name":name})
```

とてもブサイクなのですが、`chainlit`をよく理解していないので、もう少しちゃんとしたプログラムにしていくつもりです。今回は堪忍です！

# 実行
早速`chainlit`を起動します。
```bash:
python -m chainlit run chroma_chainlit.py
```

画面が出てきたので、質問をしてみます。

「0007350061この特許の概要は？」
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784222/aa9418c5-9bba-7e74-e32b-5c048db15b7a.png)

回答が返ってきました。
内容は合っているようです。

では、ログで詳細を確認します。

```
---------------document.metadata---------------
{'name': '0007350061', 'source': 'C:\\Users\\ogiki\\langchain_book\\JPB_2023185\\DOCUMENT\\P_B1\\0007350001\\0007350061\\0007350061\\0007350061.xml'}
0.35284486413002014
---------------document.metadata---------------
{'name': '0007350061', 'source': 'C:\\Users\\ogiki\\langchain_book\\JPB_2023185\\DOCUMENT\\P_B1\\0007350001\\0007350061\\0007350061\\0007350061.xml'}
0.39237409830093384
---------------document.metadata---------------
{'name': '0007350061', 'source': 'C:\\Users\\ogiki\\langchain_book\\JPB_2023185\\DOCUMENT\\P_B1\\0007350001\\0007350061\\0007350061\\0007350061.xml'}
0.40330567955970764
```
それぞれのスコアは`0.35284486413002014` `0.39237409830093384` `0.40330567955970764`です。前回(約0.3)より少し上がっていることが確認できます。
また、`source`がすべて「0007350061.xml」からということで、より精度が上がっていることもわかりました。

# まとめ
上記の様に、VectorDBの属性を増やすことにより、生成AIに精度の高い質問ができる可能性が出てきました。VectorDBには`tag` `title` `entry_name`という属性も追加したことから、これらに対して条件を設定することでより精度の高い検索が出来そうです。
ただ、残念なのは私が`chainlit`を理解していない為、メッセージの記述方法に変な制約を与えることになっていましたが・・・