社内データを使ったChatGPT利用のための準備:SharePointのファイルをベクトルデータベースに保存する方法

はじめに

ISID AITC所属の後藤です。 本記事では、タイトルの通りSharePoint上のデータをChatGPT(Azure OpenAI Service)で利用できるようにベクトルとしてストレージに保存するシステムを紹介します。

背景

昨今ChatGPTの活用がビジネスの現場で注目されています。その際、課題となるのがChatGPT(学習済みモデル)が持っていない知識をいかに与えるかです。一般的な機械学習のアプローチではファインチューニングを行う場合が多いです。しかし、ChatGPTに関してはファインチューニングではなく、モデルが持っていない知識を検索によって獲得し、検索結果をプロンプトに組み込み、モデルへの入力として与える方法が一般的です。

検索の方法は色々ありますが、よく使われているのは入力と検索対象データをベクトル表現に変換し、それらの類似度をスコアとして検索を行うベクトル検索です。 今回はSharePointにデータがアップロードされるとそれらを自動的にベクトルデータベースに保存し、検索可能にするシステムを紹介します。

社内データをベクトルとしてデータベースに保存できてしまえば後はLangchainなどのライブラリを使用することで簡単に社内データに関する内容を答えてくれる、ChatGPTによる問い合わせシステムを実現できます。

システム全体像

以下、システムの全体像です。作成が必要なAzureリソースはAzure OpenAI Service、Storage Account、Azure Functions、Azure Container Appsです(※ 作成過程で必要なリソースは除く)。

Azure OpenAI serviceを使ったベクトル検索システム

ユーザーがSharePointの特定のフォルダにファイルをアップロードすると、自動的にファイルの中身のチャンク化(一定の長さに区切られた文章の塊にする)とベクトル化をおこない、ベクトルデータベースであるQdrantに保存されます。

ベクトルデータベースとしてQdrantを使用したのはLangchainに対応済みであることと、dockerを使用したデプロイが容易だったためです。FaissやMilvusなどLangchainが対応している他のベクトルデータベースでも代用可能です。

また、ベクトルデータベースをホストしているAzure Container Appsですが、こちらもAzure Container Instanceなどで代用可能です。Container Appsはデプロイも容易であり、運用を考慮した各種設定も簡単にできることから今回採用しました。

構成方法

以降ではシステム全体像で説明した流れの実現方法を紹介します。

SharePointからBlobへデータを移行する方法

SharePointにデータがアップロードされたタイミングでデータをBlobへ移す処理はPower Automateを利用することで簡単に実現できます。

Power Automateではよくあるユースケースについてはテンプレートが用意されており、今回は「SharePoint フォルダーから AzureBlob フォルダーにファイルをコピーする」というテンプレートを使います。

フローの設定ではアップロード元となるSharePointのリンクとフォルダ情報、コピー先のAzure Blob Storageの接続文字列とコンテナ名が必要となります。Blobの接続情報以外はプルダウンから選択可能で、それ以外はデフォルト設定で大丈夫です。デフォルトでは、Blob名がファイル名となりBlobのコンテンツはファイルの中身となります。

Blobの入力トリガーを利用したAzure Functionsの実行

ここまででSharePointからBlobへのデータ移行は出来ました。続いてはBlobのデータを読み取りベクトルに変換し、ベクトルデータベースに保存する処理についてです。 Azure FunctionsではBlob入力時に関数が起動するようなユースケースがサポートされており、公式ドキュメントの実装を参考に作成可能です。 Azure Functions Core Toolsを使用してローカルにAzure Functionsのプロジェクトを作成する際にBlobの入力トリガーによるユースケースを選択します。

今回使用したAzure Functionsのプロジェクトのディレクトリ構成とコードの中身を紹介します。

.
├── chunk
│   ├── __init__.py
│   ├── function.json
│   └── readme.md
├── host.json
├── local.settings.json
└── requirements.txt

こちらがfunction.jsonの中身です。pathとconnectionについては書き換える必要がありますが、そのほかは自動で生成してくれます。これでBlob入力時に関数が起動します。

{
  "scriptFile": "__init__.py",
  "bindings": [
    {
      "name": "myblob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "test/{name}", //testはコンテナ名
      "connection": "storage_test" //任意の値。ただしlocal.settings.jsonと一致させる
    }
  ]
}

では処理部分である__init__.pyの中身を紹介します。以下が全体のコードです。

import logging
import os
import tempfile

import azure.functions as func
import openai
from azure.storage.blob import BlobServiceClient
from langchain.document_loaders import TextLoader
from langchain.embeddings import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.vectorstores import Qdrant
from qdrant_client import QdrantClient
from qdrant_client.http import models

CONTAINER_NAME = "test"
COLLECTION_NAME = os.getenv("COLLECTION_NAME", "test_openai")
QDRANT_URL = os.getenv("QDRANT_URL", "")

# Configure OpenAI API
openai.api_type = "azure"
openai.api_version = "2022-12-01"
openai.api_base = os.getenv("OPENAI_API_BASE", "")
openai.api_key = os.getenv("OPENAI_API_KEY")

logger = logging.getLogger("azure.core.pipeline.policies.http_logging_policy")
logger.setLevel(logging.WARNING)


def connect_to_vectorstore():
    client = QdrantClient(url=QDRANT_URL, port=443, https=True)
    try:
        client.get_collection(COLLECTION_NAME)
    except Exception as e:
        logging.info("create new collection")
        client.recreate_collection(
            collection_name=COLLECTION_NAME,
            vectors_config=models.VectorParams(
                size=1536, distance=models.Distance.COSINE
            ),
        )
    return client


def main(myblob: func.InputStream):
    logging.info(
        f"Python blob trigger function processed blob \n"
        f"Name: {myblob.name}\n"
        f"Blob Size: {myblob.length} bytes"
    )

    connect_str = os.environ["storage_test"]
    blob_service_client = BlobServiceClient.from_connection_string(
        connect_str, logging_enable=False
    )

    blob_name = os.path.basename(myblob.name)
    blob_client = blob_service_client.get_blob_client(
        container=CONTAINER_NAME, blob=blob_name
    )

    with tempfile.TemporaryDirectory() as dname:
        temp_blob_file_name = os.path.join(dname, "temp.txt")
        # 入力ファイル内容の読み取り
        with open(temp_blob_file_name, "wb") as f:
            contents = blob_client.download_blob().readall()
            f.write(contents)

        if myblob.name is not None:
            loader = TextLoader(temp_blob_file_name, encoding="utf8")
        else:
            raise Exception("Blob is None")

        logging.info("Loading documents...")
        documents = loader.load()

        logging.info(f"Loading done. Number of documents: {str(len(documents))}")

        # チャンク分割設定
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=100,
            chunk_overlap=0,
        )

        logging.info("Create chunk...")
        texts = text_splitter.split_documents(documents)
        logging.info(f"Chunk done. Number of chunked documents: {str(len(texts))}")

        logging.info("Connect vector store...")
        client = connect_to_vectorstore()
        logging.info("Success connect")

        embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")
        qdrant_client = Qdrant(
            client=client,
            collection_name=COLLECTION_NAME,
            embeddings=embeddings.embed_query,
        )

        logging.info(f"Adding embedding vector to {COLLECTION_NAME}...")
        qdrant_client.add_documents(texts)

        logging.info("Finish!!")

中身の解説をすると、まず関数の引数からアップロードされたBlobの情報を取得し、そこからBlobのSDKを使用してファイルデータを関数内で扱えるようにダウンロードします。続いて、得られたファイルデータからLangchainを使用し、チャンクに分割します。 そこからAzure OpenAI Serviceが提供する埋め込み用のモデルであるadaを使用し、チャンク化された文章をベクトル化します。最後にContainer Appsで起動しているQdrantに接続し、作成したベクトルデータをコレクションに追加して処理は完了です。

結果確認

本記事では紹介しませんが、保存済みの文章のベクトルデータがあれば先ほど登場したLangchainを使用することで、ChatGPTが保存したドキュメントに関する質問に答えられるようになります。 試しに今回のシステムを利用し、wikipediaの情報を利用して作成した以下のtxtファイルを保存して「2023年のWBCのMVPは?」という質問に答えてもらいました。ちなみにChatGPT(GPT 3.5)では「2021年までの情報しか持っていないため答えられません」と返ってきます。

大谷 翔平(おおたに しょうへい、1994年7月5日 - )は岩手県水沢市(現・奥州市)出身のプロ野球選手(投手・外野手)。右投左打。MLBのロサンゼルス・エンゼルス所属。投手としても打者としても活躍する「二刀流(英: two-way player)」の選手。
2012年のNPBドラフト1位で北海道日本ハムファイターズから指名され、2013年の入団以降、投手と打者を両立する「二刀流」の選手として試合に出場。2014年には11勝、10本塁打で日本プロ野球(NPB)史上初となる「2桁勝利・2桁本塁打」を達成。2016年には、NPB史上初となる投手と指名打者の両部門でベストナインのダブル受賞に加え、リーグMVPに選出された。投手としての球速165 km/hは日本人最速記録である。
2017年オフ、ポスティングシステムでメジャーリーグベースボール(MLB)のロサンゼルス・エンゼルスに移籍。
2018年シーズンから投打にわたり活動し、日本人史上4人目の新人王を受賞。2021年シーズンでは、2001年のイチロー以来となる日本人史上2人目(アジア人史上でも2人目)のシーズンMVPとシルバースラッガー賞を受賞している。
2021年9月、タイム誌による「世界で最も影響力のある100人」 に、「アイコン(象徴)」のカテゴリーでヘンリー王子&メーガン妃、女優のブリトニー・スピアーズらと共に選出された。2021年12月、スポーティングニュース発表の「スポーツ史上最高のシーズンTOP50」では、エンゼルス大谷翔平の2021年シーズンを1位に選定。2021年12月、AP通信の年間最優秀男性アスリート賞を受賞した。
2022年8月9日、メジャーリーグではベーブ・ルース以来約104年ぶりとなる、二桁勝利・二桁本塁打を達成。
2022年10月5日、近代メジャーリーグベースボール(MLB)で投手打者の両方で規定回に達した初めての選手となった。
2023年のワールド・ベースボール・クラシック (WBC)では、日本代表のエース兼打者として活躍し、WBC史上初の2部門(投手部門・指名打者部門)でのオールWBCチームに選ばれた上にMVPを受賞している。

こちらが得られた回答です。

大谷翔平選手が2023年のWBCでMVPを受賞しました。彼は日本代表のエース兼打者として活躍し、投手部門と指名打者部門の両方でオールWBCチームに選ばれました。

ChatGPTを純粋に利用した場合では答えられなかった質問に答えられていることがわかります。このように今回のシステムを応用することでモデルがまだ知らない情報に関しても回答してくれるようにできます。

まとめ

本記事では、Azure OpenAIサービスのChatGPTにまだ知らない情報を答えさせるための準備として、SharePointのデータをベクトルデータベースに保存する仕組みをAzureで実現する方法をご紹介しました。今回はベクトル検索を用いた場合のユースケースを前提とした話でしたが、キーワード検索であればAzureのCognitive Searchを組み合わせた構築も可能です。

ここまでお読みいただきありがとうございました!

執筆
AITC AI製品開発グループ
後藤 勇輝