モデルカタログ で Cohere-embed-v3-multilingual をデプロイして Azure AI Search でベクトル検索する

[Azure AI Studio](https://azure.microsoft.com/products/ai-studio) または [Azure Machine Learning](https://azure.microsoft.com/products/machine-learning) から利用できる[モデルカタログ](https://learn.microsoft.com/azure/ai-studio/how-to/model-catalog-overview)を使用し、従量課金制の [Cohere Embed モデル](https://learn.microsoft.com/azure/ai-studio/how-to/deploy-models-cohere-embed)を MaaS(Models-as-a-Service) としてデプロイします。デプロイしたモデルを使用して Embeddings を生成して、そのデータを Azure AI Search に格納してベクトル検索を実行します。

# モデルカタログ
Azure 上のモデルカタログでは **1,600 以上**のモデルを検索・比較・テスト・ファインチューニング・デプロイ・モニタリング等をすることができます。モデルカタログのコレクションフィルターで「Cohere」を選択してフィルタリングすると、以下のようなモデルが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/d65876e9-95e0-45f4-ec6d-38fdeba7b2b4.png)

## MaaS(Models-as-a-Service) デプロイ
モデルカタログには、カタログからモデルをデプロイして使用するための 2 つの異なる方法が用意されています。それはリアルタイム エンドポイントと従量課金制です。**従量課金制**のデプロイ方法は、 "サービスとしてのモデル (MaaS)" と呼ばれます。 MaaS を介して使用できるモデルは、Microsoft によって管理されるインフラ上でホストされます。これにより、モデルプロバイダーのモデルへの API ベースのアクセスが可能になります。API ベースのアクセスにより、モデルへのアクセスコストが大幅に削減され、プロビジョニング手順が大幅に簡素化されます。ほとんどの MaaS モデルは、トークンベースの価格で使用できます。これは **Azure OpenAI Service の使用・課金方法と同様**になります。


今回「Cohere-embed-v3-multilingual」を使用して Embeddings を生成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/b384f47f-5a51-3ac6-d480-f8b537291426.png)

「サブスクライブとデプロイ」をクリックして、デプロイ名を入力してデプロイします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/0c268185-b3b8-6f84-e5bb-631578487a98.png)

「サーバーレス エンドポイント」にデプロイが完了したエンドポイントが表示されます。「Target URI」と「キー」をコピーしておきます。

# Cohere-embed-v3-multilingual モデル
Embed v3 は Cohere の最新の埋め込みモデルであり、多言語用の「Cohere-embed-v3-multilingual」は日本語を含む 100 以上の言語に対応しています。モデルの諸元は以下の通りです。

- 次元数: `1024`
- コンテキストウィンドウ: `512` トークン
- 入力タイプ: `search_document`, `search_query`, `classification`, `clustering`
- 型: `float`, `int8`, `uint8`, `binary`, `ubinary`
- 価格: $0.0001 / 1000 トークン

OpenAI の Ada-002 と比べてコンテキストウィンドウが少ない点に注意ですね。複数の入力タイプがあり、それぞれのタスクで最高の品質が保証されます。様々な型を選択できるのも魅力的です。価格については Ada-002 と同じになります。

# Cohere SDK(Python)
`pip install cohere` で Cohere の SDK をインストールできます。あとは以下のようにして Embeddings を取得します。

```python
url = "https://<Your-Endpoint>.eastus2.inference.ai.azure.com/v1"
api_key = "Your-API-Key"

co = cohere.Client(
    base_url=url, api_key=api_key
)

def generate_embeddings_cohere(text):
    response = co.embed(
        texts=[text],
        input_type="search_document"
    )
    return response.embeddings[0]
```

## トークン数計測
残念ながら、MaaS エンドポイントには `/tokenize` がありませんでした。Cohere 公式のアカウントを使用すれば以下のようにしてトークン数を計測できます。

```python
response = co.tokenize(text="こんにちは", model="embed-multilingual-v3.0")
print(len(response.tokens))

2
```

# Azure AI Search 統合
Azure AI Search と Cohere-embed-v3-multilingual をデプロイした MaaS エンドポイントを統合するには、データインジェスト時とクエリ時に Embeddings API をコールする必要があります。特に、Integrated Vectorization の Vectorizer にデータ形式を合わせるため、MaaS エンドポイントをコールするための Azure Functions を配置します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/674344/02db946c-fcdd-ca87-49bf-a28934641b56.png)


## 1. データインジェスト時
カスタムスキルのデータ形式ルールに従って以下のような入出力ができる Functions を開発します。Cohere Embed v3 はデータインジェスト時とクエリ時の入力タイプを変える必要があるため `input_type` を指定できるようにしています。デフォルトは `search_query`。

さらにチャンク分割処理を実装する場合、[SplitSkill](https://learn.microsoft.com/azure/search/cognitive-search-skill-textsplit) をスキルセットに実装します。

### カスタムスキル
#### Sample Input:
```json
{
    "values": [
        {
            "recordId": "record1",
            "data": { 
                "text": "平清盛は、山城国の京都または伊勢国の産品（うぶしな）の生まれとされる。",
                "input_type": "search_document"
            }
        }
    ]
}
```

#### Sample Output:
```json
{
    "values": [
        {
            "recordId": "record1",
            "data": {
                "vector": [
                    -0.036010742,
                    -0.02658081,
                    -0.0335083,
                    ...
                ]
            }
        }
    ]
}             
```

### スキルセット
```json
 "skills": [
    {
      "@odata.type": "#Microsoft.Skills.Custom.WebApiSkill",
      "name": "#1",
      "description": null,
      "context": "/document/pages/*",
      "uri": "[AzureFunctionEndpointUrl]/api/embeddings?code=[AzureFunctionDefaultHostKey]",
      "inputs": [
        {
          "name": "text",
          "source": "/document/pages/*"
        },
        {
          "name": "input_type",
          "source": "= 'search_document'"
        }
      ],
      "outputs": [
        {
          "name": "vector",
          "targetName": "vector"
        }
      ]
    },
    {
      "@odata.type": "#Microsoft.Skills.Text.SplitSkill",
      "name": "#2",
      "description": "Split skill to chunk documents",
      "context": "/document",
      "defaultLanguageCode": "ja",
      "textSplitMode": "pages",
      "maximumPageLength": 500,
      "pageOverlapLength": 50,
      "maximumPagesToTake": 0,
      "inputs": [
        {
          "name": "text",
          "source": "/document/text"
        }
      ],
      "outputs": [
        {
          "name": "textItems",
          "targetName": "pages"
        }
      ]
    }
  ],
  ```

### Index Projection
[SplitSkill](https://learn.microsoft.com/azure/search/cognitive-search-skill-textsplit) によって分割されたチャンクは `/document/pages/*` 配下に保持されますが、これを子インデックスとして投射します。
```json
 "indexProjections": {
    "selectors": [
      {
        "targetIndexName": "cohere-embed-v3-multi",
        "parentKeyFieldName": "parent_id",
        "sourceContext": "/document/pages/*",
        "mappings": [
          {
            "name": "chunk",
            "source": "/document/pages/*",
            "sourceContext": null,
            "inputs": []
          },
          {
            "name": "vector",
            "source": "/document/pages/*/vector",
            "sourceContext": null,
            "inputs": []
          },
          {
            "name": "title",
            "source": "/document/title",
            "sourceContext": null,
            "inputs": []
          },
          {
            "name": "docid",
            "source": "/document/docid",
            "sourceContext": null,
            "inputs": []
          }
        ]
      }
    ],
    "parameters": {
      "projectionMode": "skipIndexingParentDocuments"
    }
  },
```

## 2. クエリ時の自動ベクトル化
クエリ時に自動的に Embeddings 化を行うには、インデックスに `vectorizers` 設定を追加します。Azure OpenAI Service 統合だけでなく、`customWebApiParameters` によって独自の Embeddings API エンドポイントを指定することができます。

```json
    "vectorizers": [
      {
        "name": "vectorizer-1716053850371",
        "kind": "customWebApi",
        "azureOpenAIParameters": null,
        "customWebApiParameters": {
          "httpMethod": "POST",
          "uri": "[AzureFunctionEndpointUrl]/api/embeddings?code=[AzureFunctionDefaultHostKey]",
          "timeout": "PT3M50S",
          "authResourceId": "",
          "httpHeaders": {},
          "authIdentity": null
        }
      }
    ],
```

データの入出力形式はカスタムスキルと同様になりますが、エンドポイントからの出力形式についての Docs がありませんでした。[こちらの方](https://qiita.com/dija/items/57d61d1499623d993ac6)が `values[].data.vector` であることを発見されています。

## Azure AI Search SDK(Python)
Azure AI Search の Python SDK は `11.6.0b4` をインストールします。
```bash
!pip install azure-search-documents==11.6.0b4
```

`vectorizers` で検索するには `VectorizableTextQuery` を使用する必要があります。

```python
from azure.search.documents import SearchClient
from azure.search.documents.models import (
    VectorizableTextQuery
)

search_client = SearchClient(
    service_endpoint, index_name, credential=credential
)
vector_query = VectorizableTextQuery(
    text=query, k_nearest_neighbors=5, fields="vector", exhaustive=False
)
docs = search_client.search(
    search_text=None,
    vector_queries=[vector_query],
    select=["text,docid"],
)

for doc in docs:
    print(doc["docid"], doc["text"])

```


# 精度比較
[以前行った](https://qiita.com/nohanaga/items/e159712acbf80e40ca1a#%E7%B5%90%E6%9E%9C) OpenAI v2/v3 との精度比較と同様の試験を行うと、大体 `ada-v2`,`v3-small` 以上、 `v3-large` 以下というデータが取れました。

| | text-embedding-ada-002|text-embedding-3-small|Cohere-embed-v3-multi(document)|Cohere-embed-v3-multi(query)
|:-:|:-:|:-:|:-:|:-:|
|#1 Recall@3 avg|0.521|0.562|0.617|**0.652**
|#1 Recall@5 avg|0.706|0.723|0.737|**0.786**
|#2 Recall@3 avg|0.524|0.594|0.619|**0.686**
|#2 Recall@5 avg|0.643|0.657|0.781|**0.817**

ドキュメントは　`input_type: search_document` で Embeddings を生成し、クエリ時に `input_type` を `search_query` にした場合と、 `search_document` のままにした場合で比較しています。[Docs](https://learn.microsoft.com/azure/ai-studio/how-to/deploy-models-cohere-embed#v1embed-request-schema) 通りにクエリ時は `search_query` にするべきですね。

# MaaS 従量課金モデルのデータプライバシー
気になる入出力データのプライバシーポリシーについては、以下のように規定されているので安心です。
> Microsoft は、これらのプロンプトおよび出力をモデル プロバイダーと共有しません。また、Microsoft は、Microsoft、モデル プロバイダー、またはサード パーティのモデルをトレーニングまたは改善するためにこれらのプロンプトおよび出力を使用しません。モデルはステートレスであり、プロンプトや出力はモデルに保存されません。

## モデルのライセンス条項
>モデルはモデル プロバイダーによって提供され、モデルの使用 (およびモデルとその出力に対するモデル プロバイダーの責任) はモデルに付属のライセンス条項に従うものとします

https://learn.microsoft.com/azure/ai-studio/how-to/concept-data-privacy#generate-inferencing-outputs-with-pay-as-you-go-deployments-models-as-a-service


# カスタムスキル GitHub
https://github.com/nohanaga/cohere-embed-v3-multilingual-skill

# 次回

https://qiita.com/nohanaga/items/a6cd2c2d589a95a3722a