Art的辦公桌

有兩個螢幕真好

0%

azure認知搜尋初學

Azure Search DocumentAzure Cognitive Search documentation(認知搜尋文件) 這項服務的 Nuget 套件,也就是以前說的 Azure Search,這項服務可以讓使用者的搜尋體驗變得較為友善-透過自動完成、同義字比對、模糊比對、模式比對、篩選和排序

一般情況下大部分我們自己做的搜尋都還在篩選排序等等,要做到同義字比對與模糊比對就比較複雜,或者說需要付出較高的企業成本,而採用Azure Search就是一個可以考慮的解決方案了,接下來就著重在如何使用的部分,透過一個簡單的範例來說明。

intro

搜尋服務最核心的就是下面的三個東西,我們首先需要先準備資料,放在資料來源當中,並且建立好索引,也就是資料與欄位的對應關係,並確認該欄位的一些屬性,例如是否可搜尋、可排序等等;最後再透過索引子實際執行,將資料來源的資料經過處理後放到索引之內。之後就可以透過 API 去做查詢,也可以透過 Azure Search Document這個 Nuget 套件查詢資料

  1. 索引:透過 SDK 或 REST API 發出請求,針對索引欄位作操作
  2. 索引子:索引&資料來源中間愛的橋樑
  3. 資料來源:支援 Azure Blob、Azure SQL Database…等等

詳細的設定及文件還是需要參考官方文件比較清楚,這裡只是稍微說明一下

資料來源

能夠作為資料來源的有很多,這邊以 Azure Blob 容器為例子,指定好連接字串容器名稱就可以使用這裡也有很多設定值可以調整,也要考量到自己的規劃來做設定。像是追蹤刪除就會影響到你索引的定義,還有資料來源上傳的檔案 Schema 要怎麼規劃,在我的使用情境下,我是需要追蹤刪除的,所以我採用了使用一個欄位IsDel來做,只要這個欄位是 1 就代表這筆資料需要被刪除

  1. 教學課程:使用 .NET SDK 為 Azure SQL 資料編製索引
  2. 教學課程:使用 .NET SDK 從多個資料來源編製索引
  3. Sample Code - 1
  4. Sample Code - 2
  5. 適用于 Azure 認知搜尋的 .NET (c # ) 程式碼範例

索引

在這邊設定的就是定義好每一筆資料的欄位,是否可以被搜尋、篩選、取出、排序等等,比較重要的部分就是要定義那個欄位是Key值,當然也要加上一個虛刪除的欄位

假設我要做一個商品查詢,那索引應該要有哪些欄位呢?

  • 搜尋結果:前端會用到的欄位,如商品標題,價格等等
  • 篩選條件:商品數量、銷售狀態、分類等等
  • 關鍵字查詢:商品名稱、商品明細
  • 統計資訊:分類等等
  • 排序條件:商品庫存、價格等等

決定好有哪些欄位之後,依據各欄位用途設定屬性,使其可以被取出、篩選、排序、搜尋、Facet

  1. 可取出:大概的意思就是跟 SQL 的 Select 差不多吧。基本上不需要被取出的欄位也沒必要上傳到資料來源了吧(除非要做篩選排序搜尋等等)
  2. 可排序:就是字面上的意思,該欄位可以用來排序
  3. 可篩選:這個意思就是類似 SQL 的 Where 條件,例如當我設定商品類型可以篩選,那我就可以透過篩選商品類型的值,取得某類型的商品
  4. 可 Facet:假設我有三筆資料,兩筆是 3C 用品,一筆是文具,當我加上了參數要他回傳 Facet,並且搜尋所有商品,他就會額外給我一個統計資料,告訴我 3C 有兩筆,文具有一筆;大概就是這樣的意思;另一種解釋是:用該面向去看你的搜尋結果
  5. 可搜尋:設定為可搜尋的話,後續需要指定要用哪一種分析器,應該是跟語意分析、拆字有關係吧…

在這裡要注意資料庫的欄位定義與索引的欄位定義資料型態的問題,要看一下是否相容

索引子

索引子的設定相對較為單純,這裡因為我採用的是 json 的資料來源,所以parsingMode設定為 jsonArray,代表的是 Json 檔案內是一個 json 物件的集合

同義詞建立

建立、更新和刪除同義字地圖永遠是整份檔的作業,這表示您無法以累加方式更新或刪除同義字地圖的部分。 即使只更新單一規則,也需要重載。

  1. 只能每次重新做

  2. 做完就需要去更新索引有哪些欄位使用到了同義字

  3. 新增同義字

  4. Sample Code

  5. Example: Add synonyms for Azure Cognitive Search in C#

搜尋建議

  1. 建立建議工具,以在查詢中啟用自動完成和建議的結果

範例程式碼

基於開發時期常常會需要修改調整,如果都透過手動來建立索引等等動作,會非常的煩人,所以寫個 Console 專案透過參數來做這些事情會比較方便,這邊就節錄我自己寫的一個 Helper,全都是參考官方範例改的。下面的程式碼僅供參考

AppSettingConfig.json

1
2
3
4
5
6
7
8
9
10
11
12
{
"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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

1
2
3
4
5
protected override void Main(string[] param)
{
// 建立 DataSource
_helper.CreateOrUpdateDataSourceConnection();
}

呼叫端: Index Create or Remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}
1
2


後記

其實在實做的過程還有很多細節需要注意,這些東西就很難一一說明,但是官方文件大部分都有提到,如果沒有提到的部分,可以多參考一下 API 的文件,也可以從中取得相關資訊,整體來說文件還蠻詳細,但是就是需要花時間找,而且大部分都有官方範例可以看,需要考量的反而是制訂解決方案,還有決定付費層級,如果只是自己練習的話,建議還是開個新帳號去申請試用,趁著免費的期間熟悉吧。