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 物件的集合
同義詞建立 建立、更新和刪除同義字地圖永遠是整份檔的作業,這表示您無法以累加方式更新或刪除同義字地圖的部分。 即使只更新單一規則,也需要重載。
只能每次重新做
做完就需要去更新索引有哪些欄位使用到了同義字
新增同義字
Sample Code
Example: Add synonyms for Azure Cognitive Search in C#
搜尋建議
建立建議工具,以在查詢中啟用自動完成和建議的結果
範例程式碼 基於開發時期常常會需要修改調整,如果都透過手動來建立索引等等動作,會非常的煩人,所以寫個 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" ; public const string SynonymsName = "kw-synonym" ; public static readonly string [] SynonymsFields = { "ProductName" , "Detail" }; public static readonly string [] SuggestFields = { "ProductName" , "Detail" }; public string ServiceName { get ; set ; } = string .Empty; public string ApiKey { get ; set ; } = string .Empty; public string IndexName { get ; set ; } = string .Empty; public Uri ServiceEndPoint => new ($"https://{ServiceName} .search.windows.net/" ); public string DataSourceName { get ; set ; } = string .Empty; public string DataSourceConnectionString { get ; set ; } = string .Empty; public string DataSourceContainer { get ; set ; } = string .Empty; public string DataSourceContainerQuery { get ; set ; } = string .Empty; 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 { [SimpleField(IsKey = true) ] public string ProductID { get ; set ; } [SimpleField ] public string Pic { get ; set ; } [SimpleField ] public long Price { get ; set ; } [SimpleField(IsFilterable = true) ] public int Qty { get ; set ; } [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.StandardLucene, SynonymMapNames = new[ ] { "kw-synonym" })] public string ProductName { get ; set ; } [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.StandardLucene, SynonymMapNames = new[ ] { "kw-synonym" })] public string Detail { get ; set ; } [SimpleField(IsFacetable = true, IsFilterable = true) ] public int ProductType { get ; set ; } [SimpleField(IsSortable = true) ] public int Sort { get ; set ; } [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); _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 () { 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 } }; } public void CreateOrUpdateSynonymMap () { var synonymMap = new SynonymMap(AzureSearchSettings.SynonymsName, SynonymsString()); _indexClient.CreateOrUpdateSynonymMap(synonymMap); Console.WriteLine("上傳同義詞 OK." ); } public void DeleteSynonymMap () { _indexClient.DeleteSynonymMap(AzureSearchSettings.SynonymsName); Console.WriteLine("刪除同義詞 OK." ); } public void DeleteIndexIfExists () { var isExistIndex = _indexClient.GetIndexNames().Any(index => index == _indexName); if (!isExistIndex) return ; _indexClient.DeleteIndex(_indexName); Console.WriteLine("清除索引 OK." ); } public void CreateIndex <T >() { var fieldBuilder = new FieldBuilder(); var searchFields = fieldBuilder.Build(typeof (T)); var definition = new SearchIndex(_indexName, searchFields); var suggester = new SearchSuggester("sg" , AzureSearchSettings.SuggestFields); definition.Suggesters.Add(suggester); _indexClient.CreateOrUpdateIndex(definition); Console.WriteLine($"建立索引:[{_indexName} ] OK." ); } public void HandleSynonymsWhenIndexExist (string action ) { var isExistIndex = _indexClient.GetIndexNames().Any(index => index == _indexName); if (!isExistIndex) return ; HandleSynonymsIndexSafely(action); } 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." ); } public void CreateOrUpdateIndexer () { var blobIndexer = PrepareSearchIndexer(); try { _indexerClient.GetIndexer(blobIndexer.Name); _indexerClient.ResetIndexer(blobIndexer.Name); } catch (RequestFailedException ex) when (ex.Status == 404 ) { } _indexerClient.CreateOrUpdateIndexer(blobIndexer); Console.WriteLine($"建立 Indexer:[{blobIndexer.Name} ] OK." ); } public void CreateOrUpdateDataSourceConnection () { var blobDataSource = PrepareDataSourceConnection(); _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(); _helper.DeleteIndexIfExists(); _helper.CreateIndex<MyProduct>(); _helper.CreateOrUpdateDataSourceConnection(); _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 ){ _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: _helper.CreateOrUpdateIndexer(); break ; case Opera.Run: _helper.RunIndexer(); break ; default : Console.WriteLine("Command Error , 'create' or 'run' only" ); break ; } }
後記 其實在實做的過程還有很多細節需要注意,這些東西就很難一一說明,但是官方文件大部分都有提到,如果沒有提到的部分,可以多參考一下 API 的文件,也可以從中取得相關資訊,整體來說文件還蠻詳細,但是就是需要花時間找,而且大部分都有官方範例可以看,需要考量的反而是制訂解決方案,還有決定付費層級,如果只是自己練習的話,建議還是開個新帳號去申請試用,趁著免費的期間熟悉吧。