Azure Search Document
是 Azure Cognitive Search documentation
(認知搜尋文件) 這項服務的 Nuget 套件,也就是以前說的 Azure Search
,這項服務可以讓使用者的搜尋體驗變得較為友善-透過自動完成、同義字比對、模糊比對、模式比對、篩選和排序
一般情況下大部分我們自己做的搜尋都還在篩選排序等等,要做到同義字比對與模糊比對就比較複雜,或者說需要付出較高的企業成本,而採用Azure Search
就是一個可以考慮的解決方案了,接下來就著重在如何使用的部分,透過一個簡單的範例來說明。
intro
搜尋服務最核心的就是下面的三個東西,我們首先需要先準備資料,放在資料來源當中,並且建立好索引,也就是資料與欄位的對應關係,並確認該欄位的一些屬性,例如是否可搜尋、可排序等等;最後再透過索引子實際執行,將資料來源的資料經過處理後放到索引之內。之後就可以透過 API 去做查詢,也可以透過 Azure Search Document
這個 Nuget 套件查詢資料
- 索引:透過 SDK 或 REST API 發出請求,針對索引欄位作操作
- 索引子:索引&資料來源中間愛的橋樑
- 資料來源:支援 Azure Blob、Azure SQL Database…等等
詳細的設定及文件還是需要參考官方文件比較清楚,這裡只是稍微說明一下
資料來源
能夠作為資料來源的有很多,這邊以 Azure Blob 容器為例子,指定好連接字串
、容器名稱
就可以使用這裡也有很多設定值可以調整,也要考量到自己的規劃來做設定。像是追蹤刪除
就會影響到你索引的定義,還有資料來源上傳的檔案 Schema 要怎麼規劃,在我的使用情境下,我是需要追蹤刪除的,所以我採用了使用一個欄位IsDel
來做,只要這個欄位是 1 就代表這筆資料需要被刪除
- 教學課程:使用 .NET SDK 為 Azure SQL 資料編製索引
- 教學課程:使用 .NET SDK 從多個資料來源編製索引
- Sample Code - 1
- Sample Code - 2
- 適用于 Azure 認知搜尋的 .NET (c # ) 程式碼範例
索引
在這邊設定的就是定義好每一筆資料的欄位,是否可以被搜尋、篩選、取出、排序等等,比較重要的部分就是要定義那個欄位是Key
值,當然也要加上一個虛刪除的欄位
假設我要做一個商品查詢,那索引應該要有哪些欄位呢?
- 搜尋結果:前端會用到的欄位,如商品標題,價格等等
- 篩選條件:商品數量、銷售狀態、分類等等
- 關鍵字查詢:商品名稱、商品明細
- 統計資訊:分類等等
- 排序條件:商品庫存、價格等等
決定好有哪些欄位之後,依據各欄位用途設定屬性,使其可以被取出、篩選、排序、搜尋、Facet
- 可取出:大概的意思就是跟 SQL 的 Select 差不多吧。基本上不需要被取出的欄位也沒必要上傳到資料來源了吧(除非要做篩選排序搜尋等等)
- 可排序:就是字面上的意思,該欄位可以用來排序
- 可篩選:這個意思就是類似 SQL 的 Where 條件,例如當我設定商品類型可以篩選,那我就可以透過篩選商品類型的值,取得某類型的商品
- 可 Facet:假設我有三筆資料,兩筆是 3C 用品,一筆是文具,當我加上了參數要他回傳 Facet,並且搜尋所有商品,他就會額外給我一個統計資料,告訴我 3C 有兩筆,文具有一筆;大概就是這樣的意思;另一種解釋是:用該面向去看你的搜尋結果
- 可搜尋:設定為可搜尋的話,後續需要指定要用哪一種分析器,應該是跟語意分析、拆字有關係吧…
在這裡要注意資料庫的欄位定義與索引的欄位定義資料型態的問題,要看一下是否相容
索引子
索引子的設定相對較為單純,這裡因為我採用的是 json 的資料來源,所以parsingMode
設定為 jsonArray
,代表的是 Json 檔案內是一個 json 物件的集合
同義詞建立
建立、更新和刪除同義字地圖永遠是整份檔的作業,這表示您無法以累加方式更新或刪除同義字地圖的部分。 即使只更新單一規則,也需要重載。
-
只能每次重新做
-
做完就需要去更新索引有哪些欄位使用到了同義字
搜尋建議
範例程式碼
基於開發時期常常會需要修改調整,如果都透過手動來建立索引等等動作,會非常的煩人,所以寫個 Console 專案透過參數來做這些事情會比較方便,這邊就節錄我自己寫的一個 Helper,全都是參考官方範例改的。下面的程式碼僅供參考
AppSettingConfig.json
{
"azureSearchConfig": {
"ServiceName": "service-name",
"ApiKey": "api-key",
"IndexName": "index-name",
"DataSourceName": "datasource-name",
"DataSourceConnectionString": "datasource-conn-str",
"DataSourceContainer": "container-name",
"DataSourceContainerQuery": "container-folder",
"IndexerName": "indexer-name"
}
}
AzureSearchSettings.cs
public class AzureSearchSettings
{
public const string Position = "AzureSearchConfig";
/// <summary>
/// 同義詞名稱
/// </summary>
public const string SynonymsName = "kw-synonym";
/// <summary>
/// 採用同義詞的欄位
/// </summary>
public static readonly string[] SynonymsFields = { "ProductName", "Detail" };
/// <summary>
/// 搜尋建議來源欄位
/// </summary>
public static readonly string[] SuggestFields = { "ProductName", "Detail" };
/// <summary>
/// service name
/// </summary>
public string ServiceName { get; set; } = string.Empty;
/// <summary>
/// API Key
/// </summary>
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// 索引名稱
/// </summary>
public string IndexName { get; set; } = string.Empty;
/// <summary>
/// Azure Search Service EndPoint
/// </summary>
public Uri ServiceEndPoint => new($"https://{ServiceName}.search.windows.net/");
/// <summary>
/// 資料來源名稱
/// </summary>
public string DataSourceName { get; set; } = string.Empty;
/// <summary>
/// 資料來源連線字串
/// </summary>
public string DataSourceConnectionString { get; set; } = string.Empty;
/// <summary>
/// 容器名稱
/// </summary>
public string DataSourceContainer { get; set; } = string.Empty;
/// <summary>
/// 容器子目錄名稱
/// </summary>
public string DataSourceContainerQuery { get; set; } = string.Empty;
/// <summary>
/// 索引子名稱
/// </summary>
public string IndexerName { get; set; } = string.Empty;
}
MyProduct.cs
public class MyProduct
{
/// <summary>
/// 商品代碼
/// </summary>
[SimpleField(IsKey = true)]
public string ProductID { get; set; }
/// <summary>
/// 圖片
/// </summary>
[SimpleField]
public string Pic { get; set; }
/// <summary>
/// 售價
/// </summary>
[SimpleField]
public long Price { get; set; }
/// <summary>
/// 商品數量顯示所需資料
/// </summary>
[SimpleField(IsFilterable = true)]
public int Qty { get; set; }
/// <summary>
/// 關鍵字商品查詢條件
/// </summary>
[SearchableField(AnalyzerName = LexicalAnalyzerName.Values.StandardLucene, SynonymMapNames = new[] { "kw-synonym" })]
public string ProductName { get; set; }
/// <summary>
/// 關鍵字商品查詢條件
/// </summary>
[SearchableField(AnalyzerName = LexicalAnalyzerName.Values.StandardLucene, SynonymMapNames = new[] { "kw-synonym" })]
public string Detail { get; set; }
/// <summary>
/// 篩選條件:商品分類
/// </summary>
[SimpleField(IsFacetable = true, IsFilterable = true)]
public int ProductType { get; set; }
/// <summary>
/// 排序條件:商品排序
/// </summary>
[SimpleField(IsSortable = true)]
public int Sort { get; set; }
/// <summary>
/// 是否刪除
/// </summary>
[SimpleField]
public int IsDel { get; set; }
}
AzureSearchHelper.cs
public class AzureSearchHelper
{
private readonly SearchIndexClient _indexClient;
private readonly SearchIndexerClient _indexerClient;
private readonly string _indexName;
private readonly AzureSearchSettings _settings;
public AzureSearchHelper(AzureSearchSettings settings)
{
_settings = settings;
_indexName = settings.IndexName;
_indexClient = new SearchIndexClient(settings.ServiceEndPoint, new AzureKeyCredential(settings.ApiKey));
_indexerClient = new SearchIndexerClient(settings.ServiceEndPoint, new AzureKeyCredential(settings.ApiKey));
}
private SearchIndexerDataSourceConnection PrepareDataSourceConnection()
{
return new SearchIndexerDataSourceConnection(
_settings.DataSourceName,
SearchIndexerDataSourceType.AzureBlob,
_settings.DataSourceConnectionString,
new SearchIndexerDataContainer(_settings.DataSourceContainer) { Query = _settings.DataSourceContainerQuery })
{
DataDeletionDetectionPolicy = new SoftDeleteColumnDeletionDetectionPolicy
{
SoftDeleteColumnName = "IsDel",
SoftDeleteMarkerValue = "1"
}
};
}
private void HandleSynonymsIndexSafely(string action)
{
var MaxNumTries = 3;
for (var i = 0; i < MaxNumTries; ++i)
try
{
SearchIndex index = _indexClient.GetIndex(_indexName);
index = HandleSynonymMapsToFields(index, action);
// The IfNotChanged condition ensures that the index is updated only if the ETags match.
_indexClient.CreateOrUpdateIndex(index);
Console.WriteLine("更新索引 OK.");
break;
}
catch (Exception ex)
{
Console.WriteLine($"Index update failed : . Attempt({i}/{MaxNumTries}).\n");
Console.WriteLine(ex);
}
}
private static SearchIndex HandleSynonymMapsToFields(SearchIndex index, string action)
{
foreach (var fields in AzureSearchSettings.SynonymsFields)
{
var synonymMapNames = index.Fields.First(f => f.Name == fields).SynonymMapNames;
if (action == SynonymsOpera.Create)
synonymMapNames.Add(AzureSearchSettings.SynonymsName);
else
synonymMapNames.Clear();
}
return index;
}
private static string SynonymsString()
{
// TODO:取得同義詞
return "喫肉趣, 御牧牛, 台畜\n卜蜂,美特多,洽富氣冷雞 => 雞肉";
}
private SearchIndexer PrepareSearchIndexer()
{
return new SearchIndexer(_settings.IndexerName, _settings.DataSourceName, _indexName)
{
Parameters = new IndexingParameters
{
Configuration =
{
{ "parsingMode", "jsonArray" },
{ "dataToExtract", "contentAndMetadata" }
},
MaxFailedItems = 0,
MaxFailedItemsPerBatch = 0
}
};
}
/// <summary>
/// 建立同義詞
/// </summary>
public void CreateOrUpdateSynonymMap()
{
var synonymMap = new SynonymMap(AzureSearchSettings.SynonymsName, SynonymsString());
_indexClient.CreateOrUpdateSynonymMap(synonymMap);
Console.WriteLine("上傳同義詞 OK.");
}
/// <summary>
/// 刪除同義詞
/// </summary>
public void DeleteSynonymMap()
{
_indexClient.DeleteSynonymMap(AzureSearchSettings.SynonymsName);
Console.WriteLine("刪除同義詞 OK.");
}
/// <summary>
/// 刪除索引
/// </summary>
public void DeleteIndexIfExists()
{
var isExistIndex = _indexClient.GetIndexNames().Any(index => index == _indexName);
if (!isExistIndex) return;
_indexClient.DeleteIndex(_indexName);
Console.WriteLine("清除索引 OK.");
}
/// <summary>
/// 建立 INDEX
/// </summary>
/// <typeparam name="T">索引資料類別</typeparam>
public void CreateIndex<T>()
{
var fieldBuilder = new FieldBuilder();
var searchFields = fieldBuilder.Build(typeof(T));
var definition = new SearchIndex(_indexName, searchFields);
// 搜尋建議
// REF:https://docs.microsoft.com/zh-tw/azure/search/index-add-suggesters
var suggester = new SearchSuggester("sg", AzureSearchSettings.SuggestFields);
definition.Suggesters.Add(suggester);
_indexClient.CreateOrUpdateIndex(definition);
Console.WriteLine($"建立索引:[{_indexName}] OK.");
}
/// <summary>
/// 更新同義詞索引欄位
/// </summary>
/// <param name="action">新增或移除</param>
public void HandleSynonymsWhenIndexExist(string action)
{
// 更新索引欄位定義
var isExistIndex = _indexClient.GetIndexNames().Any(index => index == _indexName);
if (!isExistIndex) return;
// Console.WriteLine($"索引存在,{(action == SynonymsOpera.Create ? "新增" : "移除")}索引同義詞 ...");
// foreach (var field in AzureSearchSettings.SynonymsFields) Console.WriteLine($"同義詞欄位:{field}");
HandleSynonymsIndexSafely(action);
}
/// <summary>
/// 執行索引子
/// </summary>
public void RunIndexer()
{
Console.WriteLine("Running Blob Storage indexer...\n");
try
{
_indexerClient.RunIndexer(_settings.IndexerName);
}
catch (RequestFailedException ex) when (ex.Status == 429)
{
Console.WriteLine("Failed to run indexer: {0}", ex.Message);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
Console.WriteLine("執行 Indexer OK.");
}
/// <summary>
/// 建立 Indexer
/// </summary>
public void CreateOrUpdateIndexer()
{
var blobIndexer = PrepareSearchIndexer();
// Reset the indexer if it already exists
try
{
_indexerClient.GetIndexer(blobIndexer.Name);
//Rest the indexer if it exists.
_indexerClient.ResetIndexer(blobIndexer.Name);
}
catch (RequestFailedException ex) when (ex.Status == 404)
{
}
_indexerClient.CreateOrUpdateIndexer(blobIndexer);
Console.WriteLine($"建立 Indexer:[{blobIndexer.Name}] OK.");
}
/// <summary>
/// 建立 DataSource
/// </summary>
public void CreateOrUpdateDataSourceConnection()
{
var blobDataSource = PrepareDataSourceConnection();
// The blob data source does not need to be deleted if it already exists,
// but the connection string might need to be updated if it has changed.
_indexerClient.CreateOrUpdateDataSourceConnection(blobDataSource);
Console.WriteLine($"建立 Data Source:[{blobDataSource.Name}] OK.");
}
}
呼叫端: init
protected override void Main(string[] param)
{
// 建立同義詞
_helper.CreateOrUpdateSynonymMap();
// 確保同名的 INDEX 不存在
_helper.DeleteIndexIfExists();
// 建立 INDEX
_helper.CreateIndex<MyProduct>();
// 建立 DataSource
_helper.CreateOrUpdateDataSourceConnection();
// 建立 Indexer
_helper.CreateOrUpdateIndexer();
_helper.RunIndexer();
Console.WriteLine("Done!");
}
呼叫端: Synonyms Create or Remove
protected override void Main(string[] param)
{
switch (param[1])
{
case Opera.Create:
// 建立同義詞
_helper.CreateOrUpdateSynonymMap();
// 更新同義詞索引欄位 - 新增
_helper.HandleSynonymsWhenIndexExist(SynonymsOpera.Create);
break;
case Opera.Remove:
// 刪除同義詞
_helper.DeleteSynonymMap();
// 更新同義詞索引欄位 - 移除
_helper.HandleSynonymsWhenIndexExist(SynonymsOpera.Remove);
break;
default:
Console.WriteLine("Command Error , 'create' or 'remove' only");
break;
}
}
呼叫端: DataSrouce Create or Update
protected override void Main(string[] param)
{
// 建立 DataSource
_helper.CreateOrUpdateDataSourceConnection();
}
呼叫端: Index Create or Remove
protected override void Main(string[] param)
{
switch (param[1])
{
case Opera.Create:
// 建立索引
_helper.CreateIndex<MyProduct>();
break;
case Opera.Remove:
// 刪除索引
_helper.DeleteIndexIfExists();
break;
default:
Console.WriteLine("Command Error , 'create' or 'remove' only");
break;
}
}
呼叫端: Indexer Create or Run
protected override void Main(string[] param)
{
switch (param[1])
{
case Opera.Create:
// 建立 Indexer
_helper.CreateOrUpdateIndexer();
break;
case Opera.Run:
// 執行索引子
_helper.RunIndexer();
break;
default:
Console.WriteLine("Command Error , 'create' or 'run' only");
break;
}
}
後記
其實在實做的過程還有很多細節需要注意,這些東西就很難一一說明,但是官方文件大部分都有提到,如果沒有提到的部分,可以多參考一下 API 的文件,也可以從中取得相關資訊,整體來說文件還蠻詳細,但是就是需要花時間找,而且大部分都有官方範例可以看,需要考量的反而是制訂解決方案,還有決定付費層級,如果只是自己練習的話,建議還是開個新帳號去申請試用,趁著免費的期間熟悉吧。