Share via


教學課程:建立電話號碼的自定義分析器

在搜尋解決方案中,具有複雜模式或特殊字元的字串可能會是一項挑戰,因為 預設分析器 會去除或錯誤解譯模式有意義的部分,而導致使用者找不到預期的資訊時,搜尋體驗不佳。 電話 數位是難以分析的字串傳統範例。 它們以各種格式提供,且包含預設分析器忽略的特殊字元。

本教學課程使用電話號碼作為其主題,仔細查看模式數據的問題,並示範如何使用 自定義分析器解決問題。 此處所述的方法可用於電話號碼,或針對具有相同特性的欄位進行調整,例如 URL、電子郵件、郵遞區號和日期。

在本教學課程中,您會使用 REST 用戶端和 Azure AI 搜尋 REST API 來:

  • 了解問題
  • 開發處理電話號碼的初始自定義分析器
  • 測試自定義分析器
  • 反覆運算自定義分析器設計,以進一步改善結果

必要條件

本教學課程需要下列服務和工具。

下載檔案

本教學課程的原始程式碼是 Azure-Samples/azure-search-rest-samples GitHub 存放庫中的 custom-analyzer.rest 檔案。

複製金鑰和 URL

本教學課程中的 REST 呼叫需要搜尋服務端點和系統管理 API 金鑰。 您可以從 Azure 入口網站 取得這些值。

  1. 登入 Azure 入口網站,流覽至 [概觀] 頁面,然後複製 URL。 範例端點看起來會像是 https://mydemo.search.windows.net

  2. 在 [設定> Keys] 下,複製管理金鑰。 管理員 索引鍵可用來新增、修改和刪除物件。 有兩個可交換的系統管理密鑰。 複製任一個。

    Screenshot of the URL and API keys in the Azure portal.

有效的 API 金鑰會根據每個要求,在傳送要求的應用程式與處理要求的搜尋服務之間建立信任。

建立初始索引

  1. 在 Visual Studio Code 中開啟新的文字檔。

  2. 將變數設定為您在上一個步驟中收集的搜尋端點和 API 金鑰。

    @baseUrl = PUT-YOUR-SEARCH-SERVICE-URL-HERE
    @apiKey = PUT-YOUR-ADMIN-API-KEY-HERE
    
  3. 以擴展名儲存 .rest 盤案。

  4. 貼上下列範例,以建立名為 phone-numbers-index 且具有兩個字段的小型索引: idphone_number。 我們尚未定義分析器,因此 standard.lucene 預設會使用分析器。

    ### Create a new index
    POST {{baseUrl}}/indexes?api-version=2023-11-01  HTTP/1.1
      Content-Type: application/json
      api-key: {{apiKey}}
    
      {
        "name": "phone-numbers-index",  
        "fields": [
          {
            "name": "id",
            "type": "Edm.String",
            "key": true,
            "searchable": true,
            "filterable": false,
            "facetable": false,
            "sortable": true
          },
          {
            "name": "phone_number",
            "type": "Edm.String",
            "sortable": false,
            "searchable": true,
            "filterable": false,
            "facetable": false
          }
        ]
      }
    
  5. 選取 [ 傳送要求]。 您應該有 HTTP/1.1 201 Created 回應,而響應主體應該包含索引架構的 JSON 表示法。

  6. 使用包含各種電話號碼格式的檔,將數據載入索引中。 這是您的測試數據。

    ### Load documents
    POST {{baseUrl}}/indexes/phone-numbers-index/docs/index?api-version=2023-11-01  HTTP/1.1
      Content-Type: application/json
      api-key: {{apiKey}}
    
      {
        "value": [
          {
            "@search.action": "upload",  
            "id": "1",
            "phone_number": "425-555-0100"
          },
          {
            "@search.action": "upload",  
            "id": "2",
            "phone_number": "(321) 555-0199"
          },
          {  
            "@search.action": "upload",  
            "id": "3",
            "phone_number": "+1 425-555-0100"
          },
          {  
            "@search.action": "upload",  
            "id": "4",  
            "phone_number": "+1 (321) 555-0199"
          },
          {
            "@search.action": "upload",  
            "id": "5",
            "phone_number": "4255550100"
          },
          {
            "@search.action": "upload",  
            "id": "6",
            "phone_number": "13215550199"
          },
          {
            "@search.action": "upload",  
            "id": "7",
            "phone_number": "425 555 0100"
          },
          {
            "@search.action": "upload",  
            "id": "8",
            "phone_number": "321.555.0199"
          }
        ]  
      }
    
  7. 讓我們嘗試一些類似使用者可能輸入的查詢。 使用者可以以任意數目的格式搜尋 (425) 555-0100 ,但仍預期會傳回結果。 從搜尋 (425) 555-0100開始:

    ### Search for a phone number
    GET {{baseUrl}}/indexes/phone-numbers-index/docs/search?api-version=2023-11-01&search=(425) 555-0100  HTTP/1.1
      Content-Type: application/json
      api-key: {{apiKey}}
    

    查詢會傳回四個預期結果中的三個,但也傳回兩個非預期的結果

    {
        "value": [
            {
                "@search.score": 0.05634898,
                "phone_number": "+1 425-555-0100"
            },
            {
                "@search.score": 0.05634898,
                "phone_number": "425 555 0100"
            },
            {
                "@search.score": 0.05634898,
                "phone_number": "425-555-0100"
            },
            {
                "@search.score": 0.020766128,
                "phone_number": "(321) 555-0199"
            },
            {
                "@search.score": 0.020766128,
                "phone_number": "+1 (321) 555-0199"
            }
        ]
    }
    
  8. 讓我們再試一次,而不使用任何格式設定: 4255550100

     ### Search for a phone number
     GET {{baseUrl}}/indexes/phone-numbers-index/docs/search?api-version=2023-11-01&search=4255550100  HTTP/1.1
       Content-Type: application/json
       api-key: {{apiKey}}
    

    此查詢會更糟,只 傳回四個正確相符專案的其中一個。

    {
        "value": [
            {
                "@search.score": 0.6015292,
                "phone_number": "4255550100"
            }
        ]
    }
    

如果您發現這些結果令人困惑,您並不孤單。 在下一節中,讓我們深入探討為什麼取得這些結果。

檢閱分析器的運作方式

若要了解這些搜尋結果,我們需要瞭解分析器正在執行的動作。 從該處,我們可以使用分析 API 測試預設分析器,為設計更符合我們需求的分析器提供基礎。

分析器是全文搜尋引擎元件,負責處理查詢字串和索引檔中的文字。 不同的分析器會根據案例以不同的方式操作文字。 在此案例中,我們需要建置專為電話號碼量身打造的分析器。

分析器包含三個元件:

在下圖中,您可以看到這三個元件如何一起運作,以標記句子:

Diagram of Analyzer process to tokenize a sentence

這些令牌接著會儲存在反向索引中,以允許快速、全文搜索。 反向索引會藉由將語彙分析期間擷取的所有唯一字詞對應至發生時的檔,來啟用全文搜索。 您可以在下圖中看到範例:

Example inverted index

所有搜尋都會歸結為搜尋儲存在反向索引中的字詞。 當使用者發出查詢時:

  1. 會剖析查詢,並分析查詢字詞。
  2. 然後掃描反向索引是否有具有相符字詞的檔。
  3. 最後,擷取的檔會依 評分演算法進行排名。

Diagram of Analyzer process ranking similarity

如果查詢字詞不符合反向索引中的字詞,則不會傳回結果。 若要深入了解查詢的運作方式,請參閱本文的 全文搜索

注意

部分字詞查詢 是此規則的重要例外狀況。 這些查詢(前置詞查詢、通配符查詢、regex 查詢)會略過與一般詞彙查詢不同的語彙分析程式。 在與索引中的字詞進行比對之前,部分字詞只會小寫。 如果分析器未設定為支援這些類型的查詢,您通常會收到非預期的結果,因為索引中沒有相符字詞。

使用分析 API 測試分析器

Azure AI 搜尋提供 分析 API ,可讓您測試分析器以瞭解其處理文字的方式。

使用下列要求呼叫分析 API:

POST {{baseUrl}}/indexes/phone-numbers-index/analyze?api-version=2023-11-01  HTTP/1.1
  Content-Type: application/json
  api-key: {{apiKey}}

  {
    "text": "(425) 555-0100",
    "analyzer": "standard.lucene"
  }

API 會使用您指定的分析器,傳回從文字擷取的令牌。 標準 Lucene 分析器會將電話號碼分割成三個不同的令牌:

{
    "tokens": [
        {
            "token": "425",
            "startOffset": 1,
            "endOffset": 4,
            "position": 0
        },
        {
            "token": "555",
            "startOffset": 6,
            "endOffset": 9,
            "position": 1
        },
        {
            "token": "0100",
            "startOffset": 10,
            "endOffset": 14,
            "position": 2
        }
    ]
}

相反地,未經標點符號格式化的電話號碼 4255550100 會標示為單一令牌。

{
  "text": "4255550100",
  "analyzer": "standard.lucene"
}

回應:

{
    "tokens": [
        {
            "token": "4255550100",
            "startOffset": 0,
            "endOffset": 10,
            "position": 0
        }
    ]
}

請記住,查詢字詞和索引文件都會進行分析。 回想上一個步驟中的搜尋結果,我們可以開始瞭解傳回這些結果的原因。

在第一個查詢中,會傳回非預期的電話號碼,因為其中一個令牌符合 555我們搜尋的其中一個字詞。 在第二個查詢中,只會傳回一個數位,因為它是唯一具有相符 4255550100令牌的記錄。

建置自定義分析器

既然我們已經瞭解所看到的結果,讓我們建置自定義分析器來改善令牌化邏輯。

目標是要針對電話號碼提供直覺式搜尋,無論查詢或索引字串的格式為何。 為了達成此結果,我們將指定字元篩選令牌化程式和令牌篩選

字元篩選

字元篩選可用來處理文字,再將文字送入令牌化程式。 字元篩選的常見用法包括篩選 HTML 元素或取代特殊字元。

針對電話號碼,我們想要移除空格符和特殊字元,因為並非所有電話號碼格式都包含相同的特殊字元和空格。

"charFilters": [
    {
      "@odata.type": "#Microsoft.Azure.Search.MappingCharFilter",
      "name": "phone_char_mapping",
      "mappings": [
        "-=>",
        "(=>",
        ")=>",
        "+=>",
        ".=>",
        "\\u0020=>"
      ]
    }
  ]

篩選條件會-)+(.從輸入中移除和空格。

輸入 輸出
(321) 555-0199 3215550199
321.555.0199 3215550199

權杖化工具

令牌化程式會一路上將文字分割成標記,並捨棄某些字元,例如標點符號。 在許多情況下,標記化的目標是將句子分割成個別單字。

在此案例中,我們將使用關鍵詞 Tokenizer, keyword_v2因為我們想要將電話號碼擷取為單一字詞。 請注意,這不是解決此問題的唯一方法。 請參閱下方的 替代方法 一節。

關鍵詞 Tokenizer 一律會輸出與單一字詞相同的文字。

輸入 輸出
The dog swims. [The dog swims.]
3215550199 [3215550199]

權杖篩選

令牌篩選會篩選掉或修改Tokenizer所產生的令牌。 令牌篩選的其中一個常見用法是使用小寫令牌篩選來小寫所有字元。 另一個常見的用法是篩選出 停用字詞, 例如 theandis

雖然我們不需要針對此案例使用其中一個篩選條件,但我們會使用 nGram 令牌篩選來允許部分搜尋電話號碼。

"tokenFilters": [
  {
    "@odata.type": "#Microsoft.Azure.Search.NGramTokenFilterV2",
    "name": "custom_ngram_filter",
    "minGram": 3,
    "maxGram": 20
  }
]

NGramTokenFilterV2

nGram_v2標記篩選器會根據 minGrammaxGram 參數,將令牌分割成指定大小的 n-gram。

對於電話分析器,我們設定 minGram3 ,因為這是我們預期用戶搜尋的最短子字元串。 maxGram 設定為 20 ,以確保所有電話號碼,即使有擴充功能,也會放入單一 n-gram 中。

n-gram 的不幸副作用是會傳回一些誤判。 我們將在稍後的步驟中修正此問題,方法是為不包含 n-gram 令牌篩選的搜尋建置個別分析器。

輸入 輸出
[12345] [123, 1234, 12345, 234, 2345, 345]
[3215550199] [321, 3215, 32155, 321555, 3215550, 32155501, 321555019, 3215550199, 215, 2155, 21555, 215550, ... ]

分析器

有了我們的字元篩選器、Tokenizer 和令牌篩選,我們就可以定義分析器。

"analyzers": [
  {
    "@odata.type": "#Microsoft.Azure.Search.CustomAnalyzer",
    "name": "phone_analyzer",
    "tokenizer": "keyword_v2",
    "tokenFilters": [
      "custom_ngram_filter"
    ],
    "charFilters": [
      "phone_char_mapping"
    ]
  }
]

在分析 API 中,假設有下列輸入,自定義分析器的輸出如下表所示。

輸入 輸出
12345 [123, 1234, 12345, 234, 2345, 345]
(321) 555-0199 [321, 3215, 32155, 321555, 3215550, 32155501, 321555019, 3215550199, 215, 2155, 21555, 215550, ... ]

輸出數據列中的所有標記都存在於索引中。 如果我們的查詢包含任何字詞,則會傳回電話號碼。

使用新的分析器重建

  1. 刪除目前的索引:

     ### Delete the index
     DELETE {{baseUrl}}/indexes/phone-numbers-index?api-version=2023-11-01 HTTP/1.1
         api-key: {{apiKey}}
    
  2. 使用新的分析器重新建立索引。 此索引架構會新增自定義分析器定義,以及電話號碼字段上的自定義分析器指派。

    ### Create a new index
    POST {{baseUrl}}/indexes?api-version=2023-11-01  HTTP/1.1
      Content-Type: application/json
      api-key: {{apiKey}}
    
    {
        "name": "phone-numbers-index-2",  
        "fields": [
          {
              "name": "id",
              "type": "Edm.String",
              "key": true,
              "searchable": true,
              "filterable": false,
              "facetable": false,
              "sortable": true
          },
          {
              "name": "phone_number",
              "type": "Edm.String",
              "sortable": false,
              "searchable": true,
              "filterable": false,
              "facetable": false,
              "analyzer": "phone_analyzer"
          }
        ],
        "analyzers": [
            {
              "@odata.type": "#Microsoft.Azure.Search.CustomAnalyzer",
              "name": "phone_analyzer",
              "tokenizer": "keyword_v2",
              "tokenFilters": [
              "custom_ngram_filter"
            ],
            "charFilters": [
              "phone_char_mapping"
              ]
            }
          ],
          "charFilters": [
            {
              "@odata.type": "#Microsoft.Azure.Search.MappingCharFilter",
              "name": "phone_char_mapping",
              "mappings": [
                "-=>",
                "(=>",
                ")=>",
                "+=>",
                ".=>",
                "\\u0020=>"
              ]
            }
          ],
          "tokenFilters": [
            {
              "@odata.type": "#Microsoft.Azure.Search.NGramTokenFilterV2",
              "name": "custom_ngram_filter",
              "minGram": 3,
              "maxGram": 20
            }
          ]
        }
    

測試自定義分析器

重新建立索引之後,您現在可以使用下列要求來測試分析器:

POST {{baseUrl}}/indexes/tutorial-first-analyzer/analyze?api-version=2023-11-01  HTTP/1.1
  Content-Type: application/json
  api-key: {{apiKey}} 

  {
    "text": "+1 (321) 555-0199",
    "analyzer": "phone_analyzer"
  }

您現在應該會看到電話號碼所產生的權杖集合:

{
    "tokens": [
        {
            "token": "132",
            "startOffset": 1,
            "endOffset": 17,
            "position": 0
        },
        {
            "token": "1321",
            "startOffset": 1,
            "endOffset": 17,
            "position": 0
        },
        {
            "token": "13215",
            "startOffset": 1,
            "endOffset": 17,
            "position": 0
        },
        ...
        ...
        ...
    ]
}

修改自定義分析器以處理誤判

使用自定義分析器針對索引進行一些範例查詢之後,您會發現召回已改善,而且現在會傳回所有相符的電話號碼。 不過,n-gram 令牌篩選也會傳回一些誤判。 這是 n-gram 令牌篩選的常見副作用。

為了防止誤判,我們將建立個別的分析器來查詢。 此分析器與上一個分析器相同,不同之處在於它會省略 custom_ngram_filter

    {
      "@odata.type": "#Microsoft.Azure.Search.CustomAnalyzer",
      "name": "phone_analyzer_search",
      "tokenizer": "custom_tokenizer_phone",
      "tokenFilters": [],
      "charFilters": [
        "phone_char_mapping"
      ]
    }

在索引定義中,我們接著同時 indexAnalyzer 指定 和 searchAnalyzer

    {
      "name": "phone_number",
      "type": "Edm.String",
      "sortable": false,
      "searchable": true,
      "filterable": false,
      "facetable": false,
      "indexAnalyzer": "phone_analyzer",
      "searchAnalyzer": "phone_analyzer_search"
    }

有了這項變更,您就會全部設定。 以下是後續步驟:

  1. 刪除索引。

  2. 在新增自定義分析器 (phone_analyzer-search) 並將該分析器指派給 phone-number 字段的 searchAnalyzer 屬性之後,重新建立索引。

  3. 重載數據。

  4. 重新測試查詢,以確認搜尋如預期般運作。 如果您使用範例檔案,此步驟會建立名為 的第三個 phone-number-index-3索引。

替代方法

上一節所述的分析器是設計來最大化搜尋的彈性。 不過,其代價是將許多可能不重要的詞彙儲存在索引中。

下列範例顯示替代分析器,在令牌化方面更有效率,但有缺點。

根據的 14255550100輸入,分析器無法以邏輯方式將電話號碼區塊化。 例如,它無法將國家/地區代碼 1與區域代碼 425區隔開。 如果使用者未在其搜尋中包含國家/地區代碼,則此差異會導致電話號碼不會傳回。

"analyzers": [
  {
    "@odata.type": "#Microsoft.Azure.Search.CustomAnalyzer",
    "name": "phone_analyzer_shingles",
    "tokenizer": "custom_tokenizer_phone",
    "tokenFilters": [
      "custom_shingle_filter"
    ]
  }
],
"tokenizers": [
  {
    "@odata.type": "#Microsoft.Azure.Search.StandardTokenizerV2",
    "name": "custom_tokenizer_phone",
    "maxTokenLength": 4
  }
],
"tokenFilters": [
  {
    "@odata.type": "#Microsoft.Azure.Search.ShingleTokenFilter",
    "name": "custom_shingle_filter",
    "minShingleSize": 2,
    "maxShingleSize": 6,
    "tokenSeparator": ""
  }
]

您可以在下列範例中看到電話號碼會分割成您通常預期使用者要搜尋的區塊。

輸入 輸出
(321) 555-0199 [321, 555, 0199, 321555, 5550199, 3215550199]

根據您的需求,這可能是一個更有效率的問題方法。

重要心得

本教學課程示範建置和測試自定義分析器的程式。 您已建立索引、已編製索引的數據,然後針對索引進行查詢,以查看傳回的搜尋結果。 從該處,您已使用分析 API 來查看語彙分析程式的運作情形。

雖然本教學課程中定義的分析器提供簡單的解決方案來搜尋電話號碼,但此相同程式可用來針對任何共享類似特性的案例建置自定義分析器。

清除資源

當您在自己的訂用帳戶中工作時,最好移除項目結尾不再需要的資源。 資源若繼續執行,將需付費。 您可以個別刪除資源,或刪除資源群組以刪除整組資源。

您可以使用左導覽窗格中的 [所有資源] 或 [資源群組] 連結,在入口網站中尋找和管理資源。

下一步

既然您已熟悉如何建立自定義分析器,讓我們看看可供您建立豐富搜尋體驗的所有不同篩選、令牌化程式和分析器。