Azure Cosmos DB for MongoDBを用いたベクトル検索デモ

MS Build（2023年5月）にて**Azure Cosmos DB for mongoDB vcoreのベクトルサーチ機能**がリリースされたことを知ったので試しに使ってみました。
[Introducing Vector Search in Azure Cosmos DB for MongoDB vCore](https://devblogs.microsoft.com/cosmosdb/introducing-vector-search-in-azure-cosmos-db-for-mongodb-vcore/)

# どんなものを作ったか
1. SharePointへアップロードされたファイルのCosmos DBへのベクトル保存
2. Cosmos DBのベクトルインデックスを用いた類似ファイルの検索

※２に関して、ファイルはテキストファイルのみで行っています。

## 1. Cosmos DBへのベクトル保存
Wikipediaからドラゴンボールとハンターハンターの文章を取ってきて、それぞれをテキストファイルにしてSharePointにアップロードします。

ドラゴンボール
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/44518941-3640-7bb7-1232-1873dff50943.png)
ハンターハンター
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/47e77741-7cea-4418-1409-2b489e95c7b2.png)
SharePointにアップロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/564e5bd3-288c-6634-d967-e300d0a98a6e.png)

...しばらくたつと（数秒）

Cosmos DBにドキュメントが作成されている！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/4988fe84-1ce3-5bdc-ee59-dbb89d71a64d.png)
ベクトル化もされており、保存されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/a7d4b6bd-c2ff-84c7-91ce-26ccb2b70766.png)

## ベクトル検索
上記で保存したファイルの検索を行っていきます。

Azure Static Web Appsで以下のような簡単なフォームを用意しました。こちらにキーワードを入力し、それに近しいファイルを取得します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/aec75f74-972a-d974-aa52-e1f0dd59c1af.png)

「ゴン」と入力し、検索すると結果が返ってきました。
近しい順に結果を返すようにしているので、ハンターハンターのデータが先に来ているので正しいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/d957db0d-fb35-5e3a-1cff-8ce883be049a.png)
## 構成
上記１、ベクトル保存の構成
![qiita_architecture001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/3fdbcba2-fff7-f63a-c6c1-9b75092ebcbb.png)

上記２、類似ファイル検索の構成
![qiita_architecture002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/139ff70b-4a30-d165-4b12-bee9b9aef52b.png)

※Azure FunctionsではC#を用いています。

# 実装方法
以下２つに分けて説明します。

1. ベクトル保存の実装
2. ベクトル検索の実装 

## ベクトル保存の実装
各Azureリソースにおける実装の説明をします。
1. LogicApps
1. Azure Functions
1. Cosmos DB

### LogicApps
トリガーはSharePointのファイル作成時のものを使用します。
使用しているコネクタは１つだけです。AzureストレージアカウントのBlob作成を使用します。
![qiita_logicapps001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/23c79550-480a-d0c2-cc28-f8a16b7696ec.png)

### Azure Functions
Azure FunctionsはC#で記述しています。

#### コントローラー
demoコンテナーにBlobファイルが作成時にトリガーされます。
```C#
public class DocumentVectorRegister
{
    private readonly IVectorService vectorService;

    public DocumentVectorRegister(IVectorService vectorService)
    {
        this.vectorService = vectorService;
    }

    [FunctionName("DocumentVectorRegister")]
    public async Task Run([BlobTrigger("demo/{name}", Connection = "StorageConnectionString")]Stream myBlob, string name, ILogger log)
    {
        await vectorService.RegisterDocumentVector(name);
    }
}
```

#### Blobコンテンツ読み取りのリポジトリ
```C#
public class BlobRepository : IBlobRepository
{
    private readonly BlobServiceClient blobServiceClient;
    private readonly IConfiguration configuration;

    public BlobRepository(IAzureClientFactory<BlobServiceClient> clientFactory, IConfiguration configuration)
    {
        blobServiceClient = clientFactory.CreateClient("OpenAI");
        this.configuration = configuration;
    }

    async Task<string> IBlobRepository.GetBlobContent(string filename)
    {
        var containerClient = blobServiceClient.GetBlobContainerClient(configuration["OpenAIContainerName"]);
        var blobClient = containerClient.GetBlobClient(filename);
        Response<BlobDownloadInfo> downloadResponse = await blobClient.DownloadAsync();
        using (StreamReader reader = new StreamReader(downloadResponse.Value.Content))
        {
            return await reader.ReadToEndAsync();
        }
    }
}
```

#### ベクトル化部分のリポジトリ(Azure OpenAI使用)
```C#
public class OpenAIRepository : IOpenAIRepository
{
    private readonly OpenAIClient openAIClient;

    public OpenAIRepository(OpenAIClient openAIClient)
    {
        this.openAIClient = openAIClient;
    }

    async Task<List<float>> IOpenAIRepository.GetVector(string text)
    {
        EmbeddingsOptions embeddingOptions = new(text);
        var returnValue = await openAIClient.GetEmbeddingsAsync("EmbeddingAda002", embeddingOptions);
        var vectores = new List<float>();
        foreach (float item in returnValue.Value.Data[0].Embedding)
        {
            vectores.Add(item);
        }
        return vectores;
    }
}
```

#### ベクトル保存部分のリポジトリ
```C#
public class VectorRepository : IVectorRepository
{
    private readonly IMongoClient mongoClient;

    public VectorRepository(IMongoClient mongoClient)
    {
        this.mongoClient = mongoClient;
    }

    async Task IVectorRepository.RegisterDocument(Doc document)
    {
        var db = mongoClient.GetDatabase("sample_docs");
        var _docs = db.GetCollection<Doc>("docs");
        await _docs.InsertOneAsync(document);
    }
{
```
### Cosmos DB
手順としては以下になります。
1. Cosmos DB for MongoDB vcore のリソースを作成
2. MongoDB Compassのダウンロード
3. データベースとコレクションの作成 

#### Cosmos DB for MongoDB vcore のリソースを作成
[Azure Portal](https://portal.azure.com/)から作成します。特記事項はありません。

#### MongoDB Compassのダウンロード
2023年6月時点でCosmos DB for MongoDB vcoreがプレビュー版のため、Azure Portalでデータエクスプローラーがありませんでした。データベースの作成やコレクション作成をためダウンロードしました。
https://www.mongodb.com/ja-jp/products/compass

#### データベースとコレクションの作成
MongoDB Compassにて、先ほど作成したCosmos DB for MongoDB vcoreの接続文字列を使用し、接続します。以下黄色ライン部分に入力して接続します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/4ed2400d-4be7-8b15-561b-075aefcff860.png)

接続後、mongoshellよりデータベースとコレクションの作成を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/1786a0d6-5aaf-014c-2f6b-e639da32a7a4.png)

データベース作成
```
use 'データベース名'
```

コレクション作成
```
db.createCollection('コレクション名')
```

これで、ベクトル保存の実装が終わりました。
SharePointにファイルをアップロードして、実際にCosmos DBにデータが作成されるか試してみてください。


## ベクトル検索の実装
各Azureリソースにおける実装の説明をします。
1. Azure Functions
1. Cosmos DB

### Azure Functions
HTTPトリガーで作成します。
#### コントローラー
```C#
public class SearchDocument
{
    private readonly IVectorService vectorService;

    public SearchDocument(IVectorService vectorService)
    {
        this.vectorService = vectorService;
    }

    [FunctionName("SearchDocument")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")] [FromQuery]SearchRequestModel request)
    {
        var input = new SearchInputModel()
        {
            Keyword = request.Keyword,
        };
        var res = await vectorService.GetDocuments(input);
        var result = new List<SearchDocumentsResponseModel>();
        foreach (var item in res)
        {
            result.Add(new SearchDocumentsResponseModel() { 
                Text = item.Text,
                Filename = item.Filename,
            });
        }
        return new JsonResult(result);
    }
}
```

#### 入力キーワードのベクトル化のリポジトリ
先ほどのベクトル保存で用いたものと同じものを使用します。
#### ベクトル検索のリポジトリ
MSの公式ドキュメントでは、2023年6月時点で、monogoshellでの検索の仕方しか記載されてなく、上手いやり方が分からなかったので、BsonDocumentを使ってMongoDBクエリを作成しています。
参考：[$search を使用して vectorIndex に対してクエリを実行する](https://learn.microsoft.com/ja-jp/azure/cosmos-db/mongodb/vcore/vector-search#performing-a-vector-search/)

```C#
public class VectorRepository : IVectorRepository
{
    private readonly IMongoClient mongoClient;

    public VectorRepository(IMongoClient mongoClient)
    {
        this.mongoClient = mongoClient;
    }

    async Task<List<Doc>> IVectorRepository.GetDocuments(List<float> vectors)
    {
        var db = mongoClient.GetDatabase("sample_docs");
        var _docs = db.GetCollection<Doc>("docs");

        BsonArray bsonVectorArray = new BsonArray();
        foreach ( var v in vectors )
        {
            bsonVectorArray.Add(v);
        }

        PipelineDefinition<Doc, BsonDocument> pipeline = new BsonDocument[]
        {
            new BsonDocument("$search",
            new BsonDocument
            {
                { "cosmosSearch", new BsonDocument{ { "vector", bsonVectorArray }, { "path", "Vectors" }, { "k", 3 } } },
                { "returnStoredSource", true }
            })
        };

        var resultDocuments = _docs.Aggregate(pipeline).ToList();
        var result = BsonSerializer.Deserialize<List<Doc>>(resultDocuments.ToJson());

        return result;
    }
}
```
### Cosmos DB
検索用に今回新たに実装された、検索用ベクターインデックスも作成していきます。
参考：[Azure Cosmos DB for MongoDB 仮想コアの埋め込みでのベクター検索の使用](https://learn.microsoft.com/ja-jp/azure/cosmos-db/mongodb/vcore/vector-search/)
```
use '先ほど作成したデータベース名'
```
インデックス作成
```
db.runCommand({
  createIndexes: 'exampleCollection',
  indexes: [
    {
      name: 'vectorSearchIndex',
      key: {
        "vectorContent": "cosmosSearch"
      },
      cosmosSearchOptions: {
        kind: 'vector-ivf',
        numLists: 100,
        similarity: 'COS',
        dimensions: 3
      }
    }
  ]
});
```
exampleCollectionに先ほど作成したコレクション名を指定します。
vectorContentにはDBでのベクターが保存されるカラム名を指定します。

※dimentionsの値について
ドキュメント通り3で指定すると以下のエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409268/bd9a67c2-157e-4c17-bf72-45aa887eb2b0.png)
Azure OpenAIのベクトル化に使用するモデル（text-embeddings-ada）でベクトル化行った結果、1536のベクトル数になるので、1536を指定します。

インデックス作成成功すると、以下のレスポンスが返ってきます。
```
{
  raw: {
    defaultShard: {
      numIndexesBefore: 1,
      numIndexesAfter: 2,
      createdCollectionAutomatically: false,
      ok: 1
    }
  },
  ok: 1
}
```

これでベクトル検索の実装は終わりました。
実際にキーワードを投げてみて、そのキーワードが含まれるファイルが取得できるか試してみましょう。





# さいごに雑記
* 今回はテキストファイルのみだったので、PDFやPowerPointなどにも対応できるといい。その際はAzure Functions内で、その形式に合わせたコンテンツ読み取りの実装を行う必要がありそう。
* SharePoint自体の検索にコンテンツ検索ができるため、SharePoint内の検索のみであれば今回の実装を使う必要はない。
* Azureのベクトル保存の実装方法は他にもたくさんあるので、要件に応じてどれを使うか選ぶ必要がある。