Руководство. Создание пользовательского анализатора для номеров телефонов

В решениях поиска строки, имеющие сложные шаблоны или специальные символы, могут быть проблемой для работы, так как анализатор по умолчанию удаляет или неправильно интерпретирует значимые части шаблона, что приводит к плохому поиску, когда пользователи не могут найти ожидаемые сведения. Телефон числа — это классический пример строк, которые трудно проанализировать. Они представлены в различных форматах, и они включают специальные символы, которые анализатор по умолчанию игнорирует.

С номерами телефонов в качестве темы в этом руководстве рассматривается проблемы шаблонных данных и показано, как решить эту проблему с помощью пользовательского анализатора. Описанный здесь подход можно использовать как есть для номеров телефонов или адаптирован для полей с теми же характеристиками (шаблонными, с особыми символами), такими как URL-адреса, электронные письма, почтовые коды и даты.

В этом руководстве вы используете rest-клиент и ИНТЕРФЕЙСы REST API службы "Поиск ИИ Azure" , чтобы:

  • Определение проблемы
  • Разработка начального пользовательского анализатора для обработки номеров телефонов
  • Тестирование пользовательского анализатора
  • Итерацию по пользовательскому конструктору анализатора для дальнейшего улучшения результатов

Необходимые компоненты

Для выполнения инструкций из этого руководства потребуются перечисленные ниже службы и инструменты.

Загрузка файлов

Исходный код для этого руководства — это файл custom-analyzer.rest в репозитории GitHub Azure-Samples/azure-search-rest-samples .

Копирование ключа и 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 с двумя полями: id и phone_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

Если термины запроса не соответствуют терминам в инвертированного индекса, результаты не возвращаются. Чтобы узнать больше о принципах работы запросов, см. статью о полнотекстовом поиске.

Примечание.

Запросы с частичными терминами являются важным исключением из этого правила. Такие запросы (запрос с префиксом, запрос с подстановочными знаками, запрос с регулярными выражениями) обрабатываются в ходе лексического анализа не так, как обычные запросы терминов. Частичные термины переводятся в нижний регистр только при сопоставлении с терминами в индексе. Если в анализаторе не включена поддержка таких типов запросов, вы будете часто получать непредвиденные результаты, так как совпадений терминов в индексе не будет.

Тестовые анализаторы с помощью API анализа

Поиск ИИ Azure предоставляет 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

Токенизаторы

Создатели маркеров разбивают текст на маркеры и удаляют некоторые символы, например знаки пунктуации. Во многих случаях цель разбивки заключается в разделении предложения на отдельные слова.

В таком сценарии мы используем создатель маркеров для получения ключевых слов, keyword_v2, так как телефонный номер должен обрабатываться как один термин. Учтите, что это не единственный способ решения такой проблемы. См. раздел Альтернативные подходы ниже.

Создатели маркеров для получения ключевых слов всегда выводят вводный текст в виде одного термина.

Входные данные Выходные данные
The dog swims. [The dog swims.]
3215550199 [3215550199]

Фильтры токенов

Фильтры маркеров фильтруют или изменяют маркеры, созданные создателем маркеров. Некоторые из распространенных способов использования таких фильтров заключаются в переводе всех символов в нижний регистр с помощью специального фильтра или в фильтрации стоп-слов, например the, and или is.

Хотя в этом сценарии мы не будем использовать такие фильтры, мы применим фильтр маркеров nGram для частичного поиска телефонных номеров.

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

NGramTokenFilterV2

Фильтр маркеров nGram_v2 разделяет маркеры на N-граммы заданного размера с учетом параметров minGram и maxGram.

При использовании анализатора телефонных номеров мы зададим для параметра minGram значение 3, так как это самая короткая подстрока, которую могут ввести пользователи для поиска. Для параметра maxGram задано значение 20, чтобы любой телефонный номер (даже с расширениями) можно было поместить в одну N-грамму.

Но побочным эффектом использования таких N-грамм будет получение некоторых ложноположительных результатов. Мы исправим это на следующем шаге, создав отдельный анализатор для поиска, который не включает фильтр маркеров n-грамма.

Входные данные Выходные данные
[12345] [123, 1234, 12345, 234, 2345, 345]
[3215550199] [321, 3215, 32155, 321555, 3215550, 32155501, 321555019, 3215550199, 215, 2155, 21555, 215550, ... ]

Анализатор

При наличии фильтров символов, создателя маркеров и фильтров маркеров мы готовы определить анализатор.

"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-граммами. Это распространенный побочный эффект использования такого фильтра.

Чтобы предотвратить возникновение ложноположительных результатов, мы создадим отдельный анализатор для отправки запросов. Этот анализатор идентичен предыдущему, за исключением того, что он не указывает.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 анализа для просмотра лексического анализа в действии.

Хотя анализатор, определенный в этом руководстве, предлагает простое решение для поиска по номерам телефонов, этот же процесс можно использовать для создания пользовательского анализатора для любого сценария, который использует аналогичные характеристики.

Очистка ресурсов

Если вы работаете в своей подписке, после завершения проекта целесообразно удалить ресурсы, которые вам больше не нужны. Ресурсы, которые продолжат работать, могут быть платными. Вы можете удалить ресурсы по отдельности либо удалить всю группу ресурсов.

Просматривать ресурсы и управлять ими можно на портале с помощью ссылок "Все ресурсы" или "Группы ресурсов" в области навигации слева.

Следующие шаги

Теперь, когда вы узнали, как создать пользовательский анализатор, мы рассмотрим разные фильтры, создатели маркеров и анализаторы, которые вы можете использовать для улучшения поиска.