Databricks Vector SearchをLangChainと組み合わせて試す

# 導入

DatabricksのVector Searchが2024/5月にGAとなりました。

https://docs.databricks.com/en/release-notes/product/2024/may.html#databricks-vector-search-is-ga

その影響なのか、自分が利用している環境(AWS/東京リージョン)でも、Vector Searchエンドポイントが作れるようになっていました。いつの間に。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/08f83718-8087-0adc-f5b7-4a6733bad14b.png)

せっかくなので、以下の記事やLangChainのDocを読みつつ、LangChainと組み合わせた簡単なサンプルを実行してみます。

https://qiita.com/taka_yayoi/items/3bdffae4bd040d25ee60

https://python.langchain.com/v0.1/docs/integrations/vectorstores/databricks_vector_search/

検証環境はDatabricks on AWS、DBRは15.1MLを使用しました。

# Step1. パッケージインストール

必要なパッケージをインストールします。
重要なのは```databricks-vectorsearch```パッケージですね。

```python
%pip install -U databricks-sdk
%pip install --upgrade --force-reinstall databricks-vectorsearch
%pip install -U langchain langchain_community

dbutils.library.restartPython()
```

# Step2. サンプル文書を準備

ベクトル化する文書を準備するために、Wikipediaからテキストデータを取得・チャンク化し、LangChainの```Document```形式に変換します。

```python
import requests

def get_wikipedia_page(title: str):
    """
    Retrieve the full text content of a Wikipedia page.

    :param title: str - Title of the Wikipedia page.
    :return: str - Full text content of the page as raw string.
    """
    # Wikipedia API endpoint
    URL = "https://ja.wikipedia.org/w/api.php"

    # Parameters for the API request
    params = {
        "action": "query",
        "format": "json",
        "titles": title,
        "prop": "extracts",
        "explaintext": True,
    }

    # Custom User-Agent header to comply with Wikipedia's best practices
    headers = {"User-Agent": "tutorial/0.0.1"}

    response = requests.get(URL, params=params, headers=headers)
    data = response.json()

    # Extracting page content
    page = next(iter(data["query"]["pages"].values()))
    return page["extract"] if "extract" in page else None

full_document = get_wikipedia_page("葬送のフリーレン")
```

```python
from typing import Any
from langchain.text_splitter import RecursiveCharacterTextSplitter

class JapaneseCharacterTextSplitter(RecursiveCharacterTextSplitter):
    """句読点も句切り文字に含めるようにするためのシンプルなスプリッタ"""

    def __init__(self, **kwargs: Any):
        separators = ["\n\n", "\n", "。", "、", " ", "", "==="]
        super().__init__(separators=separators, **kwargs)

# split it into chunks
text_splitter = JapaneseCharacterTextSplitter(chunk_size=400, chunk_overlap=80)
docs = text_splitter.create_documents([full_document], metadatas=[{"source":"葬送のフリーレン"}])
# print(docs)
```

# Step3. Embedding用エンドポイントの準備

埋め込み計算に使うためのエンドポイントを準備します。
こちらは、以下の記事で作成したBGE M3モデルのサービングエンドポイントを利用します。

https://qiita.com/isanakamishiro2/items/66eaedf438fd62913804

```python
from langchain_community.embeddings import DatabricksEmbeddings

embedding_model_endpoint = "bge_m3_endpoint"

embeddings = DatabricksEmbeddings(endpoint=embedding_model_endpoint)
```


# Step4. Databricks Vector Searchの準備

Vector Searchを利用するためのベクトル検索エンドポイント、およびインデックスを作成します。

まず、クライアントインスタンスを作成。

```python
from databricks.vector_search.client import VectorSearchClient

vsc = VectorSearchClient()
```

ベクトル検索エンドポイントを作成。

```python
vector_search_endpoint_name = "test-vector-search-demo-endpoint"

vsc.create_endpoint(
    name=vector_search_endpoint_name,
    endpoint_type="STANDARD"
)

```

数分後に利用可能となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/91b4a898-d016-d8c3-8b04-6cd8b754fc76.png)

<br>

次にインデックス(ベクトル変換後のデータなどを格納したオブジェクト)を作成します。

Databricksのインデックは以下の2種類あります。([こちらのドキュメントより](https://docs.databricks.com/ja/generative-ai/create-query-vector-search.html#create-a-vector-search-index))

| 種類 | 詳細 |
|:-|:-|
| Delta同期インデックス|ソースDeltaテーブルと自動的に同期し、 Deltaテーブル内の基礎となるデータの変更に応じてインデックスを自動的に増分更新します。|
|ダイレクト・ベクトル・アクセス・インデックス| ベクターとメタデータの直接読み取りと書き込みをサポートします。 ユーザーは、REST API または Python SDK を使用してこのテーブルを更新する必要があります。 この種類のインデックスは、UI を使用して作成することはできません。 REST API または SDK を使用する必要があります。|

実運用上はDeltaテーブルを基にインデックス作成することが多い＝Delta同期インデックスで作成することが多いと思います。
ただ、今回はChromaやFAISSなどと同様に、直接ベクトルストアを作成する「ダイレクト・ベクトル・アクセス・インデックス」で作成してみます。

VectorStoreClientの```create_direct_access_index```を実行して、ダイレクト・ベクトル・アクセス・インデックスを作成します。

```python
source_catalog = "training"
source_schema = "llm"
index_name = f"{source_catalog}.{source_schema}.demo_index"

index = vsc.create_direct_access_index(
    endpoint_name=vector_search_endpoint_name,
    index_name=index_name,
    primary_key="id",
    embedding_dimension=1024,
    embedding_vector_column="text_vector",
    schema={
        "id": "string",
        "text": "string",
        "text_vector": "array<float>",
        "source": "string",
    },
    embedding_model_endpoint_name=embedding_model_endpoint,
)
```

指定したテーブルにインデックスタイプ=直接アクセスのインデックスが作成されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/d2eb97ab-3589-bb05-8786-3a775605133f.png)

# Step5. インデックスへのデータ登録

ダイレクト・ベクトル・アクセス・インデックスは作成直後だと何もデータが入っていません。
Step2で作成した文書を登録しましょう。

LangChainの```DatabricksVectorSearch```クラスからインスタンスを作成します。

```python
from langchain_community.vectorstores import DatabricksVectorSearch

dvs = DatabricksVectorSearch(
    index, text_column="text", embedding=embeddings, columns=["source"]
)
```

文書を追加。

```python
dvs.add_documents(docs)
```

このあたり、LangChainはインターフェースが統一されているので、Chromaなどと同じ形で利用できますね。
これでインデックスにデータを追加できました。

カタログエクスプローラを見ると、登録されている件数を確認することができます。
（なお、サンプルデータ確認は未実装）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/28ea88a5-2b31-f066-419a-2b5ca35ba911.png)

# Step6. 検索

では、実際に検索してみます。
このあたりはLangChainの通常のVectorstore利用と同じやり方です。

```python
query = "フェルンの声優は誰？"
dvs.similarity_search(query, k=1)
```

```:出力
[Document(page_content='「歴史上で最もダンジョンを攻略したパーティーの魔法使い」と自称するだけあり、ダンジョンには詳しい。道中で宝箱を発見するとその中身に異常なまでの興味を示し、判別魔法で99パーセントミミック（宝箱に化けた魔物）とみやぶってなお、残り1パーセントの可能性に賭けて宝箱を開け、上半身をミミックに噛まれてもがくという場面が何度も描かれている。\nフェルン (Fern)\n声 - 市ノ瀬加那', metadata={'source': '葬送のフリーレン', 'id': 'aa521aa0-5998-468a-9028-937945b1f1fc'})]
```

ちなみに一度作成したインデックスを再度利用するには、VectorSearchClientの```get_index```からインデックスオブジェクトを再度取得できます。

```python
index = vsc.get_index(endpoint_name=vector_search_endpoint_name, index_name=index_name)

# LangChainで利用する場合は、DatabricksVectorSearchで再度ラップ
dvs = DatabricksVectorSearch(
    index, text_column="text", embedding=embeddings, columns=["source"]
)
```

# まとめ

いつ東京リージョンに来るかと心待ちにしていたのですが、さらっと来ていました。
これまでRAGを実装するにあたってChromaや外部のベクトルストア系サービスを使っていたものをDatabricksのサービスで置き換えることができそうです。

DatabricksのVector Searchで強力なのはUnity CatalogやDeltaテーブルの連携だと考えていて、従来のデータ管理の上に強力な検索コンポーネントが使えるというのはかなり良いです。
RAGはアーキテクチャ設計において、良い選択肢が増えてありがたいですね。