LoginSignup
2
2

More than 5 years have passed since last update.

Azure Search の Index と Indexer を自動生成する

Posted at

今日は、Azure Search と一日格闘したので、その成果を記録しておきたい。

Capture.JPG

Azure Search を導入した理由

現在パーソナルプロジェクトを開発中なのだが、CosmosDB で前方一致のクエリをする必要がある場面があった。当初考えたソリューションでは、該当の項目の先頭文字をインデックスとして保存して、文字の長さ別にそこをサーチするという作戦を考えた。ところが周りの人は Azure Search をすすめていたので素直に従ってみました。確かにロジックでごねごねやるよりも、ずっときれいだし、何より、Query を発行すればするほど、RUの制限が近づきますが、Search を併用すると、そこを防ぐことも出来て一石二鳥です。ついでにいうと、前方一致どころか、全文検索もできますし、ほかのカラムも検索できますし、自分が作っているプロダクトの要素からするとそちらのほうが100倍フィットしそうです。

導入時の課題

導入時の課題としては、むかしちょろっと触ったことがあるぐらいで、あまりちゃんと使ったことがなかったところ、いきなり本番でガチで運用するシステムに導入するので、がっつり勉強が必要でした。勉強以外の点では次の点が疑問でした。

  • Infrastructure as Code. 特に Index と Indexer の作成
  • クエリーの書き方
  • Microsoft.Azure.Search SDK

ちなみに、最後の SDK ですが、私が公式ドキュメントのページ Create an Azure Search index using the .NET SDKを見ると近くにサンプルのページがありますが、私がたまたま見たプロジェクトが、GAのものではなく、PreRelease の Fluent API のもので、ドキュメントは、Microsoft.Azure.Search なのだけど、あまりAPIの説明もなかったので(実際はGetting Started with Azure Search using .NETにサンプルがあったw)自分でReSharperを使ってデコンパイルしまくりながら、動作を確認していきました。

上記のことについて整理しておきたいと思います。

Index と Indexer の生成

Azure Search の作成

今回は Terraform でサクッと作成

resource "azurerm_search_service" "test" {
  name  = "${var.environment_base_name}search"
  location                  = "japanwest"
  resource_group_name       = "${azurerm_resource_group.test.name}"
  sku  = "standard"

  tags {
    environment = "production"
    database = "${azurerm_cosmosdb_account.test.name}"
  }
}

Search Management Key の取得

残念ながらここは手段がなく、まだ実装していません。REST API があります。

これをたたけばadmin key が取得できます。Terraform にはないので貢献しようかな、、、

他には Azure CLI を使う手もあります。

az search admin-key show --resource-group some-repository-rg --service-name somesearch
{
  "primaryKey": "SOMEKEYISHERE",
  "secondaryKey": "OTHERKEYISHERE"
}

Index の作成

Cosmos DB に対してインデックスをかけたい場合は CosmosDB に対してインデックスとデータソースを作って、インデクサーというもので、スケジューリングする必要があります。まずはインデックス。この3つをやれば、ポータル上で操作するのとほぼ同じような設定ができます。

Model を作る

Cosmos DB のテーブルのうち、検索対象にしたいものにアノテーションをつけます。ちなみに、元のはこんなのだったのですが、このままではうまくいきませんでした。Release[] というのがあるので、実際にIndexを作ると、Release が見つからないとエラーになります。悔しいですが、Search の項目を表すオブジェクトを作りました。


    public class Package
    {
        [System.ComponentModel.DataAnnotations.Key]
        [JsonProperty("id")]
        public string Id { get; set; }

        [Required]
        public string Name { get; set; }

        public string Description { get; set; }
        public string Author { get; set; }
        [Url]
        public string ProjectPage { get; set; }
        [Url]
        public string ProjectRepo { get; set; }
        public DateTime CreatedTime { get; set; }
        public Release[] Releases { get; set; }

        // Column for Azure Search soft delete
        public bool IsDeleted { get; set; }

        public void GenerateId()
        {
            Id = Guid.NewGuid().ToString();
        }

    }

SearchPackage.cs

[SerializePropertyNameAsCamelCase] をつけておくと、Public のプロパティをCamelCase でサーチの項目とみなしてくれます。ちなみに、[JsonProperty()] が指定しているときは、そちらが優先されます。ほかには、Data.Annotation.Key としてキーの値を、[IsSearchable][IsFilterable] は、検索したい方法について書いてあります。 これらは、Request Body Syntaxを参照してみてください。アトリビュートによって、どの項目に対してサーチをかけたかを設定できます。

    [SerializePropertyNamesAsCamelCase]
    public class SearchPackage
    {
        [System.ComponentModel.DataAnnotations.Key]
        [JsonProperty("id")]
        public string Id { get; set; }
        [IsSearchable, IsFilterable, IsSortable]

        public string Name { get; set; }
        [IsSearchable]
        public string Description { get; set; }
        public string Author { get; set; }

        public string ProjectPage { get; set; }

        public string ProjectRepo { get; set; }
        public DateTime CreatedTime { get; set; }
        public string Releases { get; set; }

        // Column for Azure Search soft delete
        public bool IsDeleted { get; set; }
    }

ポイントの一つとしては、インデックス側からすると、CosmosDB のデータを物理削除しても、Search が知りえないので、論理削除する必要があります。IsDeleted がその項目です。論理削除の項目は後で指定します。

これに対しての、index の作成についてはこんな感じです。BuilderForType のところで、先ほどのインデックス対象のモデルを指定します。CreateOrUpdateWithHttpMessageAsync は無ければ作るし、あったらアップデートするので便利な感じです。


  _client = new SearchServiceClient(searchServiceName, new SearchCredentials(adminApiKey));

   :
           var definition = new Index()
            {
                Name = INDEX_NAME,
                Fields = FieldBuilder.BuildForType<SearchPackage>()
            };
            await _client.Indexes.CreateOrUpdateWithHttpMessagesAsync(INDEX_NAME, definition);

ちなみに、インデックスの名前は、小文字で、字数制限もある様子です。

DataSource の作成

インデックスが上記のでできますが、次はデータソース、つまり、インデックスを作る元です。CosmosDB に接続します。

若干ややこしいですが、CosmosDB に接続するコネクションストリングを渡します。HighWaterMarkChangeDetectionPolicy は、_ts つまり最終更新の時間を見て、前にインデックスを作成したときより新しければ、インデックスを作成します。また、先ほど説明した、論理削除の項目も指定しています。これで、データソースも完成。

            var dataContainer = new DataContainer("Package", null);
            var dataSourceCredentials = new DataSourceCredentials(GetCosmosDBConnectionString());
            var dataChangeDetectionPolicy = new HighWaterMarkChangeDetectionPolicy("_ts");

            var dataDeletionDetectionPolicy = new SoftDeleteColumnDeletionDetectionPolicy("IsDeleted", true);

            var dataSource = new DataSource(
                DATASOURCE_NAME, 
                DataSourceType.DocumentDb, 
                dataSourceCredentials, 
                dataContainer,"Strikes CosmosDB Settings", 
                dataChangeDetectionPolicy,
                dataDeletionDetectionPolicy);

            await _client.DataSources.CreateOrUpdateWithHttpMessagesAsync(dataSource);

Indexer の作成

最後にこれらを合わせて、Indexer を作成します。次のコードでは、30分おきにインデックスを更新するように、インデクサーを設定しています。今まで作ったデータソースやインデックスの値、スケジュールを入れておしまい!これも、あったら更新するので気軽に使えます。

            var indexSchedule = new IndexingSchedule(TimeSpan.FromMinutes(30),DateTimeOffset.Now);
            var indexer = new Indexer(INDEXER_NAME, DATASOURCE_NAME, INDEX_NAME, "hourly scheduled", indexSchedule);
            await _client.Indexers.CreateOrUpdateWithHttpMessagesAsync(indexer);

Search

参考までにサーチはこんな感じ

        public async Task<IEnumerable<SearchPackage>> SearchAsync(string query)
        {
            var parameters = new SearchParameters();
            var results = await _indexClient.Documents.SearchAsync<SearchPackage>(query, parameters);
            return results.Results.Select<SearchResult<SearchPackage>, SearchPackage>(p => p.Document);
        }

ちなみに、サーチのクエリですが、目標だった全文検索は、ポータルだとこんな感じで実行できました。ポイントとしては、前方一致だけでよくても、IsSearchable が必要という感じです。

search=h*&searchFields=Name

まとめ

このようにすれば C# の世界でポータルでやったのと同じような設定ができて、自動でリソースをデプロイして、コンフィグして環境を作れます。最近は、C# は、.Net Core だったら Linux でも動作するので、私は、.Net Core でこのプログラムを作っておいて、Environment Variables 経由で、値を引き渡してこのプログラムを実行して、自動でコンフィグみたいなことをよくやっています。 CosmosDB の初期データセットアップみたいなもの同じ方法でやっています。

ちなみに、今回のコードですが、私のプロジェクトの一部ですが、シェアしておきます。

参考

2
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
2
2