今日は、Azure Search と一日格闘したので、その成果を記録しておきたい。
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 の初期データセットアップみたいなもの同じ方法でやっています。
ちなみに、今回のコードですが、私のプロジェクトの一部ですが、シェアしておきます。
参考