Tutorial: Criar um analisador personalizado para números de telefone

Nas soluções de pesquisa, lidar com cadeias de caracteres com padrões complexos ou caracteres especiais pode ser um desafio porque o analisador padrão remove ou interpreta incorretamente partes significativas de um padrão, o que resulta em uma experiência de pesquisa insatisfatória quando os usuários não conseguem encontrar as informações esperadas. Números de telefone são um exemplo clássico de cadeias de caracteres difíceis de analisar: vêm em vários formatos e incluem caracteres especiais que o analisador padrão ignora.

Tendo como tópico os números de telefone, este tutorial faz um exame minucioso dos problemas de dados padronizados e mostra como resolver esse problema usando um analisador personalizado. A abordagem que descrevemos aqui pode ser usada como está para números de telefone, ou adaptada para campos que tenham as mesmas características (padronizados, com caracteres especiais) — como URLs, emails, códigos postais e datas.

Neste tutorial, você vai usar um cliente REST e as APIs REST da Pesquisa de IA do Azure para:

  • Compreender o problema
  • Desenvolver um analisador personalizado inicial para lidar com números de telefone
  • Testar o analisador personalizado
  • Interagir com o design do analisador personalizado para aprimorar ainda mais os resultados

Pré-requisitos

Os serviços e as ferramentas a seguir são necessários para este tutorial.

Baixar arquivos

O código-fonte para este tutorial é o arquivo custom-analyzer.rest no repositório Azure-Samples/azure-search-rest-samples do GitHub.

Copiar uma chave e URL

As chamadas REST neste tutorial requerem um ponto de extremidade de serviço de pesquisa e uma chave de API de administrador. Você pode obter esses valores no portal do Azure.

  1. Entre no portal do Azure, navegue até a página Visão geral e copie a URL. Um ponto de extremidade de exemplo pode parecer com https://mydemo.search.windows.net.

  2. Em Configurações>Chaves, copie uma chave de administrador. As chaves de administrador são usadas para adicionar, modificar e excluir objetos. Há duas chaves de administrador intercambiáveis. Copie uma delas.

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

Uma chave de API válida estabelece a confiança, por solicitação, entre o aplicativo que envia a solicitação e o serviço de pesquisa que a está tratando.

Criar um índice inicial

  1. Abra um novo arquivo de texto no Visual Studio Code.

  2. Defina as variáveis para o ponto de extremidade de pesquisa e a chave de API que você coletou na etapa anterior.

    @baseUrl = PUT-YOUR-SEARCH-SERVICE-URL-HERE
    @apiKey = PUT-YOUR-ADMIN-API-KEY-HERE
    
  3. Salve o arquivo com uma extensão de arquivo .rest.

  4. Cole no exemplo a seguir para criar um índice curto chamado phone-numbers-index com dois campos: id e phone_number. Ainda não definimos um analisador e, portanto, o analisador standard.lucene será usado por padrão.

    ### 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. Selecione Enviar solicitação. Você deve ter uma resposta HTTP/1.1 201 Created e o corpo da resposta deve incluir a representação JSON do esquema de índice.

  6. Carregue dados no índice usando documentos contendo vários formatos de número de telefone. Esses serão seus dados de teste.

    ### 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. Vamos experimentar algumas consultas semelhantes às que um usuário poderia digitar. Um usuário poderia procurar por (425) 555-0100 em qualquer número de formatos e mesmo assim esperar que resultados fossem retornados. Comece pesquisando (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}}
    

    Essa consulta retorna três dos quatro resultados esperados, mas também retorna dois resultados inesperados:

    {
        "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. Vamos tentar novamente sem nenhuma formatação: 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}}
    

    Essa consulta apresenta um resultado ainda pior, retornando apenas uma de quatro correspondências corretas.

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

Se você considera esses resultados confusos, não está sozinho. Na próxima seção, vamos analisar por que estamos obtendo esses resultados.

Rever como os analisadores funcionam

Para entender esses resultados da pesquisa, precisamos entender o que o analisador está fazendo. A partir daí, poderemos testar o analisador padrão usando a API de Análise e fornecer uma base para criar um analisador que atenda melhor às nossas necessidades.

Um analisador é um componente do mecanismo de pesquisa de texto completo responsável pelo processamento de texto em cadeias de caracteres de consulta e documentos indexados. Analisadores diferentes manipulam texto de maneiras diferentes, dependendo do cenário. Para esse cenário, precisamos criar um analisador personalizado para números de telefone.

Os analisadores consistem em três componentes:

  • Filtros de caractere que removem ou substituem caracteres individuais do texto de entrada.
  • Um Criador de token que quebra o texto de entrada em tokens, que se tornam chaves no índice de pesquisa.
  • Filtros de token que manipulam os tokens produzidos pelo criador de token.

No diagrama a seguir, você pode ver como esses três componentes funcionam juntos para tokenizar uma sentença:

Diagram of Analyzer process to tokenize a sentence

Esses tokens são então armazenados em um índice invertido, que permite pesquisas rápidas de texto completo. Um índice invertido permite a pesquisa de texto completo mapeando todos os termos exclusivos extraídos durante a análise lexical para os documentos em que eles ocorrem. Você poderá ver um exemplo no diagrama a seguir:

Example inverted index

Toda a pesquisa fica inativa para pesquisar os termos armazenados no índice invertido. Quando o usuário emite uma consulta:

  1. A consulta é analisada e os termos da consulta são analisados.
  2. O índice invertido é então examinado em busca de documentos com termos correspondentes.
  3. Por fim, os documentos recuperados são classificados pelo algoritmo de pontuação.

Diagram of Analyzer process ranking similarity

Se os termos da consulta não corresponderem aos termos no seu índice invertido, não serão retornados resultados. Para saber mais sobre como funcionam as consultas, confira este artigo sobre pesquisa de texto completo.

Observação

As consultas de termo parcial são uma exceção importante a essa regra. Essas consultas (consulta de prefixo, consulta de caractere curinga, consulta regex) ignoram o processo de análise lexical, diferentemente das consultas de termo regular. Os termos parciais são apenas minúsculos antes de serem combinados com relação aos termos no índice. Se um analisador não estiver configurado para dar suporte a esses tipos de consultas, você geralmente receberá resultados inesperados porque os termos de correspondência não existem no índice.

Testar analisadores usando a API de Análise

A Pesquisa de IA do Azure fornece uma API de Análise que permite testar analisadores para entender como eles processam texto.

A API de Análise é chamada usando a seguinte solicitação:

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"
  }

A API retorna os tokens extraídos do texto usando o analisador que você especificou. O analisador Lucene padrão divide o número de telefone em três tokens separados:

{
    "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
        }
    ]
}

De modo inverso, o número de telefone 4255550100 formatado sem pontuação é indexado em um token.

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

Resposta:

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

Tenha em mente que tanto termos de consulta quanto documentos indexados são analisados. Pensando nos resultados da pesquisa da etapa anterior, podemos começar a ver por que esses resultados foram retornados.

Na primeira consulta, foram retornados números de telefone inesperados porque um de seus tokens, 555, correspondeu a um dos termos que pesquisamos. Na segunda consulta, apenas um número foi retornado por ser o único registro que tinha um token correspondente a 4255550100.

Compilar um analisador personalizado

Agora que entendemos os resultados que estamos vendo, vamos criar um analisador personalizado para aprimorar a lógica de geração de tokens.

A meta é fornecer uma pesquisa intuitiva em relação aos números de telefone, independentemente do formato em que a consulta ou a cadeia de caracteres indexada está. Para obter esse resultado, vamos especificar um filtro de caracteres, uma tokenizadora e um filtro de token.

Filtros de caractere

Os filtros de caractere são usados para processar texto antes de serem inseridos no criador. Os usos comuns dos filtros de caractere incluem a filtragem de elementos HTML ou a substituição de caracteres especiais.

Para números de telefone, queremos remover espaços em branco e caracteres especiais, pois nem todos os formatos de número de telefone contêm os mesmos caracteres e espaços especiais.

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

O filtro remove -()+. e espaços da entrada.

Entrada Saída
(321) 555-0199 3215550199
321.555.0199 3215550199

Criadores de token

Criadores de token dividem o texto em tokens e descartam alguns caracteres, como pontuação, ao longo do caminho. Em muitos casos, a meta da geração de tokens é dividir uma frase em palavras individuais.

Para este cenário, usaremos uma palavra-chave criador de token, keyword_v2, pois queremos capturar o número de telefone como um termo. Observe que essa não é a única maneira de resolver esse problema. Confira a seção Abordagens alternativas abaixo.

A palavra-chave criadores sempre produzirá o mesmo texto que foi fornecido como um termo.

Entrada Saída
The dog swims. [The dog swims.]
3215550199 [3215550199]

Filtros de token

Os filtros de token filtrarão ou modificarão os tokens gerados pelo criador. Um uso comum de um filtro de token é colocar todos os caracteres em letras minúsculas usando um filtro de token em minúsculas. Outro uso comum é a filtragem de palavras irrelevantes, como the, and ou is.

Embora não seja necessário usar nenhum desses filtros para este cenário, usaremos um filtro de token nGram para permitir pesquisas parciais de números de telefone.

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

NGramTokenFilterV2

O filtro de token de nGram_v2 divide tokens em n-grams de um determinado tamanho com base nos parâmetros minGram e maxGram.

Para o analisador de telefone, definimos minGram como 3 porque essa é a substring mais curta que esperamos que os usuários pesquisem. maxGram é definido como 20 para garantir que todos os números de telefone, mesmo com extensões, caibam em um n-grama.

O efeito colateral incerto de n-gram é que alguns falsos positivos serão retornados. Vamos corrigir isso em uma etapa posterior criando um analisador separado para pesquisas que não incluam o filtro de token n-gram.

Entrada Saída
[12345] [123, 1234, 12345, 234, 2345, 345]
[3215550199] [321, 3215, 32155, 321555, 3215550, 32155501, 321555019, 3215550199, 215, 2155, 21555, 215550, ... ]

Analisador

Com nossos filtros de caractere, criador e filtros de token em vigor, estamos prontos para definir o analisador.

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

A partir da API de Análise, considerando-se as entradas a seguir, as saídas do analisador personalizado são mostradas na tabela a seguir.

Entrada Saída
12345 [123, 1234, 12345, 234, 2345, 345]
(321) 555-0199 [321, 3215, 32155, 321555, 3215550, 32155501, 321555019, 3215550199, 215, 2155, 21555, 215550, ... ]

Todos os tokens na coluna de saída existem no índice. Se nossa consulta incluir qualquer um desses termos, o número de telefone será retornado.

Recompilar usando o novo analisador

  1. Exclua o índice atual:

     ### Delete the index
     DELETE {{baseUrl}}/indexes/phone-numbers-index?api-version=2023-11-01 HTTP/1.1
         api-key: {{apiKey}}
    
  2. Recrie o índice usando o novo analisador. Esse esquema de índice adiciona uma definição de analisador personalizado e uma atribuição de analisador personalizado ao campo do número de telefone.

    ### 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
            }
          ]
        }
    

Testar o analisador personalizado

Após recompilar o índice, agora você pode testar o analisador usando a seguinte solicitação:

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"
  }

Agora você deve ver a coleção de tokens resultantes do número de telefone:

{
    "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
        },
        ...
        ...
        ...
    ]
}

Revisar o analisador personalizado para lidar com falsos positivos

Depois de fazer algumas consultas de exemplo no índice com o analisador personalizado, você descobrirá que o cancelamento foi aprimorado e que todos os números de telefone correspondentes agora são retornados. No entanto, o filtro de token n-gram faz com que alguns falsos positivos também sejam retornados. Esse é um efeito colateral comum de um filtro de token de n-gram.

Para evitar falsos positivos, criaremos um analisador separado para consulta. Esse analisador é idêntico ao anterior, exceto pelo fato de omitir o custom_ngram_filter.

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

Na definição de índice, especificamos um indexAnalyzer e um searchAnalyzer.

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

Com essa alteração, você está pronto. Estas são as próximas etapas:

  1. Excluir o índice.

  2. Recrie o índice após adicionar o novo analisador personalizado (phone_analyzer-search) e atribuir esse analisador à propriedade searchAnalyzer do campo phone-number.

  3. Recarregue os dados.

  4. Teste as consultas novamente para verificar se a pesquisa está funcionando conforme o esperado. Se você estiver usando a amostra de arquivo, essa etapa criará o terceiro índice chamado phone-number-index-3.

Abordagens alternativas

O analisador descrito na seção anterior foi projetado para maximizar a flexibilidade da pesquisa. No entanto, ele faz isso com o custo de armazenar muitos termos potencialmente não importantes no índice.

O exemplo a seguir mostra um analisador alternativo cuja tokenização é mais eficiente, mas tem desvantagens.

Considerando-se uma entrada de 14255550100, o analisador não consegue dividir o número de telefone logicamente. Por exemplo, não consegue separar o código de país, 1, do código de área, 425. O resultado dessa discrepância seria não retornar o número acima se um usuário não incluísse um código de país em sua pesquisa.

"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": ""
  }
]

Você pode ver no exemplo a seguir que o número de telefone foi dividido nas partes que, normalmente, você esperaria que um usuário pesquisasse.

Entrada Saída
(321) 555-0199 [321, 555, 0199, 321555, 5550199, 3215550199]

Dependendo dos seus requisitos, pode ser uma abordagem do problema mais eficiente.

Observações

Este tutorial demonstrou o processo de criação e teste de um analisador personalizado. Você criou um índice, indexou dados e consultou em relação ao índice para ver quais resultados da pesquisa eram retornados. A partir daí, você usou a API de Análise para ver o processo de análise léxica em ação.

Embora o analisador definido neste tutorial ofereça uma solução fácil para pesquisar números de telefone, esse mesmo processo pode ser usado para criar um analisador personalizado para qualquer cenário que apresente características semelhantes.

Limpar os recursos

Quando você está trabalhando em sua assinatura, é uma boa ideia remover os recursos que já não são necessários no final de um projeto. Recursos deixados em execução podem custar dinheiro. Você pode excluir os recursos individualmente ou excluir o grupo de recursos para excluir todo o conjunto de recursos.

Você pode localizar e gerenciar recursos no portal usando o link Todos os recursos ou Grupos de recursos no painel de navegação à esquerda.

Próximas etapas

Agora que você está familiarizado com a criação de um analisador personalizado, vamos dar uma olhada em todos os diferentes filtros, criadores e analisadores disponíveis para criar uma experiência de pesquisa avançada.