個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

C# だけで Azure OpenAI の RAG を試してみたい(目次)

C# だけで Azure OpenAI の RAG を試してみたい (1)検討したシステム構成と処理フロー

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (2)C# によるテキストのクレンジング・チャンクの方針

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (3)TokenCounter の実装

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (4)html ファイルのクレンジング

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (5)Blob コンテナ上でのソースデータの更新日時の管理

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (6)AI Search の検索インデックス作成

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (7)インデックスへのドキュメント登録

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (8)「データの追加」とプレイグラウンドでの動作確認

記事はこちらです。
hiro128.hatenablog.jp
 

C# だけで Azure OpenAI の RAG を試してみたい (9)デフォルトのチャットアプリをデプロイしてみる

記事はこちらです。
hiro128.hatenablog.jp

 

C# だけで Azure OpenAI の RAG を試してみたい (9)デフォルトのチャットアプリをデプロイしてみる

 
前の記事
hiro128.hatenablog.jp
 
前回の記事でプレイグラウンドで動作確認をしましたが、プレイグラウンドは公開できませんので、例えば Web アプリとして公開するというような対応が必要です。

これも手動でやると設定がいろいろ面倒ですが、Azure OpenAI Studio には Web アプリとしての公開メニューが準備されており、ボタンひとつ押していくつかの設定項目を入力すれば数分で Web アプリが公開できるようになっています。
 

①Web アプリをデプロイするボタンを押下

「配置先」-> 「新しい Web アプリ」でデプロイが開始されます。

 
 

②デプロイ設定の入力


 
 

③デプロイが進行

数分でデプロイされます。

 
 

④デプロイが完了したら、アプリにアクセスする

②で記載があったように Entra ID 認証が有効になっているので、

 
初回に認証の確認が表示されます。承諾して問題ありません。

 

⑤動作確認

自分で登録したドキュメントの内容に関する質問を投げて回答が想定通りに出れば成功です。

 

C# だけで Azure OpenAI の RAG を試してみたい (8)「データの追加」とプレイグラウンドでの動作確認

 
前の記事
hiro128.hatenablog.jp
 

データの追加

チャットアプリがこれまでの手順で自動で登録されるようになったインデックスのドキュメントを含めて検索するように、Azure OpenAI Studio で「データの追加」を行います。
 
「データの追加」はノーコードでできるようになっているので、これまで述べた手順のように自分で Azure AI Search にインデックスのドキュメントを登録するようにしなくとも単純にデータソースで Blob を選択すれば、データソースで Blob に HTML をアップロードするだけで「データの追加」は利用できるようになり、自分で登録したインデックスのドキュメントを含めた検索も可能になります。

ただし、「Azure AI Search」を選択した時のみ、「インデックスデータフィールドのマッピング」が設定できる(それ以外を選択した場合、マッピングの設定画面自体が表示されない)。という制限があるため、自分で Azure AI Search にインデックスのドキュメントを登録するようにした方が、カスタマイズの幅が広がります。そのため、本記事では自前で Azure AI Search にインデックスのドキュメントを登録するようにしています。

あとは以下の手順の通りに設定をします。
 

「データの追加」を開始する


 

データソースの選択

この画面で「Azure AI Search」を選択した時のみ、次の「インデックスデータフィールドのマッピング」設定画面自体が表示され、設定ができる。

 

「インデックスデータフィールドのマッピング」の設定


 

「データの管理」の設定


 

「設定のレビュー」行い完了させる


 

プレイグランドでの動作確認

これで、自分で登録したインデックスのドキュメントを含めた検索も可能になりました。
確認のため、プレイグラウンドで自分で追加したデータソースにもとづく内容を検索して、データソースをもとにした回答を返してくれば成功です。

 
次の記事
hiro128.hatenablog.jp

C# だけで Azure OpenAI の RAG を試してみたい (7)インデックスへのドキュメント登録

 
前の記事
hiro128.hatenablog.jp
 

ドキュメントの登録

作成したソースコードの詳細は以下を参照ください。
github.com
 

Embedding の取得

チャンクしたコンテンツから Embedding (1536次元の数値ベクトル表現)を取得します。

var embedings = await GenerateEmbeddingsAsync(openAIClient, openAIEmbeddingsDeproymentName, chunk);

openAIEmbeddingsDeproymentName は Embedding のデプロイ名です。Azure AI Studio で確認できます。

 

ドキュメントのアップロード

SearchDocument オブジェクトを生成してアップロードすることでインデックスにドキュメントを登録できます。
アップロード済みドキュメントがあれば削除してからアップロードするようにしています。

var searchClient = searchIndexClient.GetSearchClient(aiSerchIndexeName);
await DeleteDocumentByUrlAsync(searchClient, url);
await searchClient.UploadDocumentsAsync(documents);

 

ドキュメントの削除時の注意点

ドキュメントは、key 属性が設定されたフィールドの値指定でしか削除できないので、特定の記事のドキュメントを url フィールドで検索してドキュメントを取得し、 id フィールドの値を取得して、DeleteDocumentsAsyncに指定して削除します。

 

        // url フィールドでインデックスデータを取得し、id フィールドで削除する
        async Task DeleteDocumentByUrlAsync(SearchClient searchClient, string url)
        {
            SearchOptions options = new SearchOptions()
            {
                Filter = $"url eq '{url}'",
                Size = 100
            };

            var response = searchClient.Search<SearchDocument>("*", options);

            foreach (var result in response.Value.GetResults())
            {
                var id = result.Document["id"].ToString() ?? string.Empty;
                await searchClient.DeleteDocumentsAsync("id", new List<string>() { id });
            }
        }

 

ソースコード全体

作成したソースコードの詳細は以下を参照ください。
github.com
 
主要な処理部分は以下の通りです。

            BobyContentChunker chunker = new();
            var chunks = chunker.ChunkText(bodyText);

            List<SearchDocument> documents = new();
            foreach (var (chunk, index) in chunks.Select((chunk, index) => (chunk, index)))
            {
                var id = Guid.NewGuid().ToString();
                var lastUpdatedText = DateTimeOffset.Now.ToString();
                var embedings = await GenerateEmbeddingsAsync(openAIClient, openAIEmbeddingsDeproymentName, chunk);
                
                documents.Add(new SearchDocument
                {
                    ["id"] = id,
                    ["lastUpdated"] = lastUpdatedText,
                    ["content"] = chunk,
                    ["title"] = title,
                    ["filePath"] = url,
                    ["url"] = url,
                    ["metadata"] = "{\"chunk_id\": \"" + id + "\"}",
                    ["contentVector"] = embedings
                });

                //Console.WriteLine($"Uploading documents : {(index + 1).ToString("000000")}/{chunks.Count.ToString("000000")}");
            }

            var searchClient = searchIndexClient.GetSearchClient(aiSerchIndexeName);
            await DeleteDocumentByUrlAsync(searchClient, url);
            await searchClient.UploadDocumentsAsync(documents);
        }

 
ここまでで、「はてなブログの記事を関数アプリで10分ごとにクロールし、Blob に html として保存し、Blob にアイテムが追加されると、Event Grid の Blob Trigger の関数アプリで AI Search にベクトルを含むドキュメントを登録する」アプリが完成しましたので、チャットがこのインデックスの内容を含んだ回答を返してくれるか確認しましょう。

 
次の記事
hiro128.hatenablog.jp

C# だけで Azure OpenAI の RAG を試してみたい (6)AI Search の検索インデックス作成

 
前の記事
hiro128.hatenablog.jp
 

AI Search の検索インデックス作成

クロールしてクレンジングしたはてなブログ記事のドキュメントを登録する AI Search のインデックスを一括で作成するコンソールアプリを準備しました。
github.com
 
なお、AI Search の検索インデックスは RDBMS の世界に例えれば

  • 検索インデックス = RDBMS のテーブル
  • ドキュメント = RDBMS のテーブルにおける行

と理解いただければ、イメージしやすいと思います。
 
検索インデックスのスキーマは以下のように C# のコードで記述できます。
この SDK は頻繁に更新されるので文法が変わっている可能性があります。最新情報を必ずご確認下さい!

            var semanticConfigName = "defaultSemanticConfig";
            var vectorConfigName = "defaultVectorConfigName";
            var algorithmConfigName = "hnsw";

            var vectorSearch = new VectorSearch();
            vectorSearch.Algorithms.Add(new HnswAlgorithmConfiguration(algorithmConfigName));
            vectorSearch.Profiles.Add(new VectorSearchProfile(vectorConfigName, algorithmConfigName));

            var semanticPrioritizedFields = new SemanticPrioritizedFields()
            {
                TitleField = new SemanticField("title"),
            };
            semanticPrioritizedFields.ContentFields.Add(new SemanticField("content"));
            // TODO : Add prioritizedKeywordsFields

            var semanticSearch = new SemanticSearch();
            semanticSearch.Configurations.Add(new SemanticConfiguration(semanticConfigName, semanticPrioritizedFields));

            var defaulIindex = new SearchIndex(indexName)
            {
                VectorSearch = vectorSearch,
                SemanticSearch = semanticSearch,
                Fields =
                {
                    new SearchField("id", SearchFieldDataType.String)
                    {
                        IsKey = true,
                        IsSearchable = true
                    },
                    new SearchField("lastUpdated", SearchFieldDataType.String)
                    {
                        IsSearchable = true,
                        IsFacetable = false,
                    },
                    new SearchField("content", SearchFieldDataType.String)
                    {
                        IsSearchable = true,
                        IsSortable = false,
                        IsFacetable = false,
                        IsFilterable = false,
                        AnalyzerName = LexicalAnalyzerName.JaMicrosoft
                    },
                    new SearchField("title", SearchFieldDataType.String)
                    {
                        IsSearchable = true,
                        IsSortable = false,
                        IsFacetable = false,
                        IsFilterable = false,
                        AnalyzerName = LexicalAnalyzerName.JaMicrosoft
                    },
                    new SearchField("filePath", SearchFieldDataType.String)
                    {
                        IsSearchable = true,
                        IsSortable = false,
                        IsFacetable = false,
                        IsFilterable = false
                    },
                    new SearchField("url", SearchFieldDataType.String)
                    {
                        IsSearchable = true
                    },
                    new SearchField("metadata", SearchFieldDataType.String)
                    {
                        IsSearchable = true
                    },
                    new SearchField("contentVector", SearchFieldDataType.Collection(SearchFieldDataType.Single))
                    {
                        IsSearchable = true,
                        VectorSearchDimensions = 1536,
                        VectorSearchProfileName = vectorConfigName
                    },
                },
            };

 

フィールド

このアプリで作成されるフィールドは以下のようになります。

 
各フィールド定義の詳細は以下の公式ドキュメントを参照ください。
learn.microsoft.com
 

セマンティック構成

このアプリで作成されるセマンティック構成は以下のようになります。

 

ベクタープロファイル

このアプリで作成されるベクタープロファイルは以下のようになります。

  
次の記事
hiro128.hatenablog.jp