テキスト検索するためには今回は(も)Azure Searchを選択しました。
詳細ドキュメントはこちらを参照ください。
https://learn.microsoft.com/ja-jp/azure/search/
ということで、早速作成してみます。
今回はこちらのドキュメントを参考にしています。
https://learn.microsoft.com/ja-jp/azure/search/search-create-service-portal
https://learn.microsoft.com/ja-jp/azure/search/search-get-started-dotnet
下記の手順で現在Tableにあるレシート情報を検索できるようにします。
- C#用Nugetライブラリーのインストール
- データモデルの定義とインデックスの作成
- ドキュメントを読み込む(=検索インデックスにデータを挿入する)
- インデックスの検索
それでは早速進めます。
C#用Nugetライブラリーのインストール
Nugetでライブラリーをインストールします。
Install-Package Azure.Search.Documents -Version 11.4.0
データモデルの定義とインデックスの作成
こちらがモデルになります。
public class ReceiptSearchModel
{
[SimpleField(IsKey = true, IsFilterable = true)]
public string ReceiptId { get; set; }
[SimpleField(IsFilterable = true)]
public string OwnerId { get; set; }
[SimpleField(IsFilterable = true, IsSortable = true)]
public DateTimeOffset? ReceiptTransactionDate { get; set; }
[SearchableField(AnalyzerName = LexicalAnalyzerName.Values.JaLucene)]
public string ReceiptText { get; set; }
[SearchableField(AnalyzerName = LexicalAnalyzerName.Values.JaLucene, IsFacetable = true)]
public string StoreName { get; set; }
public string StoreAddress { get; set; }
[SearchableField(IsFilterable = true, IsFacetable = true)]
public string StoreCountry { get; set; }
[SearchableField(IsFilterable = true, IsFacetable = true)]
public string StorePrefecture { get; set; }
[SearchableField(IsFilterable = true, IsFacetable = true)]
public string StoreLocality { get; set; }
public string StoreTel { get; set; }
public string StoreEmail { get; set; }
[SimpleField(IsFilterable = true, IsSortable = true)]
public int GrandTotal { get; set; }
[SimpleField(IsFilterable = true)]
public string Currency { get; set; }
[SearchableField(IsFilterable = true, IsFacetable = true)]
public string PaymentMethod { get; set; }
[SearchableField(IsFilterable = true, IsFacetable = true)]
public string[] Tags { get; set; }
[SearchableField(IsFilterable = true, IsFacetable = true)]
public string Category { get; set; }
}
ここで作ったモデルをベースにIndexを作成します。
private void CreateSearchIndex(string indexName, SearchIndexClient adminClient)
{
FieldBuilder fieldBuilder = new FieldBuilder();
var searchFields = fieldBuilder.Build(typeof(ReceiptSearchModel));
var definition = new SearchIndex(indexName, searchFields);
var suggester = new SearchSuggester("stores", new[] { "StoreName" });
definition.Suggesters.Add(suggester);
adminClient.CreateOrUpdateIndex(definition);
}
このメソッドを実際にコールしてみます。
public void InitializeSaerchIndex()
{
string apiKey = "{YOUR API KEY}";
string indexName = "{YOUR INDEX NAME}";
// Create a SearchIndexClient to send create/delete index commands
Uri serviceEndpoint = new Uri($"{YOUR SERVICE URL}");
AzureKeyCredential credential = new AzureKeyCredential(apiKey);
SearchIndexClient adminClient = new SearchIndexClient(serviceEndpoint, credential);
// Create a SearchClient to load and query documents
SearchClient srchclient = new SearchClient(serviceEndpoint, indexName, credential);
SearchIndexClient searchIndexClient = new SearchIndexClient(serviceEndpoint, credential);
// Create index
CreateSearchIndex(indexName, searchIndexClient);
}
実際に↑を実装するコードは省略しますが、このコードを実装したあとにAzureポータルよりIndexが生成されていることを確認してみてください。
ドキュメントを読み込む(=検索インデックスにデータを挿入する)
次は先ほど作成したインデックスにデータを挿入します。 (IndexClientを生成するクラスは別に作ることをお勧めします。)
public void SyncSearchIndex()
{
// Get source doc.
var receipts = GetFullCustomerReceipts();
var dataList = new List<IndexDocumentsAction<ReceiptSearchModel>>();
if (receipts != null)
{
foreach (var receipt in receipts)
{
dataList.Add(IndexDocumentsAction.Upload(new ReceiptSearchModel
{
OwnerId = receipt.PartitionKey ?? "",
StoreAddress = receipt.StorePrintAddress ?? "",
StoreName = receipt.StorePrintName ?? "",
Category = receipt.ExpenseCategory ?? "",
Currency = "JPY",
ReceiptId = receipt.RowKey,
PaymentMethod = receipt.DespoitMethod.ToString(),
ReceiptTransactionDate = receipt.TerminalTransactionDateTime,
GrandTotal = decimal.ToInt32(receipt.GrandTotal),
ReceiptText = receipt.OcrText ?? "",
Tags = new string[] { "receipt" }
}));
}
try
{
string apiKey = "{YOUR API KEY}";
string indexName = "{YOUR INDEX NAME}";
// Create a SearchIndexClient to send create/delete index commands
Uri serviceEndpoint = new Uri($"{YOUR SERVICE URL}");
AzureKeyCredential credential = new AzureKeyCredential(apiKey);
SearchIndexClient adminClient = new SearchIndexClient(serviceEndpoint, credential);
// Create a SearchClient to load and query documents
SearchClient searchClient = new SearchClient(serviceEndpoint, indexName, credential);
IndexDocumentsBatch<ReceiptSearchModel> batch = IndexDocumentsBatch.Create(dataList.ToArray());
IndexDocumentsResult result = searchClient.IndexDocuments(batch);
}
catch (Exception)
{
// If for some reason any documents are dropped during indexing, you can compensate by delaying and
// retrying. This simple demo just logs the failed document keys and continues.
Console.WriteLine("Failed to index some of the documents: {0}");
}
}
}
このコードを実装したあとに、該当インデックスのドキュメント数が増えていることを確認してみてください。
特定のDBに対してインデクサーを付けることによって自動的に同期するようにすることもできます。詳細はこちらを参照ください。https://learn.microsoft.com/ja-jp/azure/search/search-indexer-tutorial
インデックスの検索
それでは最後にインデックスの検索をしてみます。検索方法に関しては詳細はこちらを参照ください。 https://learn.microsoft.com/ja-jp/azure/search/search-query-overview#types-of-queries
下記のコードでインデックスの検索を実装します。
public SearchResults<ReceiptSearchModel> RunQueries(string ownerId, string keyword)
{
SearchOptions options;
SearchResults<ReceiptSearchModel> response;
// Query 1
Console.WriteLine("Query #1: Search on empty term '*' to return all documents, showing a subset of fields...\n");
options = new SearchOptions()
{
IncludeTotalCount = true,
Filter = "OwnerId eq '" + ownerId + "'",
OrderBy = { "ReceiptTransactionDate" }
};
string apiKey = "{YOUR API KEY}";
string indexName = "{YOUR INDEX NAME}";
// Create a SearchIndexClient to send create/delete index commands
Uri serviceEndpoint = new Uri($"{YOUR SERVICE URL}");
AzureKeyCredential credential = new AzureKeyCredential(apiKey);
SearchIndexClient adminClient = new SearchIndexClient(serviceEndpoint, credential);
// Create a SearchClient to load and query documents
SearchClient searchClient = new SearchClient(serviceEndpoint, indexName, credential);
response = searchClient.Search<ReceiptSearchModel>(keyword, options);
return response;
}
コントロールから呼ぶときは、下記のようにしています。
public async Task<IActionResult> Search(string culture, string keyword)
{
var user = await _userManager.GetUserAsync(User);
var searchResults = _receiptServices.RunQueries(user.Id, keyword);
var view = new UsrReceiptSearchViewModel()
{
SearchResult = searchResults,
Description = "",
Culture = culture,
Keyword = keyword
};
return View(view);
}
Viewはこちらです。(抜粋)
@if (Model.SearchResult.TotalCount > 0)
{
<table class="table table-bordered table-striped">
<tr>
<th>日付</th>
<th>
場所
</th>
<th>
金額
</th>
</tr>
@foreach (var item in Model.SearchResult.GetResults())
{
<tr>
<td>
<a href="/@Model.Culture/usr/receipt/@item.Document.ReceiptId">
<div class="font-xs">
@item.Document.ReceiptId
</div>
@item.Document.ReceiptTransactionDate.Value.ToString("yyyy-MM-dd")
</a>
</td>
<td>
@item.Document.StoreName
</td>
<td class="text-end">
¥@item.Document.GrandTotal.ToString("N0")
</td>
</tr>
}
</table>
}
else
{
<div>該当データはありませんでした。</div>
}
まとめ
本日は簡易にAzure Searchの利用方法をまとめてみました。テキスト検索は奥深く、今後はさらに深い内容にふれていきたいと思います。
採用中です
レシートローラーでは紙レシート削減をミッションにデジタルレシートサービスを展開しています。一緒に開発進めてくれるエンジニア大募集中です。興味のある方はこちらの求人情報を確認ください。