2017年12月8日 星期五

使用 Azure Search 實作資料來源為 Azure Blob Storage 之站內搜尋的方式(檔案內容格式為 Json)

以往遇到要使用站內搜尋的個案時,都會直接使用 Google Site Search 來擋著用,不過該服務似乎 2018 年即將廢棄,因此花了點時間找了些替代方案,最後選擇了 Azure Search 來實作類似的功能,不過網路上的具體範例似乎有點少(可能微軟官方的文件已經寫得很清楚了!?),而我這邊主要是先參考 Demo 大的這篇其餘細節的部分都還是得靠自己爬官方文件來補足,不囉嗦,以下就開始實作過程


1. 首先進入 Azure Search 入口網站(Azure portal),新增一個 Search 服務,如圖所示


2. 接著填入各項資訊完成後按下建立(價格部份可以先選擇免費等級來試用看看)


3. 系統建立服務完成後,從左側選單找尋剛剛我們建立的 Azure Search 服務並進入各項設定畫面如下圖


4. 再來就是要建立資料來源(data source)、索引 (index) 及索引子 (indexer),Azure 提供很多種建立的方式,這邊只示範我使用的方式

方法一:直接在 Azure Search 入口網站裡建立

a. 在設定頁裡找到匯入資料後點選它


b. 依照步驟填完各項資料


c. 緊接著畫面會把 Blob 預設的各項欄位都秀出來,妳可以把用不到的部分都刪除掉,然後新增自己想要的欄位(欄位名稱需對應指定 Blob 資料夾下 Json 檔有的屬性欄位),然後右半部的勾選框如果不是很清楚需不需要勾選時,就先全勾吧,以上完成後按下 OK




d. 接著會到建立 indexer 的畫面,其實 Azure Search 入口網站 提供建立 indexer 的功能有點陽春,沒有地方可以設定 Field mappings 等各項功能,所以 indexer 的部分基本上我都透過方法二來建立,但這邊還是得走完整個流程,就先隨便建一個吧


e. 完成後,就會在設定畫面上看到剛剛建立的索引,等
索引更新完後就能開始體驗 Azure Search 強大的搜尋功能了


方法二:使用 Postman 呼叫 Azure Search API 來建立

a. 開啟 Postman 後,我們要先建立資料來源(Data Source),先從 Azure Search 設定畫面上取得 url,如下圖所示


b. 接著再點選左側選單的 Keys 項目,複製其中一把金鑰


c. 回到 Postman 上,開啟新的連線後,HTTP Method 選 POST,並將剛剛取得的 url 和 key 分別填入網址及 Headers 裡,url 後面需加上 "/datasources?api-version=2016-09-01",如下圖所示


d. 再來先去 Azure 入口網站裡取得欲使用的 Blob Storage Account 連線字串,請參考下圖


e. 之後 Postman 裡點選 Body 選項,資料包裝方式選擇 raw,將以下 JSON 內容貼上,並修改成自己使用的 Blob 資料

{
    "name" : "my-test-blob-datasource",
    "type" : "azureblob",
    "credentials" : { "connectionString" : "DefaultEndpointsProtocol=https;AccountName=yourAccountName;AccountKey=yourKey;" },
    "container" : { "name" : "Your_Container_Name", "query" : "sub_dir/next_dir/../target_dir" },
    "dataDeletionDetectionPolicy" : {
        "@odata.type" :"#Microsoft.Azure.Search.SoftDeleteColumnDeletionDetectionPolicy",     
        "softDeleteColumnName" : "enable",
        "softDeleteMarkerValue" : "false"
    }
} 

name:自訂 DataSource 名稱
type:固定為 azureblob (其它型態以後有機會在另開一篇XD)
credentials.connectionString:貼上剛剛取得的 Blob 連線字串
container.name:你的目標 blob container 名稱
container.query:如果你的 container 下還有其它目錄,則這邊就要填入放 blob 目錄的路徑,沒有的話該參數可省略,可參考此
dataDeletionDetectionPolicy:主要是設定當 blob 指定欄位(softDeleteColumnName)的值變更成 softDeleteMarkerValue 設定的值時,製作的索引會自動將這筆資料刪除,詳細部分可參考此

完成後畫面如下,沒問題後按下送出,回應無錯誤訊息即完成新增一個 data source


f. 接下來,我們要建立索引(index),在 Postman 上,再開啟一個新的連線,HTTP Method 選 POST,並將之前取得的 url 和 key 分別填入網址及 Headers 裡,url 後面加上 "/indexes?api-version=2016-09-01",如下圖所示


g. 然後選擇 Body 選項,資料包裝方式點選 raw,將以下 JSON 內容貼上,並修改成自己使用的欄位資料

{
  "name": "test-azureblob-index",
  "fields": [
   { "name": "id", "type": "Edm.String", "key": true, "retrievable": true, "searchable": true, "sortable": false },
   { "name": "title", "type": "Edm.String", "retrievable": true, "filterable": true, "sortable": false, "facetable": true, "searchable": true },
   { "name": "introduction", "type": "Edm.String", "retrievable": true, "filterable": true, "sortable": false, "facetable": true, "searchable": true }, 
   { "name": "enable", "type": "Edm.Boolean", "retrievable": true, "filterable": true, "sortable": false, "facetable": true },
   { "name": "sort", "type": "Edm.Int32", "retrievable": true, "filterable": true, "sortable": true, "facetable": true },
   { "name": "path", "type": "Edm.String", "retrievable": true, "filterable": true, "sortable": true, "facetable": true, "searchable": true },
   { "name": "updateOn", "type": "Edm.DateTimeOffset", "retrievable": true, "filterable": true, "sortable": true, "facetable": true },
   { "name": "tags", "type":"Collection(Edm.String)", "retrievable": true, "filterable": true, "facetable": true, "searchable": true}
  ],
  "corsOptions":{  
    "allowedOrigins": ["*"]
  }
} 

name:自訂 index 名稱
fields.key:定義該欄位為主鍵
fields.name:定義自己想要用的欄位名稱及型態
fields.type:該欄位的型態類型,其它 fields 屬性詳細定義皆可參考此
corsOptions.allowedOrigins:設定跨網域的網域名稱,["*"] 為接受任何來源,若要設定指定網域名稱,可改為 ["http://www.mydomain.com.tw", "http://www.mydomain02.com.tw", ......]

完成後畫面如下,沒問題後按下送出,回應無錯誤訊息即完成新增一個 index


h. 最後要來建立索引子(indexer),
使用 API 方式來建立 indexer 非常方便且具有很大的彈性,假如我原本的 JSON 檔裡有和索引(index)上的某個欄位對應到,但欄位名稱卻不同(這在現實中很常發生),即可使用 Azure Search API 提供的 mapping 功能,讓來源 JSON 檔上的某個欄位能對應到目標索引上的某個欄位,這樣不同的資料來源但具有相似意義的欄位就能整合在一起(欄位型態需一致),甚至還能對欄位內容做一些基本的過濾處理,以下就直接來實做一個吧

i. 
在 Postman 上,再開啟一個新的連線,HTTP Method 選 POST,並將之前取得的 url 和 key 分別填入網址及 Headers 裡,url 後面加上 "/indexers?api-version=2016-09-01",跟之前相似(網址不同而已),這邊就不附圖了然後一樣選擇 Body 選項,資料包裝方式點選 raw,將以下 JSON 內容貼上,根據以下說明來逐步修改成適合自己的索引子(indexer)

{
  "name" : "my-test-indexer",
  "dataSourceName" : "my-test-blob-datasource",
  "targetIndexName" : "test-azureblob-index",
  "schedule" : { "interval" : "P1D" },
  "parameters" : { "configuration" : { "indexedFileNameExtensions" : ".json", "parsingMode" : "json", "failOnUnsupportedContentType" : false } },
  "fieldMappings" : [
   { "sourceFieldName" : "metadata_storage_path", "targetFieldName" : "id", "mappingFunction" : { "name" : "base64Encode" } },
   { "sourceFieldName" : "metadata_storage_name", "targetFieldName" : "title", "mappingFunction" : { "name" : "extractTokenAtPosition", "parameters" : { "delimiter" : ".", "position" : 0 } }},
   { "sourceFieldName" : "intro", "targetFieldName" : "introduction"},
   { "sourceFieldName" : "order", "targetFieldName" : "sort"},
   { "sourceFieldName" : "metadata_storage_path", "targetFieldName" : "path", "mappingFunction" : { "name" : "extractTokenAtPosition", "parameters" : { "delimiter" : "/", "position" : 7 } } },
   { "sourceFieldName" : "updateTime", "targetFieldName" : "updateOn"}
  ]
}  

name:自訂 indexer 名稱
dataSourceName:來源 DataSource 名稱(參考步驟 e)
targetIndexName:目標 Index 名稱(參考步驟 g)
schedule:自動排程更新時間,P1D 代表每日,PT5M 則代表每5分鐘,詳細規則可參考此
parameters.configuration.indexedFileNameExtensions:只擷取符合指定副檔名的 blob 檔
parameters.configuration.parsingMode:解析模式
parameters.configuration.failOnUnsupportedContentType:解析檔案出現錯誤時忽略該檔案
fieldMappings.sourceFieldName:來源 JSON 檔原欄位名稱
fieldMappings.targetFieldName:目標索引上對應的欄位名稱
fieldMappings.mappingFunction:參考此

完成後畫面如下,沒問題後一樣按下送出,回應無錯誤訊息即完成新增一個 indexer


5. 最後就是實作搜尋程式的部分,可以參考 Demo 大直接使用Azure Search 提供的 REST API,這邊我則是使用 Azure Search .NET SDK,可以直接使用 NuGet 來安裝,以下直接貼出完成的程式碼片段供參考

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
//.......... other namespace ..........
using System.ComponentModel.DataAnnotations;
using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;


namespace MyWeb.Service.SearchExample
{
    public class WidgetService
    {
        //.......... other code ..........
  
        public SearchData Search(string text, int count, int page)    //text:使用者輸入的搜尋字串, count:ㄧ頁要取得的筆數, page:分頁頁碼
        {
            var lastWeekend = DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd'T'HH:mm:ss-00:00");    //可被接受的時間格式
            SearchIndexClient indexClient = new SearchIndexClient("my-test-search-service", "test-azureblob-index", new SearchCredentials("Your Search Service Key"));
            
            SearchParameters parameters = new SearchParameters() {
                Select = new[] { "id", "title", "introduction", "path", "updateOn"},    //選擇只回傳指定的欄位
                SearchMode = SearchMode.Any,    //Any: 任一搜尋條件符合, All: 需全符合搜尋條件                
                Filter = string.Format("enable eq true and (sort ne 0 or (updateOn ge {0}))", lastWeekend),    //篩選條件,更多技巧可參考微軟提供的線上文件
                Top = count,
                Skip = count * (page-1),
                IncludeTotalResultCount = true
            };

            var keyWords = text.Split(' ');
            var searchText = "";
            foreach (var key in keyWords)
            {
                searchText = searchText + "+" + "\"" + key + "\"";
            }
            
            var documents = indexClient.Documents.Search<SearchDocument>(searchText, parameters).Results.Select(r => r.Document);

            long totalCounts = 0;
            long totalPages = 0;
            if (documents.Count != null)
            {
                totalCounts = documents.Count.Value;
                totalPages = (totalCounts % count > 0) ? (totalCounts/count + 1) : (totalCounts/count);
            }

            return new SearchData { Documents = documents, TotalPages = totalPages, TotalCounts = totalCounts };
        }
        
        [DataContract]
        public class SearchData
        {
                [DataMember]
                public List<SearchDocument> Documents { get; set; }
 
                [DataMember]
                public long TotalCounts { get; set; }
 
                [DataMember]
                public long TotalPages { get; set; }
        }
 
        [DataContract]
        public class SearchDocument
        {
                [DataMember]
                public string Id { get; set; }
   
                [DataMember]
                public string Title { get; set; }
   
                [DataMember]
                public int? Sort { get; set; }
 
                [DataMember]
                public string Introduction { get; set; }
 
                [DataMember]
                public string Path { get; set; }
 
                [DataMember]
                public DateTimeOffset? UpdateOn { get; set; }
        }
 
        //.......... other code ..........
 
    }
}

到這邊大致上 Azure Search 的處女秀實作也就告一段落了,當然這也只是 Azure Search 提供的功能的一小部分(已經快把我弄得不要不要的了...)如果有什麼地方寫錯或會錯意的地方也歡迎留言一起討論!




參考資料:




訪客統計