こんにちは、サイオステクノロジーの佐藤陽です。
今日はAzure AI Searchのインデクシングに関する記事になります。
RAGの構築にあたっても非常に重要となる「Azure AI Search」の基本的なところを解説していきます!
はじめに
正直なところ、今までなんとなくでAI Search使っていました。
- AzurePortalのAI Searchから「データをインポート」としたら、いい感じにインデックスが作成された
- サンプルコードとか見ると、良い感じにインデックス作成するプログラムが既に提供されていた
- とりあえず適当にデータ投入しても、RAGに組み込んだらそれなりの回答返ってくる
といった感じで、何となくでも使えてはいたのですが「これでいいのか…?」と思い、
腰を据えて公式ドキュメントや、様々な記事を読んでまとめてみました。
目新しい情報は無いかもしれないですが
「AI Search全然わからん」から「AI Searchなんとなく分かった」となれば幸いです。
(自分としては、公式のサンプルのインデクシングのサンプルコードは理解できるようになりました。)
今回の記事で扱うテーマとしては、AI Searchの中でもスキーマ設計の基本や、データ投入の実装の部分とし、
スキーマの各種パラメータの解説や、設計からデータの投入までの基本的な流れを解説してきます。
また、まだまだ勉強中の身なので、見当違いのことを書いていたらコメントなどで教えていただけると幸いです!
AI Searchの主な機能
Azure AI Searchの主たる機能としては以下2つです。
- Azure AI Searchにデータを追加する処理(インデクシング)
- Azure AI Searchに対する検索処理(クエリの実行)
MSの公式でも描かれている以下の図が分かりやすいです。
インデクシング
各種データをAI Searchに登録し、検索可能な状態とします。
特徴としては
- テキストとして登録するだけではなく、ベクトル値としても登録が可能
- 投入するデータに関して、細かいレベルで変換や検索の設定を行う事が可能(検索性能に直結)
といった点が挙げられるかと思います。
繰り返しにはなりますが、今日はこのインデクシングの機能にフォーカスし
- スキーマの定義
- インデックスの作成
- データ投入
あたりの話をメインに書いていきます。
クエリの実行
ユーザーからの質問に対して回答を出す機能です。
ユーザーからの質問事項に対して、関連するデータを返すことが可能です。
この時単なるテキスト検索も可能ですし、ベクトル検索やセマンティック検索といった高度な検索も行うことが可能です。
クエリの実行に関しは、弊社武井が分かりみ深く紹介しているので、こちらを参照ください!
(そのため本記事ではスコープ外とします。)
生成AI時代の様々な検索手法を検証する 〜Azure AI Searchによるベクトル/セマンティック/ハイブリッド検索〜
インデクシングとは
MSのドキュメントを確認すると、以下のように定義されています。
検索インデックスを設定するテキストのコンテンツとトークンを取り込み、解析し、格納することを指します。 インデックス作成により、情報取得をサポートする逆インデックスやその他の物理的なデータ構造が作成されます。 スキーマにベクター フィールドが含まれている場合は、ベクトル インデックスが作成されます。
ちなみにこの「Indexing」という単語ですが、日本語だと「インデクシング」と「インデックスの作成」と訳されることがあります。
また、他にも「インデクサー」という表現もあり、やや混乱しがちです。
本記事では以下のように使い分けたいと思います。
インデックス | コンテンツを格納する場所(データ構造) |
インデックス作成 | Azure AI Search上にインデックスを作成する事 |
インデクシング | 作成されたインデックスに対してデータを投入する事 |
インデクサー | インデクサーを行うアプリケーション |
インデックスのスキーマ定義
まずはインデックスの、おもにスキーマの設計について見ていきたいと思います。
いわゆるインデックスのデータ構造を決める部分になります。
スキーマに関しては、公式のドキュメントに以下のようなjsonの形で定義されていました。
{
"name": "name_of_index, unique across the service",
"fields": [
{
"name": "name_of_field",
"type": "Edm.String | Collection(Edm.String) | Collection(Edm.Single) | Edm.Int32 | Edm.Int64 | Edm.Double | Edm.Boolean | Edm.DateTimeOffset | Edm.GeographyPoint",
"searchable": true (default where applicable) | false (only Edm.String and Collection(Edm.String) fields can be searchable),
"filterable": true (default) | false,
"sortable": true (default where applicable) | false (Collection(Edm.String) fields cannot be sortable),
"facetable": true (default where applicable) | false (Edm.GeographyPoint fields cannot be facetable),
"key": true | false (default, only Edm.String fields can be keys),
"retrievable": true (default) | false,
"analyzer": "name_of_analyzer_for_search_and_indexing", (only if 'searchAnalyzer' and 'indexAnalyzer' are not set)
"searchAnalyzer": "name_of_search_analyzer", (only if 'indexAnalyzer' is set and 'analyzer' is not set)
"indexAnalyzer": "name_of_indexing_analyzer", (only if 'searchAnalyzer' is set and 'analyzer' is not set)
"normalizer": "name_of_normalizer", (applies to fields that are filterable)
"synonymMaps": "name_of_synonym_map", (optional, only one synonym map per field is currently supported)
"dimensions": "number of dimensions used by an emedding models", (applies to vector fields only, of type Collection(Edm.Single))
"vectorSearchProfile": "name_of_vector_profile" (indexes can have many configurations, a field can use just one)
}
],
"suggesters": [ ],
"scoringProfiles": [ ],
"analyzers":(optional)[ ... ],
"charFilters":(optional)[ ... ],
"tokenizers":(optional)[ ... ],
"tokenFilters":(optional)[ ... ],
"defaultScoringProfile": (optional) "...",
"corsOptions": (optional) { },
"encryptionKey":(optional){ },
"semantic":(optional){ },
"vectorSearch":(optional){ }
}
一部抽出して図に起こすと以下のような形となります。
イメージとしてはRDBMSのテーブル設計にも近いのかな、と個人的には思っています。
name
がテーブル名、fields
が各カラムに対応している感じです。
RDBMSでも各カラムに対して型の定義をしたり、nullableに設定したりできますよね。
AI Searchはこれに加えて、「検索対象とするかどうか」などの検索に特化したパラメータを持っているような形となります。
スキーマ内容
このインデックスを構成するための値について、「何となく理解できる」レベルを目標に、表にまとめてみました。
一応わかりやすさのため「RDBMSでいうと?」という項目を付けてますが、あくまでイメージをつかむ程度で読んでください。
おそらくですがRDBMSのテーブル設計とAI Searchのスキーマ設計では考え方が異なる部分が多いかと思います。
key | velue | RDMBSでいうと? |
---|---|---|
name | インデックス名です。 | テーブル名 |
fields | インデックスの主役です。 データを投入するにあたって、データの形式などの詳細を決定します | カラム |
fields.name | フィールドの名称です。 | カラム名 |
fields.type | 投入するデータの型. Edm.StringやEdm.Singleなどがあります。 ベクトル値を入れる際はCollection[Edm.single]を利用します。 |
カラムの型 |
fields.searchable | 全文またはベクトル検索を可能とするか否かを設定します。 | |
fields.filterable | 検索時の$filterクエリに対応するか否かを設定します。 | |
fields.sortable | 検索結果の並べ替え可能か否かを設定します。
デフォルトではAI Searchで算出されたスコアが高い順に並び替えられますが、 |
|
fields.facetable | フィールドの値での集約などが行えます。 例えば商品のデータをAI Searchに登録する場合、 fields.nameをcategoryしておき、「food」「clothes」などの値を登録します。 すると検索結果として、「food」のデータが何件、「clothes」のデータが何件、 |
|
fields.key | 各フィールドを一意に定めるための値。 typeとしてはEdm.Stringである必要があります |
主キー(Primary Key) |
fields.retrievable | 検索結果として値を返すか否かを設定します。
「FilterやSortの対象としては利用したいが、ユーザーには値を返したくない!」 |
|
fields.analyzer | テキストをトークンに変換するためのアナライザの設定です。
文章を形態素解析する際に利用されます。 |
|
fields.searchAnalyzer | 上記のanalyzerはインデクシングとクエリ検索の場合に同じAnalyzerを使いますが、 それらを分けたい場合に個別に設定することも可能です |
|
fields.indexAnalyzer | 同上 | |
fields.normalizer | Normalizerは、filterableやsortableとして指定されたフィールドに対する検索テキストを事前に処理する機能です。
処理内容としては、例えば検索テキストをlowercaseに変換したりといった内容で、表記揺れなどに対応可能です。 |
|
fields.synonymMaps | 類義語をまとめたsynonymMapを指定します。
例えば「にわか雨」「夕立」「ゲリラ豪雨」といった言葉はどれも似たような事象を示しています。 |
|
fields.dimensions | ベクトル化のデータの次元数になります。 | |
fields.vectorSearchProfile | ベクトル化をする際に行うプロファイルを指定します(後述) | |
suggesters | オートコンプリートやサジェストの入力に使用するフィールドを指定します。 | |
scoringProfiles | スコア付けをする際に、各フィールドに対して重みづけを行うことができます。
例えば「メニュー」というフィールドを重視するように設定した場合、 |
|
analyzers | fields.analyzerでカスタムアナライザー利用する場合は、ここでカスタムアナライザーの内容を設定します | |
corsOptions | CORSの設定を行います | |
encryptionKey | インデックス内のデータを二重で暗号化します。 | |
semantic | セマンティックランカーの構成パラメータの指定を行います(後述) | |
vectorSearch | ベクトル検索のためのアルゴリズムおよびパラメータの指定を行います(後述) |
Fieldに中身に着目すると、searchableやfilterable, sortableなど、RDBMSではクエリを工夫して実行している部分を
AI Searchではインデックス定義の時に細かく定義していることが分かります。
恐らくこれが膨大なデータから正確かつ高速に検索するためのポイントなのかと思います。
それだけAI Searchにおいてスキーマ設計が重要である、という事もうかがえます。
ここで、semantic
とvectorSearch
に関しては重要なパラメータであるので、もう少し深堀してみてみたいと思います。
semantic
公式のドキュメントにsemanticの値のサンプルがありました。
"semantic": {
"defaultConfiguration": "my-semantic-config-default",
"configurations": [
{
"name": "my-semantic-config-default",
"prioritizedFields": {
"titleField": {
"fieldName": "HotelName"
},
"prioritizedContentFields": [
{
"fieldName": "Description"
}
],
"prioritizedKeywordsFields": [
{
"fieldName": "Tags"
}
]
}
},
{
"name": "my-semantic-config-desc-only",
"prioritizedFields": {
"prioritizedContentFields": [
{
"fieldName": "Description"
}
]
}
}
]
}
大きな流れとしては、
- configurationsの配列の中で各セマンティックランカーの設定を行い
- それらを検索時に指定して利用する形になります。
各値の説明としてはこちらに書いてありました。
今回設定する項目としては以下3つです。
- titleField
- prioritizedContentFields
- prioritizedKeywordsFields
それぞれのパラメータ(Title, Content, Keyword)として、どのフィールドを重視するかを設定します。
最低限prioritizedContentFieldsのみ設定されていればOKです。
これらの設定によって、セマンティックのランク付けの精度が変わってきます。
vectorSearch
次にvectorSearchの内容についてもみていきます。
こちらも公式のサンプルを載せます。
"vectorSearch": {
"algorithms": [
{
"name": "my-hnsw-config-1",
"kind": "hnsw",
"hnswParameters": {
"m": 4,
"efConstruction": 400,
"efSearch": 500,
"metric": "cosine"
}
},
{
"name": "my-hnsw-config-2",
"kind": "hnsw",
"hnswParameters": {
"m": 8,
"efConstruction": 800,
"efSearch": 800,
"metric": "cosine"
}
},
{
"name": "my-eknn-config",
"kind": "exhaustiveKnn",
"exhaustiveKnnParameters": {
"metric": "cosine"
}
}
],
"profiles": [
{
"name": "my-default-vector-profile",
"algorithm": "my-hnsw-config-2"
}
]
}
大きく分けてalgorithmsとprofilesの2つの値があります。
algorithmsとして、ベクトル検索に利用するアルゴリズムの詳細を定義します。
次に、profileの値としてnameと、使用するalgorithmを設定し、
nameの値(my-default-vector-profile)の値を、fields.vectorSearchProfile
に設定します。
"fields": [
{
"vectorSearchProfile" = "my-default-vector-profile"
}
]
今回、ベクトル検索のアルゴリズム内容の細かい設定に関しては割愛させていただきます。
インデックスの作成方法
次に、非常にシンプルなインデックスを実際に作成してみたいと思います。
作成に関してはRESTや各種SDKで作成可能です。
今回はRESTを使っていきたいと思います。
RESTの場合は、先程定義したjsonをbodyに入れて実行すればOKです。
今回は簡略化のためAPI Keyを利用して実行します。
※今回、検索時の検証のためTagsのフィールドに関してはsearchable
はfaleseとしています。
POST /indexes?api-version=2024-07-01 HTTP/1.1
Host: {{resourceName}}.search.windows.net
Content-Type: application/json
api-key: {{apyKey}}
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": "Age",
"type": "Edm.Int32",
"searchable": false,
"filterable": true,
"sortable": true,
"facetable": true
},
{
"name": "Tags",
"type": "Collection(Edm.String)",
"searchable": false, //falseとする
"filterable": true,
"sortable": false,
"facetable": true
}
]
}
Azure Portal上からみると、正しく生成できていることが確認できました。
なお、もちろんAzure Portal上からでもインデックスの作成は可能です。
GUIベースでインデックスの作成や、フィールドの追加が行えます。
注意ポイント
感覚的にも分かることですが、 多くのフィールドを追加したり、フィールドに対するパラメータを複雑に設定すると
インデックス作成の速度が低下します。 インデックスが小さいほど、インデックス作成は速くなります。
また、同様にパラメータを複雑にすることにより、格納するデータ量が増え、ストレージを圧迫するといったことも起こります。
パフォーマンスや、コスト影響する部分なので、このあたり気にしながら設計が必要かと思います。
インデクシング
定義したインデックスに対してデータを投入していきます。
データの投入方法としては、プッシュ型とプル型の2種類があります。
今回はプッシュ型を例に挙げてデータの投入を行っていきます。
先程作成したインデックスに対して、ダミーのデータを10件ほど用意します。
ChatGPTさんに、インデックス作成時に利用したbodyの内容を提示して、サンプルデータを作ってもらいました。
地味にこういう時に生成AIの便利さを痛感しますね。
{
"UserId": "1001",
"UserName": "田中 一郎",
"Profile": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き",
"Age": 36,
"Tags": ["Engineer", "Azure"]
},
....
これらのサンプルをアップロードしていきます。
注意点としては、
- リクエストのパスパラメータとして対象のインデックス名を入れる
- bodyのパラメータに
@search.action
を追加する
の2点です。
POST /indexes/idx-users/docs/index?api-version=2024-07-01 HTTP/1.1
Host: {{resourceName}}.search.windows.net
Content-Type: application/json
api-key: {{apiKey}}
Content-Length: 3518
{
"value": [
{
"@search.action": "upload",
"UserId": "1001",
"UserName": "田中 一郎",
"Profile": "東京都出身で大学卒業後に大手SIerに就職。Azureを使ったアプリケーションの開発に携わり、Azure Developer Associateの資格も取得している。趣味は本を読むことで、無類のミステリー好き",
"Age": 36,
"Tags": [
"Engineer",
"Azure"
]
},
{
"@search.action": "upload",
"UserId": "1002",
"UserName": "鈴木 花子",
"Profile": "神奈川県出身。デザイン専門学校を卒業後、デザイナーとして活動。UI/UXデザインに興味があり、最近はフリーランスとして様々なプロジェクトに参加。趣味は絵を描くこと",
"Age": 28,
"Tags": [
"Designer",
"Freelance"
]
},
{
"@search.action": "upload",
"UserId": "1003",
"UserName": "佐藤 次郎",
"Profile": "大阪府出身。マーケティングマネージャーとして多くの企業で経験を積む。デジタルマーケティングに精通しており、現在は自社のマーケティング戦略を担当。趣味はゴルフ",
"Age": 42,
"Tags": [
"Marketing",
"Manager"
]
},
{
"@search.action": "upload",
"UserId": "1004",
"UserName": "山田 真美",
"Profile": "福岡県出身。大学卒業後、プロジェクトマネージャーとして多くのプロジェクトを成功に導く。現在はIT企業でPMとして活躍。趣味は旅行で、特に海外旅行が好き",
"Age": 33,
"Tags": [
"Project Manager",
"IT"
]
},
{
"@search.action": "upload",
"UserId": "1005",
"UserName": "中村 健",
"Profile": "北海道出身。大学で情報工学を学び、現在は札幌のスタートアップ企業でエンジニアとして働く。クラウド技術に強く、AWSやAzureの資格を多数保有。趣味はキャンプ",
"Age": 29,
"Tags": [
"Engineer",
"Cloud"
]
},
{
"@search.action": "upload",
"UserId": "1006",
"UserName": "伊藤 美咲",
"Profile": "名古屋市出身。大学で経営学を学び、現在は企業コンサルタントとして働く。経営戦略や財務分析に強く、多くの企業をサポート。趣味は読書で、特にビジネス書が好き",
"Age": 37,
"Tags": [
"Consultant",
"Business"
]
},
{
"@search.action": "upload",
"UserId": "1007",
"UserName": "小林 隆",
"Profile": "千葉県出身。大学卒業後、大手メーカーでエンジニアとしてキャリアをスタート。現在はチームリーダーとして多くのプロジェクトを指揮。趣味は釣り",
"Age": 45,
"Tags": [
"Engineer",
"Leader"
]
},
{
"@search.action": "upload",
"UserId": "1008",
"UserName": "松本 由美",
"Profile": "京都府出身。大学で心理学を学び、現在は人事部門で働く。社員のメンタルヘルスケアに力を入れており、多くの研修を実施。趣味はヨガ",
"Age": 31,
"Tags": [
"HR",
"Psychology"
]
},
{
"@search.action": "upload",
"UserId": "1009",
"UserName": "高橋 大輔",
"Profile": "福島県出身。大学で化学を専攻し、現在は研究所で働く。新素材の研究に従事しており、多くの論文を発表。趣味は山登り",
"Age": 39,
"Tags": [
"Researcher",
"Chemistry"
]
},
{
"@search.action": "upload",
"UserId": "1010",
"UserName": "森 美佳",
"Profile": "広島県出身。大学で文学を学び、現在は出版社で編集者として働く。多くのベストセラーを手がけ、業界で高い評価を受ける。趣味は料理",
"Age": 34,
"Tags": [
"Editor",
"Literature"
]
}
]
}
実行後しばらくすると、10件のデータが投入されていることが確認できます。
なお今回、ベクトルデータは入れてないのでベクトルインデックスのサイズは0のままです。
せっかくなので、このインデックスに対して検索をしてみたいと思います。
例えば「広島」と入れて検索すると、Profileに広島が入っているドキュメントが抽出されます。
また、先程searchable
をfalseとしたTagsの情報で検索すると、ヒットしないことが分かります。
ただ、今回はベクトル検索をしているわけでもないので、特に検索の真新しさはないですね…。
今回の記事はあくまでインデクシングにフォーカスしているので、
また検索周りについては調査して記事にしていきたいと思います。
まとめ
今回はAzure AI Searchに対してインデックスを作成し、実際にデータを導入する流れを解説しました。
インデックスのスキーマ設計に関しては、非常に詳細な部分まで設定が可能で、ここがAzure AI Searchの性能を決める肝かと思いました。
RDBMSでもテーブル設計は非常に重要とされていますが、AI Searchのスキーマ設計はそれ以上に気を付けて行わなければいけ名ように感じます。
そのためにも、しっかりと投入元のデータの性質などに関して理解を深める必要がありそうですね。
また、今回はインデクシングに関して概要をさらっただけであり、まだまだ機能は多く存在しています。
- ベクトル検索のためのスキーマ設計
- セマンティック検索のためのスキーマ設計
- プル型のデータ投入
- スキルセット
そのあたりにもまた触れて、しっかりと理解していきたいと思います。
ではまた!
余談
AI Searchはよく価格が高い事が言われてます。
実際ちょっとお高めだよなぁ、とは思います。
確かに、そのぶん高機能なのは今回勉強してみて感じましたが…。
一方で、最近CosmosDBがベクトル検索に対応したことで非常に盛り上がっており
シンプルなRAGの構築するくらいなら、CosmosDBでもいいんじゃないか?という話も出てます。
ということでAI Searchのキャッチアップと並行して、CosmosDBを使った場合の検索についてもまた検討していきたいと思います。
参考資料
Azure AI Search ドキュメント – Microsoft
基本概念から理解するAzure AI Search – Azure OpenAI Serviceとの連携まで
Azure Cognitive Search で日本語全文検索をするためのアナライザー実装メモ
生成AI時代の様々な検索手法を検証する 〜Azure AI Searchによるベクトル/セマンティック/ハイブリッド検索〜