Azure OpenAI Service ワークショップに参加したので復習してみる(ベクター化)

# はじめに
前回、CosmosDBのChange Feedを使って、登録されたデータをリアルタイムでAI Searchのインデックスにデータ登録までをする機能を作成しました。
今回はインデックス登録にベクターの値を格納するためのフィールドを追加します。また、Azure Open AIを使って、データをベクター化してもらいます。

# 今回実装してみるところ

下の図の赤枠のところです。
Azure Open AIを使ってベクター化し、その結果をAI Searchのフィールドに格納します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/1c8fa880-0a3c-5c0b-d94d-619b0f628f91.png)


### ベクター検索
> データをベクトル形式で表現し、その類似性に基づいて検索を行う手法。
> テキストや画像などの非構造化データもベクター化できる。
> text-embedding-ada-002では1536次元。

# 環境
windows11
Visual Studio Code
Python v3.10.11
Azure Core Tools v4.0.5455(x64)

# 新たに必要なリソース
- Azure Open AI (申請が必要なので注意)
    - text-embedding-ada-002モデルを作成する必要がある

https://learn.microsoft.com/ja-JP/azure/ai-services/openai/how-to/create-resource?pivots=web-portal#deploy-a-model

(前回まで)
- CosmosDB
- AI Search(プランはBasic)
- Azure Functions(ローカルに作成)

# AI Searchでインデックスのフィールドを追加

フィールドの追加から、画像の内容でフィールドを追加します。
ここで、ベクター検索プロファイルを作成するため、作成ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/ec7f7f63-0515-ed8e-d094-03fce877564d.png)


続いて、アルゴリズムの構成を作成するため、作成ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/2c72979d-8c12-dd16-5555-3353d294a18e.png)


デフォルトのまま保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/34cf50b2-f22a-568d-15c8-6b9613914931.png)


そのまま保存します。次の画面でも保存をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/15095bbf-0808-b20a-4359-56ac1f431138.png)


保存されると以下の画像のように、フィールドが追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/48cc4bdc-0481-1a5a-7bf7-943fc4db1216.png)


# Functionsの実装を追加

設定値はAZURE_OPENAI_ENDPOINTとAZURE_OPENAI_API_KEYが追加されています。

```json:local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "python",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "COSMOS_CONNECTION": "",
    "AI_SEARCH_ENDPOINT": "",
    "AI_SEARCH_ADMIN_KEY": "",
    "AZURE_OPENAI_ENDPOINT": "",
    "AZURE_OPENAI_API_KEY": ""
  }
}
```

`AZURE_OPENAI_ENDPOINT`の値👇

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/8da47ee0-9279-3512-02ef-39011045f9b6.png)


`AZURE_OPENAI_API_KEY`の値👇

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/5745e23d-c3b3-d582-f167-d557101a1f67.png)


追加とコメントのある部分のソースコードが追加された部分です。

```python
import azure.functions as func
import logging
import os

from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient

# 追加
from openai import AzureOpenAI
from typing import List

app = func.FunctionApp()

# 追加
ADA_DEPLOYMENT_NAME = "text-embedding-ada-002"

COSMOS_DATABASE_NAME = ""
COSMOS_CONTAINER_NAME = ""
AI_SEARCH_INDEX_NAME = ""

# 追加
aoai_client = AzureOpenAI(azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
                          api_key=os.getenv("AZURE_OPENAI_API_KEY"),
                          api_version="2023-05-15")

search_client = SearchClient(os.getenv("AI_SEARCH_ENDPOINT"),
                             AI_SEARCH_INDEX_NAME,
                             AzureKeyCredential(os.getenv("AI_SEARCH_ADMIN_KEY")))

@app.cosmos_db_trigger(arg_name="items",
                       connection="COSMOS_CONNECTION",
                       database_name=COSMOS_DATABASE_NAME, 
                       container_name=COSMOS_CONTAINER_NAME, 
                       create_lease_container_if_not_exists=False,
                       feed_poll_delay=5000,
                       lease_container_name="leases")
def indexer(items: func.DocumentList):
    logging.info('Python HTTP trigger function processed a request.')
    documents_to_upsert = []
    for item in items:
        
        # 追加
        # content の内容を Azure OpenAI Service にアクセスしてベクター化
        vector_content = get_vector(item["content"])

        documents_to_upsert.append({
            "id": item["id"],
            "title": item["title"],
            "category": item["category"],
            "content": item["content"],
            "contentVector": vector_content}) # 追加
        # ベクター化された値を見るためのログ
        logging.info(vector_content)

    search_client.merge_or_upload_documents(documents_to_upsert)
    logging.info(f"{len(documents_to_upsert)} document(s) uploaded")

# 追加　(ベクター化する関数)
def get_vector(text: str) -> List[float]:
    return aoai_client.embeddings.create(input=text, model=ADA_DEPLOYMENT_NAME).data[0].embedding
```

# 試す

CosmosDBからデータを登録してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/ed8f2fc7-e306-7368-1450-eb2f031315fe.png)

ベクターされた値を見るためにソースコードにログをはりました。
ログを見てみると、謎の数字がたくさん現れています。(長いので省略)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/5ac1f7ca-1d39-c818-13d7-ab2ff20ee532.png)

これがデータが数値のベクトルに変換された証です。

インデックスにデータもしっかり登録されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201826/27c50b90-99d6-4115-7dfb-3401c5d64b0e.png)

# おわりに
ベクター検索をする準備ができました。
公式ドキュメントを見ても私の頭ではチンプンカンプンだったりしたところなので、今回のハンズオンで理解が深まりました。