Azure AI Searchにおけるベクトル値のインデクシング入門ガイド

 

こんにちは、サイオステクノロジーの佐藤陽です。
今回は前回に引き続き、Azure AI Searchのインデクシングに関して記事を書いていこうと思います。

前回は基本的なインデクシングの部分に触れたので、今回は肝となるベクトル値のインデクシング部分を試してみたいと思います。
ベクトル値を扱うことで、ぐぐっと検索性能が高まるので、是非マスターしましょう。

はじめに

前回の記事ではAzure AI Searchにとりあえずインデックスを定義し、テキストデータをインデクシングする流れを紹介しました。
ただ、やはりAI Searchといえばベクトル値を扱えることが大きな強みです。
そこで今回は実際に格納するデータをベクトル化し、それらの値をインデクシングする流れをご紹介します。

全体の流れ

ベクトル値をAI Searchに登録していくためには以下の3つのステップがあります。

  1. AI Searchにベクトル値を格納するためのスキーマを定義する
  2. 値をベクトル化する
  3. インデクシングを行う

このブログでも、この3つのステップに沿って解説していきたいと思います。

値のベクトル化

まず値のベクトル化とは何か簡単に説明します。

ベクトルという言葉自体は高校数学で出てくる単語であり、「向きと大きさを持つ」という言葉はよく覚えているのではないでしょうか?
よく見るのが下図のようなX軸、Y軸があり、矢印が伸びてるやつですね。

今回の「値のベクトル化」というのもイメージは同じです。
例えば「犬」という言葉も、実はこのようなベクトルとして表すことが可能です。

ただし、X軸,Y軸といった2次元ではなく、1000を超えるような多次元での表記になります。
そしてこのベクトル化を実現するのが、OpenAI社等が提供しているembeddingsモデルになります。

例えばOpenAI社からはtext-embedding-3-smallといったモデルが提供されており、これはAzure OpenAI Serviceからでも利用可能になります。
今回はAzure OpenAI Service上にtext-embedding-3-smallモデルをデプロイして、実際にベクトル化を行ってみたいと思います。

モデルをデプロイし、以下のような形でAPIを実行します。

POST /openai/deployments/{{deployment name}}/embeddings?api-version=2024-02-01 HTTP/1.1
Host:  {{AOAI HostName}}
Content-Type: application/json
api-key: {{api-key}}
Content-Length: 29

{
    "input": "犬"
}

以下がレスポンスとなり、data/embeddingの値が「犬」という言葉をベクトル化した実際の値になります。
※1536個の配列で表現されているため、一部省略しています。

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [ //1536個の配列
        -0.019703003,
        -0.014401983,
        -0.011315843,
        0.0118301995,
        0.0028840704,
        -0.010234645,
        (略)
        -0.0044927467
      ]
    }
  ],
  "model": "text-embedding-3-small",
  "usage": {
    "prompt_tokens": 3,
    "total_tokens": 3
  }

ベクトル化をすると何が嬉しいか?

では言葉をベクトル化すると何が嬉しいのでしょうか?
大きなメリットとして、「機械が自然言語を扱いやすくなる」ということが挙げられます。

そして、このベクトル化を行うことでベクトル検索やハイブリット検索などが可能になり
RAGにおけるRetrieveの処理が、高い精度で実現できるようになります。

ただし今回はインデクシングにスポットを当てた記事であるため、検索手法に関しては深くは触れません。
気になる方は、弊社のMVPである武井が分かりみ深く解説しているので、こちらの記事を是非参照してください!

生成AI時代の様々な検索手法を検証する 〜Azure AI Searchによるベクトル/セマンティック/ハイブリッド検索〜

インデクシング

では、言葉のベクトル化が実現できたところで、次はこの値をAzure AI Searchに対してインデクシングしていきます。
大きな流れとしては、前回の記事で紹介した時と同じです。

まずはベクトル値を格納できるようなスキーマを定義し、インデックスを作成します。

スキーマ定義

今回は、前回の記事で作成したインデックスをベースとして、ここにベクトル値を扱うためのスキーマを追加します。

ちなみに前回は以下のようなデータを扱っていました。

  {
    "UserId": "1001",
    "UserName": "田中 一郎",
    "Profile": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き",
    "Age": 36,
    "Tags": ["Engineer", "Azure"]
  },

今回は、このProfileの値をベクトル化し、インデクシングすることを想定します。 インデックスを作成するため、以下の内容でAPIを実行します。

前回の記事の違いとしては以下2点が挙げられます。

  1. fieldsにおけるProfileVectorの追加
  2. vectorSearchパラメータの追加
POST /indexes?api-version=2024-07-01 HTTP/1.1
Host: {{AI Search}}.search.windows.net
Content-Type: application/json
api-key: {{AI Search API KEY}}
Content-Length: 1090

{
    "name": "idx-users",
    "fields": [
        {
            "name": "UserId",
            "type": "Edm.String",
            "key": true,
            "filterable": true
        },
        {
            "name": "UserName",
            "type": "Edm.String",
            "searchable": true,
            "filterable": true,
            "facetable": false
        },
        {
            "name": "Profile",
            "type": "Edm.String",
            "searchable": true,
            "filterable": false,
            "sortable": false,
            "facetable": false,
            "analyzer": "ja.lucene"
        },
        {
            "name": "ProfileVector",
            "type": "Collection(Edm.Single)",
            "searchable": true,
            "retrievable": true,
            "dimensions": 1536,
            "vectorSearchProfile": "my-vector-profile"
        },
        {
            "name": "Age",
            "type": "Edm.Int32",
            "searchable": false,
            "filterable": true,
            "sortable": true,
            "facetable": true
        },
        {
            "name": "Tags",
            "type": "Collection(Edm.String)",
            "searchable": false,
            "filterable": true,
            "sortable": false,
            "facetable": true
        }
    ],
    "vectorSearch": {
        "algorithms": [
            {
                "name": "hnsw-1",
                "kind": "hnsw",
                "hnswParameters": {
                    "m": 4,
                    "efConstruction": 400,
                    "efSearch": 500,
                    "metric": "cosine"
                }
            }
        ],
        "profiles": [
            {
                "name": "my-vector-profile",
                "algorithm": "hnsw-1"
            }
        ]
    }
}

Fielidの新規追加

ベクトル値を格納するため、以下のフィールドを追加しました。
各値を確認していきます。

{
    "name": "ProfileVector", //ベクトル値を格納するためのフィールドを定義
    "type": "Collection(Edm.Single)", //ベクトル値を格納する際はCollection(Edm.Single)というtypeを利用
    "searchable": true,
    "retrievable": false,
    "stored": false,
    "dimensions": 1536, //次元数
    "vectorSearchProfile": "my-vector-profile"
}

type

今回ベクトル値を格納するFieldとして、Collection(Edm.Single)というtypeを利用します。

理由としてはEmbeddingsモデルとしてtext-embedding-3-smallを利用しており、このモデルがfloat32の型で出力するためです。
利用するモデルに合わせて適宜使い分けてください。

参考:ベクター フィールドの EDM データ型

dimensions

dimensionsはベクトルの次元数を表します。

記事冒頭で説明したX軸,Y軸のベクトルは2軸(dimensions=2)ですが、今回は1536軸という多次元のベクトルで自然言語を表します。

この数値に関してですが、これもtypeと同様に、利用するモデルに依存します。
今回はtext-embedding-3-smallを利用しており、このモデルの次元数を1536次元としているためこの値となります

vectorSearchProfile

vectorSearchProfileに関しては、ベクトル検索時の設定を行います。
また、この値はfieldのセクションの下に書かれている、vectorSearchのセクションの値を参照しています。

"vectorSearch": { //検索用の設定
    "algorithms": [
        {
            "name": "hsnw-1",
            "kind": "hnsw",
            "hnswParameters": {
                "m": 4,
                "efConstruction": 400,
                "efSearch": 500,
                "metric": "cosine"
            }
        }
    ],
    "profiles": [
        {
            "name": "my-vector-profile", //fieldsの中で、こちらを参照
            "algorithm": "hnsw-1" //上の"algorithms"のセクションを参照
        }
    ]
}
ベクトル検索手法(余談)

ここで少しだけ、ベクトル検索の手法について言及してみたいと思います。

ただし、このあたりを詳細に述べるとそれはそれで記事が何本か書けそうなのと
自分もまだ十分に理解できていない部分があるので、参考程度にして頂けると幸いです。
誤った記述がありましたら、是非コメントなどでご指摘お願いします!

ベクトル検索においては、「検索クエリのベクトル値」に最も近い「インデクシングされているデータのベクトル値」を結果として返します。
そしてこの手法は最近傍探索(Nearest Neighbor Search:NN)と呼ばれます。

最近傍探索は、AI Searchに保存されている全てのデータに対して網羅的に検索を行い、最もベクトル値の値が近しいものを抽出します。
しかし、分析するデータ量が膨大である場合、それに伴い検索時間も長くなります。

一方で、近似最近傍探索(ANN)というものが考案されました。
ANNもベクトル値の値で比較を行う事には変わりないのですが、必ずしも一番近いものとは限らないポイントをデータセットから抽出します。
精度としては最近傍探索には劣りますが、少ない検索時間で、それなりに実用的なデータを検索することが可能となります。

Azure AI SearchではこのANNアルゴリズムにHNSWが使われています。

また、このANNとNNの中間的に存在するk近傍法(kNN)という方法もあり、高速に結果を出しながらも高い正確性を保つとされています。
ただし、パラメータであるkの値を正しく決定することが難しく、扱いが難しいとされているようです。
なお、kNNに関してもAzure AI Searchにてサポートされています。

参考:近似最近傍探索(ANN)アルゴリズムを理解する
参考:最近傍検索

今回の記事においては、HNSWを利用してベクトル検索を行います。

インデクシング

スキーマの定義が行えたので、ここから実際にデータをインデクシングしていきます。

まずはインデクシングする値のベクトル化を行います。
今回はProfileの値をベクトル値として登録するため、

{
"UserId": "1001",
"UserName": "田中 一郎",
"Profile": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き",
"Age": 36,
"Tags": ["Engineer", "Azure"]
}

このデータのProfileの値を、Azure OpenAI Serviceのembeddingsモデルを使ってベクトル化します。
流れとしては、冒頭で述べた「犬」をベクトル化した時と同じです。

POST /openai/deployments/{{deployment name}}/embeddings?api-version=2024-02-01 HTTP/1.1
Host:  {{AOAI HostName}}
Content-Type: application/json
api-key: {{api-key}}
Content-Length: 125


{
    "input": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き"
}

返ってきた値がこちらです。

{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "index": 0,
      "embedding": [
        -0.016684273,
        -0.034290213,
        (略)
        0.07658217,
        -0.03793499,
        0.0010185471,
        0.018276243
      ]
    }
  ],
  "model": "text-embedding-3-small",
  "usage": {
    "prompt_tokens": 89,
    "total_tokens": 89
  }
}

このembeddingの値を利用して、実際にインデクシングを行っていきます。
前回の記事で扱ったデータ全ての対応は大変だったので、今回は2名分だけ…。

POST /indexes/idx-users/docs/index?api-version=2024-07-01 HTTP/1.1
Host:  {{AISearch HostName}}
Content-Type: application/json
api-key: {{api-key}}
Content-Length: 716


{
    "value": [
        {
            "@search.action": "upload",
            "UserId": "1001",
            "UserName": "田中 一郎",
            "Profile": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き",
            "ProfileVector": [
                -0.016684273,
                -0.034290213,
                (略)
                -0.003558369,
                -0.04298321,
                -0.014977094,
                -0.018883705,
                0.017176528,
                0.018276243
            ],
            "Age": 36,
            "Tags": [
                "Engineer",
                "Azure"
            ]
        },
        {
            "@search.action": "upload",
            "UserId": "1004",
            "UserName": "山田 真美",
            "Profile": "福岡県出身。大学卒業後、プロジェクトマネージャーとして多くのプロジェクトを成功に導く。現在はIT企業でPMとして活躍。趣味は旅行で、特に海外旅行が好き",
            "ProfileVector": [
                -0.016684273,
                -0.034290213,
                (略)
                -0.003558369,
                -0.04298321,
                -0.014977094,
                -0.018883705,
                0.017176528,
                0.018276243
            ],
            "Age": 33,
            "Tags": [
                "Project Manager",
                "IT"
            ]
        },
    ]
}

これでインデクシングの方は完了です。

検索

せっかくなので試しに検索してみます。

検索を行う場合は、 /indexes/idx-users/docs/searchのパスに対してPOSTのリクエストを発行します。

ベクトル検索

まずはベクトル検索を試します。

ベクトル検索においては、検索クエリのベクトル値と、格納されてるデータのベクトル値を比較するため、検索クエリの文章についてもベクトル化する必要があります。

今回は以下のような検索クエリを想定し、この値をベクトル化します。

「推理小説が好きなのは誰ですか?」

これは、田中さんのProfleに含まれる「ミステリー好き」にヒットさせることが目的です。

キーワード検索だと直接的なワードが入っていなければヒットしませんが、
ベクトル検索であればベクトル値として似ている単語でヒットするはずなので、 検索結果に引っかかってくるはずです。

ベクトル化の手順としては先ほどと同様なので省略します。
ベクトル化した値をvectorQueries/vectorの値に入れ、検索を行います。

POST /indexes/idx-users/docs/search?api-version=2024-07-01 HTTP/1.1
Host:  {{AISearch HostName}}
Content-Type: application/json
api-key: {{api-key}}
Content-Length: 481

{
    "count": true,
    "select": "UserName",
    "vectorQueries": [
        {
            "vector": [
                0.0067357426,
                -0.02722012,
                -0.04022639,
                0.004409659,
                (略)
                -0.0018893238,
                0.01361006,
                -0.013748635
            ],
            "k": 3,
            "fields": "ProfileVector",
            "kind": "vector",
            "exhaustive": true
        }
    ]
}

すると、以下の結果が返ってきました。

{
    "@odata.context": "https://{{resourceName}}.search.windows.net/indexes('idx-users')/$metadata#docs(*)",
    "@odata.count": 2,
    "value": [
        {
            "@search.score": 0.60031176,
            "UserName": "田中 一郎",
            "Profile": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き"
        },
        {
            "@search.score": 0.5653324,
            "UserName": "山田 真美",
            "Profile": "福岡県出身。大学卒業後、プロジェクトマネージャーとして多くのプロジェクトを成功に導く。現在はIT企業でPMとして活躍。趣味は旅行で、特に海外旅行が好き"
        }
    ]
}

予想通り田中さんも引っかかってくれたのですが、同じように山田さんも検索に引っかかりました。
しかも山田さんも割とスコアが高いですね…、もう少し差が出るかと思ったのですが。

(ちなみに@search.scoreは検索スコアと呼ばれる値であり、高ければ高いほど相関が高い事を示しています。)

以下のように検索クエリの内容をいくつか変えてみたのですが、あまりスコア差は変わりませんでした。

  • 推理小説(単語だけにしてみる)
  • 密室で事件が起こるような本を読むのが好きな人は誰?(推理小説の言い換え)
  • 首都(東京をターゲットにしてみる)
  • Trip(山田さんの”海外旅行”をターゲットにしてみる)

何かしら差別化するためのテクニックがあるのかもしれません。
あとはインデックスに登録したvectorSearchのパラメータであるのhnswParametersの設定などにも依存しそうです。

ただ今回の記事としては、このあたりの精度向上はスコープ外とさせていただきたいと思います。

全文検索

また、比較対象として全文検索の方も行っておきたいと思います。
ベクトル値は入れずに、searchの中に検索クエリを入れます。

POST /indexes/idx-users/docs/search?api-version=2024-07-01 HTTP/1.1
Host:  {{AISearch HostName}}
Content-Type: application/json
api-key: {{api-key}}
Content-Length: 125

{
    "search": "推理小説が好きなのは誰ですか?",
    "select": "UserName, Profile",
    "searchFields": "Profile",
    "count": true
}

レスポンスとしては以下の内容が返ってきました。 Profileの中に「推理小説」というワードは含まれていませんが、全文検索としても一応回答が返ってきました。 しかしベクトル検索に対して、スコアが著しく低いことが分かります。

{
    "@odata.context": "https://{{resourceName}}.search.windows.net/indexes('idx-users')/$metadata#docs(*)",
    "@odata.count": 2,
    "value": [
        {
            "@search.score": 0.1678722,
            "UserName": "田中 一郎",
            "Profile": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き"
        },
        {
            "@search.score": 0.1678722,
            "UserName": "山田 真美",
            "Profile": "福岡県出身。大学卒業後、プロジェクトマネージャーとして多くのプロジェクトを成功に導く。現在はIT企業でPMとして活躍。趣味は旅行で、特に海外旅行が好き"
        }
    ]
}

ここで、検索クエリを「推理小説」だけにして検索すると、以下のように帰ってきました。

{
    "@odata.context": "https://ais-aksato.search.windows.net/indexes('idx-users')/$metadata#docs(*)",
    "@odata.count": 0,
    "value": []
}

先程検索結果が返ってきたのは、恐らく検索クエリが形態素解析され「好き」の部分が全文検索で引っかかったためだと思われます。
実際、検索クエリを「好き」とだけして検索をかけると、0.1678722というスコアが返ってきました。

以上のことから、ベクトル検索を用いることで
インデクシングされてるデータに直接含まれていないワードでも検索できることが分かりました。

終わり

今回は、AI Searchの肝となるベクトル検索を試すため

  • 値のベクトル化
  • ベクトル値を格納するためのインデックス作成
  • ベクトル値のインデクシング

を試してみました。

AI Searchのデータインポート機能を使えば、ここのあたりもいい感じにやってくれるのですが
このあたりの流れを理解しておくと、チューニングやトラブルシューティングに役立つかと思います。

チューニングについてはまだまだ奥が深そうなので、機会があれば取り組んでみたいと思います。
是非RAGの構築にあたりAI Searchを使いこなしていきましょう!

ではまた!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です