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

{
  "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 的文件,也可以從中取得相關資訊,整體來說文件還蠻詳細,但是就是需要花時間找,而且大部分都有官方範例可以看,需要考量的反而是制訂解決方案,還有決定付費層級,如果只是自己練習的話,建議還是開個新帳號去申請試用,趁著免費的期間熟悉吧。