LoginSignup
3
2

More than 3 years have passed since last update.

[Azure Search] で高速テキスト検索

Last updated at Posted at 2019-12-31

今日はテキスト検索サービスAzure Searchを導入してみます。

Azure Search は、あらゆる種類の情報を拡充して関連コンテンツを簡単に特定し、大規模に検索する人工知能 (AI) 機能が組み込まれた唯一のクラウド検索サービスです。Bing と Office にも 10 年以上にわたって利用されている Microsoft の自然言語スタック、および視覚、言語、音声の事前構築済み AI API が使用されます。イノベーションに費やす時間を増やし、複雑なクラウド検索ソリューションの保守に費やす時間を減らすことができます。

とのことです。 正直Bingのシェアは低いのですが、Azure Searchに関しては Readヘビーなサービスには必要不可欠なサービスとなっています。

サンプル

サンプルコードはこちらを参照

https://github.com/Azure-Samples/search-dotnet-getting-started

Azure Search SDK for C

C#で利用する場合は下記のライブラリーを利用します。

https://docs.microsoft.com/en-us/dotnet/api/overview/azure/search?view=azure-dotnet

Install-Package Microsoft.Azure.Search

サーチのサービス単位はIndexという単位になります。下記がIndexの取得方法です。

using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;

// A service endpoint and an api-key are required on a connection.
// Set them in a config file (not shown) and then connect to the client.
IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
IConfigurationRoot configuration = builder.Build();

SearchServiceClient serviceClient = CreateSearchServiceClient(configuration);

// Create an index named hotels
ISearchIndexClient indexClient = serviceClient.Indexes.GetClient("hotels");

appsettings.jsonに必要な情報を入力。appsettings.jsonの使い方はこちらを参照。
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-2.1&tabs=basicconfiguration

必要なAPI情報はポータルより入手してください。

ポータルより必要な情報入手

image.png

Search用のモデル作成

Searchに使うモデルを構築します。


namespace AzureSearch.SDKHowTo
{
    using System;
    using Microsoft.Azure.Search;
    using Microsoft.Azure.Search.Models;
    using Microsoft.Spatial;
    using Newtonsoft.Json;

    public partial class Hotel
    {
        [System.ComponentModel.DataAnnotations.Key]
        [IsFilterable]
        public string HotelId { get; set; }

        [IsSearchable, IsSortable]
        public string HotelName { get; set; }

        [IsSearchable]
        [Analyzer(AnalyzerName.AsString.EnLucene)]
        public string Description { get; set; }

        [IsSearchable]
        [Analyzer(AnalyzerName.AsString.FrLucene)]
        [JsonProperty("Description_fr")]
        public string DescriptionFr { get; set; }

        [IsSearchable, IsFilterable, IsSortable, IsFacetable]
        public string Category { get; set; }

        [IsSearchable, IsFilterable, IsFacetable]
        public string[] Tags { get; set; }

        [IsFilterable, IsSortable, IsFacetable]
        public bool? ParkingIncluded { get; set; }

        // SmokingAllowed reflects whether any room in the hotel allows smoking.
        // The JsonIgnore attribute indicates that a field should not be created 
        // in the index for this property and it will only be used by code in the client.
        [JsonIgnore]
        public bool? SmokingAllowed => (Rooms != null) ? Array.Exists(Rooms, element => element.SmokingAllowed == true) : (bool?)null;

        [IsFilterable, IsSortable, IsFacetable]
        public DateTimeOffset? LastRenovationDate { get; set; }

        [IsFilterable, IsSortable, IsFacetable]
        public double? Rating { get; set; }

        public Address Address { get; set; }

        [IsFilterable, IsSortable]
        public GeographyPoint Location { get; set; }

        public Room[] Rooms { get; set; }
    }
}

Indexの作成、Documentの挿入、Indexの削除

それでは早速Indexの作成、削除、検索データの挿入をしてみます。

フルコードは下記を参照
https://github.com/Azure-Samples/search-dotnet-getting-started/blob/master/DotNetHowTo/DotNetHowTo/Program.cs

コードを抜粋して説明していきます。

#define HowToExample

namespace AzureSearch.SDKHowTo
{
    using System;
    using System.Linq;
    using System.Threading;
    using Microsoft.Azure.Search;
    using Microsoft.Azure.Search.Models;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Spatial;

    class Program
    {
        // This sample shows how to delete, create, upload documents and query an index
        static void Main(string[] args)
        {
            IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
            IConfigurationRoot configuration = builder.Build();

            SearchServiceClient serviceClient = CreateSearchServiceClient(configuration);

            string indexName = configuration["SearchIndexName"];

            Console.WriteLine("{0}", "Deleting index...\n");
            DeleteIndexIfExists(indexName, serviceClient);

            Console.WriteLine("{0}", "Creating index...\n");
            CreateIndex(indexName, serviceClient);

            ISearchIndexClient indexClient = serviceClient.Indexes.GetClient(indexName);

            Console.WriteLine("{0}", "Uploading documents...\n");
            UploadDocuments(indexClient);

            ISearchIndexClient indexClientForQueries = CreateSearchIndexClient(indexName, configuration);

            RunQueries(indexClientForQueries);

            Console.WriteLine("{0}", "Complete.  Press any key to end application...\n");
            Console.ReadKey();
        }
}

まず主となるSearchServiceClientを生成します。


SearchServiceClient serviceClient = CreateSearchServiceClient(configuration);

そしてIndexの名前を指定する値を設定


string indexName = configuration["SearchIndexName"];

Indexの削除

いきなり消すのかい、と思いながらもこれから先、生成するときにすでにあるIndexだと問題があるので一旦同じIndex名をもつIndexがある場合はまず削除します。


DeleteIndexIfExists(indexName, serviceClient);

DeleteIndexIfExistsというプライベートファンクションが下記のように定義されています。

private static void DeleteIndexIfExists(string indexName, SearchServiceClient serviceClient)
{
  if (serviceClient.Indexes.Exists(indexName))
  {
    serviceClient.Indexes.Delete(indexName);
  }
}

シンプルに、あるなら削除というコマンドです。

Indexの作成

次にIndexを生成します。


CreateIndex(indexName, serviceClient);

CreateIndexというプライベートファンクションが下記のように定義されています。

private static void CreateIndex(string indexName, SearchServiceClient serviceClient)
{
  var definition = new Index()
  {
    Name = indexName,
    Fields = FieldBuilder.BuildForType<Hotel>()
  };

  serviceClient.Indexes.Create(definition);
}

Fields = FieldBuilder.BuildForType<Hotel>() ここでフィールドの指定をしています。先ほどあったHotelクラスをベースにIndexのフィールドを生成します。

あとはserviceClient.Indexes.Create(definition)で生成を実行します。

次に指定したIndexを更新します。SearchIndexClient indexClient = serviceClient.Indexes.GetClient(indexName) でindexClientを生成し、更新する対象とします。

ISearchIndexClient indexClient = serviceClient.Indexes.GetClient(indexName);

Console.WriteLine("{0}", "Uploading documents...\n");
UploadDocuments(indexClient);

Documentの挿入

Azure Searchでは1つのデータをDocumentと呼んでいます。

UploadDocumentsというプライベートファンクションが下記のように定義されています。 hotelsというデータの中にRoomという複数のデータが存在しています。


// Upload documents in a single Upload request.
private static void UploadDocuments(ISearchIndexClient indexClient)
{
  var hotels = new Hotel[]
   {
     new Hotel()
     {
       HotelId = "1",
       HotelName = "Secret Point Motel",
       Description = "The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time's Square and the historic centre of the city, as well as other places of interest that make New York one of America's most attractive and cosmopolitan cities.",
       DescriptionFr = "L'hôtel est idéalement situé sur la principale artère commerciale de la ville en plein cœur de New York. A quelques minutes se trouve la place du temps et le centre historique de la ville, ainsi que d'autres lieux d'intérêt qui font de New York l'une des villes les plus attractives et cosmopolites de l'Amérique.",
       Category = "Boutique",
       Tags = new[] { "pool", "air conditioning", "concierge" },
       ParkingIncluded = false,
       LastRenovationDate = new DateTimeOffset(1970, 1, 18, 0, 0, 0, TimeSpan.Zero),
       Rating = 3.6,
       Location = GeographyPoint.Create(40.760586, -73.975403),
       Address = new Address()
       {
         StreetAddress = "677 5th Ave",
         City = "New York",
         StateProvince = "NY",
         PostalCode = "10022",
         Country = "USA"
       },
       Rooms = new Room[]
       {
         new Room()
         {
           Description = "Budget Room, 1 Queen Bed (Cityside)",
           DescriptionFr = "Chambre Économique, 1 grand lit (côté ville)",
           Type = "Budget Room",
           BaseRate = 96.99,
           BedOptions = "1 Queen Bed",
           SleepsCount = 2,
           SmokingAllowed = true,
           Tags = new[] { "vcr/dvd" }
         },
         new Room()
         {
           Description = "Budget Room, 1 King Bed (Mountain View)",
           DescriptionFr = "Chambre Économique, 1 très grand lit (Mountain View)",
           Type = "Budget Room",
           BaseRate = 80.99,
           BedOptions = "1 King Bed",
           SleepsCount = 2,
           SmokingAllowed = true,
           Tags = new[] { "vcr/dvd", "jacuzzi tub" }
         },
         new Room()
         {
           Description = "Deluxe Room, 2 Double Beds (City View)",
           DescriptionFr = "Chambre Deluxe, 2 lits doubles (vue ville)",
           Type = "Deluxe Room",
           BaseRate = 150.99,
           BedOptions = "2 Double Beds",
           SleepsCount = 2,
           SmokingAllowed = false,
           Tags = new[] { "suite", "bathroom shower", "coffee maker" }
         }
       }
     },
     new Hotel()
     {
       HotelId = "2",
       HotelName = "Twin Dome Motel",
       Description = "The hotel is situated in a  nineteenth century plaza, which has been expanded and renovated to the highest architectural standards to create a modern, functional and first-class hotel in which art and unique historical elements coexist with the most modern comforts.",
       DescriptionFr = "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
       Category = "Boutique",
       Tags = new[] { "pool", "free wifi", "concierge" },
       ParkingIncluded = false,
       LastRenovationDate =  new DateTimeOffset(1979, 2, 18, 0, 0, 0, TimeSpan.Zero),
       Rating = 3.60,
       Location = GeographyPoint.Create(27.384417, -82.452843),
       Address = new Address()
       {
         StreetAddress = "140 University Town Center Dr",
         City = "Sarasota",
         StateProvince = "FL",
         PostalCode = "34243",
         Country = "USA"
       },
       Rooms = new Room[]
       {
         new Room()
         {
           Description = "Suite, 2 Double Beds (Mountain View)",
           DescriptionFr = "Suite, 2 lits doubles (vue sur la montagne)",
           Type = "Suite",
           BaseRate = 250.99,
           BedOptions = "2 Double Beds",
           SleepsCount = 2,
           SmokingAllowed = false,
           Tags = new[] { "Room Tags" }
         },
         new Room()
         {
           Description = "Standard Room, 1 Queen Bed (City View)",
           DescriptionFr = "Chambre Standard, 1 grand lit (vue ville)",
           Type = "Standard Room",
           BaseRate = 121.99,
           BedOptions = "1 Queen Bed",
           SleepsCount = 2,
           SmokingAllowed = false,
           Tags = new[] { "jacuzzi tub" }
         },
         new Room()
         {
           Description = "Budget Room, 1 King Bed (Waterfront View)",
           DescriptionFr = "Chambre Économique, 1 très grand lit (vue sur le front de mer)",
           Type = "Budget Room",
           BaseRate = 88.99,
           BedOptions = "1 King Bed",
           SleepsCount = 2,
           SmokingAllowed = false,
           Tags = new[] { "suite", "tv", "jacuzzi tub" }
         }
       }
     },
     new Hotel()
     {
       HotelId = "3",
       HotelName = "Triple Landscape Hotel",
       Description = "The Hotel stands out for its gastronomic excellence under the management of William Dough, who advises on and oversees all of the Hotel’s restaurant services.",
       DescriptionFr = "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
       Category = "Resort and Spa",
       Tags = new[] { "air conditioning", "bar", "continental breakfast" },
       ParkingIncluded = true,
       LastRenovationDate = new DateTimeOffset(2015, 9, 20, 0, 0, 0, TimeSpan.Zero),
       Rating = 4.80,
       Location = GeographyPoint.Create(33.84643, -84.362465),
       Address = new Address()
       {
         StreetAddress = "3393 Peachtree Rd",
         City = "Atlanta",
         StateProvince = "GA",
         PostalCode = "30326",
         Country = "USA"
       },
       Rooms = new Room[]
       {
         new Room()
         {
           Description = "Standard Room, 2 Queen Beds (Amenities)",
           DescriptionFr = "Chambre Standard, 2 grands lits (Services)",
           Type = "Standard Room",
           BaseRate = 101.99,
           BedOptions = "2 Queen Beds",
           SleepsCount = 4,
           SmokingAllowed = true,
           Tags = new[] { "vcr/dvd", "vcr/dvd" }
         },
         new Room ()
         {
           Description = "Standard Room, 2 Double Beds (Waterfront View)",
           DescriptionFr = "Chambre Standard, 2 lits doubles (vue sur le front de mer)",
           Type = "Standard Room",
           BaseRate = 106.99,
           BedOptions = "2 Double Beds",
           SleepsCount = 2,
           SmokingAllowed = true,
           Tags = new[] { "coffee maker" }
         },
         new Room()
         {
           Description = "Deluxe Room, 2 Double Beds (Cityside)",
           DescriptionFr = "Chambre Deluxe, 2 lits doubles (Cityside)",
           Type = "Budget Room",
           BaseRate = 180.99,
           BedOptions = "2 Double Beds",
           SleepsCount = 2,
           SmokingAllowed = true,
           Tags = new[] { "suite" }
         }
       }
     }
   };

   var batch = IndexBatch.Upload(hotels);

   try
   {
     indexClient.Documents.Index(batch);
   }
   catch (IndexBatchException e)
   {
     // Sometimes when your Search service is under load, indexing will fail for some of the documents in
     // the batch. Depending on your application, you can take compensating actions like delaying and
     // retrying. For this simple demo, we just log the failed document keys and continue.
     Console.WriteLine("Failed to index some of the documents: {0}",
       String.Join(", ", e.IndexingResults.Where(r => !r.Succeeded).Select(r => r.Key)));
   }

   Console.WriteLine("Waiting for documents to be indexed...\n");
   Thread.Sleep(2000);
}

hotelsという変数を指定し、batch処理指定をします。

var batch = IndexBatch.Upload(hotels);

そしてそのbactch処理を下記で実行します。

indexClient.Documents.Index(batch);

これで指定した3つのホテルはAzure SearchIndexされました。

検索

本コードでは下記の部分で検索を実行しています。

ISearchIndexClient indexClientForQueries = CreateSearchIndexClient(indexName, configuration);

RunQueries(indexClientForQueries);

Console.WriteLine("{0}", "Complete.  Press any key to end application...\n");
Console.ReadKey();

ここに定義されているRunQueriesファンクションは下記の通り。 下記のコードでは複数の検索を実行しています。

  1. motelというキーワードでホテル名だけを返す検索。
  2. 一泊100ドル以下のホテルのみ、hotelIddescriptionを返す検索。
  3. LastRenovationDate順に並べ、上位2個の検索結果のみを返す検索。
  4. hotelというキーワードでホテル名だけを返す検索。

private static void RunQueries(ISearchIndexClient indexClient)
{
  SearchParameters parameters;
  DocumentSearchResult<Hotel> results;

  Console.WriteLine("Search the entire index for the term 'motel' and return only the HotelName field:\n");

  parameters = new SearchParameters()
  {
    Select = new[] { "HotelName" }
  };

  results = indexClient.Documents.Search<Hotel>("motel", parameters);

  WriteDocuments(results);

  Console.Write("Apply a filter to the index to find hotels with a room cheaper than $100 per night, ");
  Console.WriteLine("and return the hotelId and description:\n");

  parameters = new SearchParameters()
  {
    Filter = "Rooms/any(r: r/BaseRate lt 100)",
    Select = new[] { "HotelId", "Description" }
  };

  results = indexClient.Documents.Search<Hotel>("*", parameters);

  WriteDocuments(results);

  Console.Write("Search the entire index, order by a specific field (lastRenovationDate) ");
  Console.Write("in descending order, take the top two results, and show only hotelName and ");
  Console.WriteLine("lastRenovationDate:\n");

  parameters = new SearchParameters()
  {
    OrderBy = new[] { "LastRenovationDate desc" },
    Select = new[] { "HotelName", "LastRenovationDate" },
    Top = 2
  };

  results = indexClient.Documents.Search<Hotel>("*", parameters);

  WriteDocuments(results);

  Console.WriteLine("Search the hotel names for the term 'hotel':\n");

  parameters = new SearchParameters()
  {
    SearchFields = new[] { "HotelName" }
  };

  results = indexClient.Documents.Search<Hotel>("hotel", parameters);

  WriteDocuments(results);
}

private static void WriteDocuments(DocumentSearchResult<Hotel> searchResults)
{
  foreach (SearchResult<Hotel> result in searchResults.Results)
  {
    Console.WriteLine(result.Document);
  }

  Console.WriteLine();
}

Search メソッド

今回使っているSDKのSearchメソッドは下記です


public static Microsoft.Azure.Search.Models.DocumentSearchResult<T> Search<T> 
(
this Microsoft.Azure.Search.IDocumentsOperations operations, 
string searchText, 
Microsoft.Azure.Search.Models.SearchParameters searchParameters = null, 
Microsoft.Azure.Search.Models.SearchRequestOptions searchRequestOptions = null
);

詳しくは下記を参照ください。
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.documentsoperationsextensions.search?view=azure-dotnet#Microsoft_Azure_Search_DocumentsOperationsExtensions_Search__1_Microsoft_Azure_Search_IDocumentsOperations_System_String_Microsoft_Azure_Search_Models_SearchParameters_Microsoft_Azure_Search_Models_SearchRequestOptions_

ホテル名で検索

下記は motelというキーワードでホテル名だけを返す検索です。

parameters = new SearchParameters()
{
  Select = new[] { "HotelName" }
};

results = indexClient.Documents.Search<Hotel>("motel", parameters);

WriteDocuments(results);

ここではまずSearchParametersを生成しています。その際に選択するフィールドHotelNameに指定しています。

Searchメソッドでmotelをキーワードに指定し、先ほど生成したSearchParametersを渡しています。

フィルターでホテルを検索

下記は一泊100ドル以下のホテルのみ、hotelIddescriptionを返す検索になります。

parameters = new SearchParameters()
{
  Filter = "Rooms/any(r: r/BaseRate lt 100)",
  Select = new[] { "HotelId", "Description" }
};

results = indexClient.Documents.Search<Hotel>("*", parameters);

WriteDocuments(results);

ここでも同じく、SearchParametersを生成します。その際にFilterプロパティをつあって一泊100ドル以下というフィルターを設定しています。ここで使われるフィルターはODataで指定をします。 ODataに関してはこちらを参照してください。
https://docs.microsoft.com/en-us/azure/search/query-odata-filter-orderby-syntax

検索結果の並び替え

下記はLastRenovationDate順に並べ、上位2個の検索結果のみを返す検索になります。

parameters = new SearchParameters()
{
  OrderBy = new[] { "LastRenovationDate desc" },
  Select = new[] { "HotelName", "LastRenovationDate" },
  Top = 2
};

results = indexClient.Documents.Search<Hotel>("*", parameters);

WriteDocuments(results);

ここでも同じくSearchParametersの中でOrderByフィールドを指定することによって検索結果の順番を指定しています。そしてTopフィールドで検索結果の上位2個だけを返すように指定しています。

SearchParameters

今回の例を見てもわかるように、SearchParametersを使うと様々な検索を指定することができます。SearchParametersの詳細は下記を参照してください。
https://docs.microsoft.com/en-us/dotnet/api/microsoft.azure.search.models.searchparameters?view=azure-dotnet

次回は今回作ったIndexにデータを流し込みするIndexerの生成を見てみます。
[Azure Search] Indexerを作る :point_right_tone1:

そして、さらに複雑な検索パターンも見てみたいと思います。 TypeAheadを使った検索スジャストなども見ていきたいと思います。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2