ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure Cognitive Search のセマンティック検索を .NET アプリケーションから使ってみた

まだプレビューですが Azure Cognitive Search にセマンティック検索機能が提供されました。前回の記事ではベクトル検索を試したので、今回はセマンティック検索を試してみました。
gooner.hateblo.jp

セマンティック検索とは

従来のキーワードによる全文検索とは異なり、質問の意図を理解して関連性の高い検索結果を返すこと(Semantic re-ranking)ができます。
そのほかにも、下記のような機能があります。

  • Semantic answers
    • 検索結果に関連するドキュメントのほかに、質問に対する回答を最上位に表示する。
    • Google やBing で検索した際にトップに表示されるアレです。

  • Semantic captions and highlights
    • 検索結果に関連性がある理由を知らせるために、要約を表示したりキーワードを強調表示したりする。
    • Google やBing の検索結果の表示形式のことです。


なかなか説明が難しい機能なので、こちらの記事が分かりやすいです。
qiita.com

セマンティック検索に対応したインデックスを作成する

前回の記事で作成した .NET アプリケーションにセマンティック検索を追加します。
まずは、Azure ポータルで Cognitive Search のセマンティック検索を有効化します。

セマンティック検索に対応したインデックスを作成します。SemanticSettings プロパティを追加し、titlecontentcategory のフィールドを設定しました。

static SearchIndex CreateSearchIndex(string indexName)
{
    var vectorSearchConfigName = "vector-config";
    var modelDimensions = 1536;

    return new SearchIndex(indexName)
    {
        VectorSearch = new VectorSearch
        {
            AlgorithmConfigurations =
            {
                new HnswVectorSearchAlgorithmConfiguration(vectorSearchConfigName)
            }
        },
        SemanticSettings = new SemanticSettings
        {
            Configurations =
            {
                new SemanticConfiguration(SemanticSearchConfigName,
                    new PrioritizedFields
                    {
                        TitleField = new SemanticField{ FieldName = "title" },
                        ContentFields =
                        {
                            new SemanticField { FieldName = "content" }
                        },
                        KeywordFields =
                        {
                            new SemanticField { FieldName = "category" }
                        }
                    })
            },
        },
        Fields =
        {
            new SimpleField("id", SearchFieldDataType.String) { IsKey = true, IsFilterable = true, IsSortable = true, IsFacetable = true },
            new SearchableField("title") { IsFilterable = true, IsSortable = true, AnalyzerName = LexicalAnalyzerName.JaLucene },
            new SearchableField("content") { IsFilterable = true, AnalyzerName = LexicalAnalyzerName.JaLucene },
            new SearchField("contentVector", SearchFieldDataType.Collection(SearchFieldDataType.Single))
            {
                IsSearchable = true,
                VectorSearchDimensions = modelDimensions,
                VectorSearchConfiguration = vectorSearchConfigName
            },
            new SearchableField("category") { IsFilterable = true, IsSortable = true, IsFacetable = true, AnalyzerName = LexicalAnalyzerName.JaLucene }
        }
    };
}

コードを実行すると、インデックスにセマンティック検索の構成が設定されていることを確認できます。


セマンティック検索を使ってみる

セマンティック検索を実行する SemanticSearchAsync メソッドを作成します。ベクトル検索の処理に加えて、searchOptions プロパティでセマンティック検索の設定を実装しています。前回と同様の検索結果に加えて、セマンティック アンサーを表示する実装を追加しています。

static async Task SemanticSearchAsync(SearchClient searchClient, OpenAIClient openAIClient, string deploymentOrModelName, string query)
{
    try
    {
        var queryEmbeddings = await GetEmbeddingsAsync(deploymentOrModelName, query, openAIClient);

        var searchOptions = new SearchOptions
        {
            Vectors = { new SearchQueryVector { Value = queryEmbeddings.ToArray(), KNearestNeighborsCount = 3, Fields = { "contentVector" } } },
            Size = 10,
            QueryType = SearchQueryType.Semantic,
            QueryLanguage = QueryLanguage.JaJp,
            SemanticConfigurationName = SemanticSearchConfigName,
            QueryCaption = QueryCaptionType.Extractive,
            QueryAnswer = QueryAnswerType.Extractive,
            QueryCaptionHighlightEnabled = true,
            Select = { "title", "content", "category" },
        };

        SearchResults<SearchDocument> response = await searchClient.SearchAsync<SearchDocument>(query, searchOptions);

        int count = 0;
        Console.WriteLine("Semantic Search Results:\n");

        Console.WriteLine("Semantic Answer:");
        foreach (AnswerResult result in response.Answers)
        {
            Console.WriteLine($"Answer Highlights: {result.Highlights}");
            Console.WriteLine($"Answer Text: {result.Text}\n");
        }

        await foreach (SearchResult<SearchDocument> result in response.GetResultsAsync())
        {
            count++;
            Console.WriteLine($"Title: {result.Document["title"]}");
            Console.WriteLine($"Score: {result.Score}");
            Console.WriteLine($"Content: {result.Document["content"]}");
            Console.WriteLine($"Category: {result.Document["category"]}");

            if (result.Captions != null)
            {
                var caption = result.Captions.FirstOrDefault();
                if (caption != null)
                {
                    if (!string.IsNullOrEmpty(caption.Highlights))
                    {
                        Console.WriteLine($"Caption Highlights: {caption.Highlights}\n");
                    }
                    else
                    {
                        Console.WriteLine($"Caption Text: {caption.Text}\n");
                    }
                }
            }
        }
        Console.WriteLine($"Total Results: {count}");
    }
    catch (NullReferenceException)
    {
        Console.WriteLine("Total Results: 0");
    }
}

セマンティック検索のメソッドを呼び出すコードを実装します。

var inputQuery = "暑い日に食べられている京菓子は?";
Console.WriteLine($"Query: {inputQuery}\n");

Console.WriteLine("Choose a query approach:");
Console.WriteLine("1. Vector Search");
Console.WriteLine("2. Keyword Search");
Console.WriteLine("3. Hybrid Search");
Console.WriteLine("4. Semantic Search");
Console.Write("Enter the number of the desired approach: ");
var choice = int.Parse(Console.ReadLine() ?? "0");
Console.WriteLine("");

switch (choice)
{
    case 1:
        await VectorSearchAsync(searchClient, openAIClient, settings.DeploymentOrModelName, inputQuery);
        break;
    case 2:
        await KeywordSearchAsync(searchClient, inputQuery);
        break;
    case 3:
        await HybridSearchAsync(searchClient, openAIClient, settings.DeploymentOrModelName, inputQuery);
        break;
    case 4:
        await SemanticSearchAsync(searchClient, openAIClient, settings.DeploymentOrModelName, inputQuery);
        break;
    default:
        Console.WriteLine("Invalid choice. Exiting...");
        break;
}

前回と同様に、暑い日に食べられている京菓子を検索してみます。

ベクトル検索と同様に若鮎がトップに来ており、Caption Text に回答の要約が追加されています。一方、Semantic Answer が空で返されているのは、明確な回答を求めるような質問ではなかったことが原因だと思われます。

「若鮎とはどのような京菓子ですか?」という内容に、質問を変えてみます。

今度は、Answer HighlightsAnswer Text が返され、Caption Highlights も追加されています。
さらに、質問に対する回答に最も近い部分となる「若鮎は、カステラ生地で求肥を包んだ和菓子で、鮎の形を模しています。」には、em タグによるハイライトで強調表示されています。

まとめ

Azure Cognitive Search のセマンティック検索を .NET アプリケーションから使ってみました。
セマンティック検索では、これまでの Cognitive Search による検索では実現できなかった Google やBing のような検索ができます。このような形で社内ドキュメントからナレッジを検索できたら便利そうだと思いました。

今回のサンプルアプリは、こちらで公開しています。
github.com