Partager via


Tutoriel : Créer un analyseur personnalisé pour les numéros de téléphone

Dans les solutions de recherche, les chaînes qui ont des modèles complexes ou des caractères spéciaux peuvent être difficile à utiliser, car l’analyseur par défaut supprime ou comprend mal des parties significatives d’un modèle, ce qui entraîne une mauvaise expérience de recherche quand les utilisateurs ne trouvent pas les informations attendues. Les numéros de téléphone sont un exemple classique de chaînes difficiles à analyser. Ils sont fournis dans différents formats, et incluent des caractères spéciaux que l’analyseur par défaut ignore.

Ce tutoriel, qui porte sur les numéros de téléphone, examine de près les problèmes des données qui suivent un modèle et vous montre comment résoudre ce problème avec un analyseur personnalisé. L’approche décrite ici peut être utilisée telle quelle pour les numéros de téléphone ou adaptée pour les champs qui ont les mêmes caractéristiques (qui suivent un modèle, avec des caractères spéciaux), par exemple, les URL, les e-mails, les codes postaux et les dates.

Dans ce tutoriel, vous utilisez un client REST et les API REST Recherche Azure AI pour :

  • Comprendre le problème
  • Développer un analyseur personnalisé initial pour gérer les numéros de téléphone
  • Tester l’analyseur personnalisé
  • Itérer sur la conception de l’analyseur personnalisé pour améliorer encore les résultats

Prérequis

Les services et outils suivants sont indispensables dans ce tutoriel.

Télécharger les fichiers

Le code source utilisé dans ce tutoriel est le fichier custom-analyzer.rest du dépôt GitHub Azure-Samples/azure-search-rest-samples.

Copier une clé et une URL

Les appels REST de ce tutoriel nécessitent un point de terminaison de service de recherche et une clé API d’administration. Vous pouvez obtenir ces valeurs à partir du portail Azure.

  1. Connectez-vous au portail Azure, accédez à la page Vue d’ensemble et copiez l’URL. Voici un exemple de point de terminaison : https://mydemo.search.windows.net.

  2. Sous Paramètres>Clés, copiez une clé d’administration. Les clés d’administration sont utilisées pour ajouter, modifier et supprimer des objets. Il existe deux clés d’administration interchangeables. Copiez l’une ou l’autre.

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

Une clé d’API valide permet d’établir, en fonction de chaque requête, une relation de confiance entre l’application qui envoie la requête et le service de recherche qui en assure le traitement.

Créer un index initial

  1. Ouvrez un nouveau fichier texte dans Visual Studio Code.

  2. Définissez des variables sur le point de terminaison de recherche et la clé API que vous avez collectée à l’étape précédente.

    @baseUrl = PUT-YOUR-SEARCH-SERVICE-URL-HERE
    @apiKey = PUT-YOUR-ADMIN-API-KEY-HERE
    
  3. Enregistrez le fichier avec une extension de fichier .rest.

  4. Collez l’exemple suivant pour créer un petit index appelé phone-numbers-index avec deux champs : id et phone_number. Comme nous n’avons pas encore défini d’analyseur, l’analyseur standard.lucene est utilisé par défaut.

    ### 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. Sélectionnez Envoyer une demande. Vous devez avoir une réponse HTTP/1.1 201 Created et le corps de la réponse doit inclure la représentation JSON du schéma d’index.

  6. Chargez des données dans l’index en utilisant des documents qui contiennent différents formats de numéros de téléphone. Il s’agit de vos données de test.

    ### 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. Essayons des requêtes similaires à ce que pourrait taper un utilisateur. Un utilisateur doit pouvoir rechercher (425) 555-0100 dans n’importe quel format et quand même obtenir des résultats. Commençons par rechercher (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}}
    

    Cette requête renvoie trois des quatre résultats attendus, mais également deux résultats inattendus :

    {
        "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. Réessayons sans mise en forme : 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}}
    

    Cette requête est encore pire puisqu’elle retourne seulement un des quatre numéros correspondants.

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

Si vous trouvez que ces résultats prêtent à confusion, vous n’êtes pas le seul. Dans la section suivante, nous examinons en détail pourquoi nous obtenons ces résultats.

Examiner le fonctionnement des analyseurs

Pour comprendre ces résultats de recherche, nous devons comprendre ce que fait l’analyseur. À partir de là, nous pouvons tester l’analyseur par défaut avec l’API Analyser, qui est la base pour concevoir un analyseur correspondant mieux à nos besoins.

Un analyseur est un composant du moteur de recherche en texte intégral chargé de traiter le texte dans les chaînes de requête et les documents indexés. La façon dont les différents analyseurs manipulent le texte varie en fonction du scénario. Pour ce scénario, nous devons créer un analyseur adapté aux numéros de téléphone.

Un analyseur comprend trois composants :

  • Des filtres de caractères qui suppriment ou remplacent des caractères individuels du texte d’entrée.
  • Un générateur de jetons qui divise le texte d’entrée en jetons, lesquels deviennent des clés dans l’index de recherche.
  • Des filtres de jetons qui manipulent les jetons produits par le générateur de jetons.

Dans le diagramme suivant, vous voyez comment ces trois composants fonctionnent ensemble pour tokeniser une phrase :

Diagram of Analyzer process to tokenize a sentence

Ces jetons sont ensuite stockés dans un index inversé qui autorise des recherches rapides en texte intégral. Pour cela, un index inversé mappe tous les termes uniques extraits durant l’analyse lexicale aux documents dans lesquels ils apparaissent. Vous pouvez voir un exemple dans le diagramme suivant :

Example inverted index

Toute la recherche se résume à rechercher les termes stockés dans l’index inversé. Quand un utilisateur émet une requête :

  1. La requête est analysée et les termes de la requête sont analysés.
  2. L’index inversé est ensuite analysé à la recherche de documents contenant des termes correspondants.
  3. Enfin, les documents récupérés sont classés par l’algorithme de scoring.

Diagram of Analyzer process ranking similarity

Si les termes de la requête ne correspondent pas aux termes de votre index inversé, aucun résultat n’est renvoyé. Pour en savoir plus sur le fonctionnement des requêtes, consultez cet article sur la recherche en texte intégral.

Notes

Les requêtes sur des termes partiels constituent une exception importante à cette règle. Contrairement aux requêtes sur des termes réguliers, ces requêtes (requêtes avec des préfixes, des caractères génériques ou des expressions régulières) contournent le processus d’analyse lexicale. Les termes partiels sont uniquement mis en minuscules avant d’être mis en correspondance avec les termes de l’index. Si un analyseur n’est pas configuré pour prendre en charge ces types de requêtes, vous obtenez souvent des résultats inattendus dans la mesure où les termes correspondants n’existent pas dans l’index.

Tester les analyseurs avec l’API Analyser

La Recherche Azure AI fournit une API Analyser qui vous permet de tester les analyseurs pour comprendre comment ils traitent le texte.

L’API Analyser est appelée en utilisant la demande suivante :

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

L’API renvoie les jetons extraits du texte avec l’analyseur que vous avez spécifié. L’analyseur Lucene standard divise le numéro de téléphone en trois jetons distincts :

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

À l’inverse, le numéro de téléphone 4255550100 mis en forme sans ponctuation produit un seul jeton.

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

Réponse :

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

Gardez à l’esprit que les termes de la requête et les documents indexés sont analysés. Si vous repensez aux résultats de la recherche obtenus à l’étape précédente, vous pouvez commencer à comprendre pourquoi ces résultats ont été retournés.

Dans la première requête, des numéros de téléphone inattendus ont été renvoyés, car un de leurs jetons, 555, correspondait à un des termes recherchés. Dans la deuxième requête, un seul numéro a été renvoyé parce que c’était le seul enregistrement qui avait un jeton correspondant à 4255550100.

Créer un analyseur personnalisé

Maintenant que nous comprenons les résultats que nous avons obtenus, nous allons créer un analyseur personnalisé pour améliorer la logique de génération de jetons.

L’objectif est de pouvoir rechercher de manière intuitive des numéros de téléphone, quel que soit le format de la requête ou de la chaîne indexée. Pour obtenir ce résultat, nous spécifions un filtre de caractère, un générateur de jetons et un filtre de jeton.

Filtres de caractères

Les filtres de caractères permettent de traiter le texte avant de l’envoyer au générateur de jetons. Les filtres de caractères sont couramment utilisés pour rejeter des éléments HTML ou remplacer des caractères spéciaux.

Pour les numéros de téléphone, nous voulons supprimer les espaces blancs et les caractères spéciaux, car tous les formats de numéro de téléphone ne contiennent pas les mêmes caractères spéciaux et espaces.

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

Le filtre supprime -()+. et les espaces de l’entrée.

Entrée Output
(321) 555-0199 3215550199
321.555.0199 3215550199

Générateurs de jetons

Les générateurs de jetons divisent le texte en jetons et rejettent certains caractères, comme les signes de ponctuation, au cours du processus. Dans de nombreux cas, l’objectif de la génération de jetons est de diviser une phrase en mots individuels.

Pour ce scénario, nous allons utiliser le générateur de jetons keyword_v2 pour capturer le numéro de téléphone comme un terme unique. Notez qu’il existe d’autres moyens de résoudre ce problème. Pour les voir, consultez la section Autres approches ci-dessous.

Les générateurs de jetons (mots clés) génèrent toujours le même texte que celui qu’ils ont reçu sous la forme d’un terme unique.

Entrée Output
The dog swims. [The dog swims.]
3215550199 [3215550199]

Filtres de jeton

Les filtres de jetons rejettent ou modifient les jetons générés par le générateur de jetons. Un filtre de jetons est couramment utilisé pour mettre en minuscules pour tous les caractères à l’aide d’un filtre de jeton. Une autre utilisation courante consiste à rejeter les mots vides comme the, and ou is.

Bien que nous n’ayons pas besoin d’utiliser l’un de ces filtres dans ce scénario, nous allons utiliser un filtre de jetons n-gramme pour pouvoir effectuer des recherches partielles de numéros de téléphone.

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

NGramTokenFilterV2

Le filtre de jetons nGram_v2 divise les jetons en n-grammes d’une taille donnée en fonction des paramètres minGram et maxGram.

Pour l’analyseur de numéros de téléphone, nous définissons minGram avec la valeur 3, car nous ne nous attendons pas à ce que les utilisateurs recherchent des sous-chaînes plus courtes. Nous définissons maxGram avec la valeur 20 pour nous assurer que tous les numéros de téléphone, même ceux avec des extensions, tiennent dans un seul n-gramme.

Malheureusement, la génération de faux positifs est un effet secondaire des n-grammes. Nous corrigerons ce problème dans une étape ultérieure en créant un analyseur distinct pour les recherches qui n’incluent pas le filtre de jeton n-gramme.

Entrée Output
[12345] [123, 1234, 12345, 234, 2345, 345]
[3215550199] [321, 3215, 32155, 321555, 3215550, 32155501, 321555019, 3215550199, 215, 2155, 21555, 215550, ... ]

Analyseur

Une fois les filtres de caractères, le générateur de jetons et les filtres de jetons en place, nous sommes prêts à définir notre analyseur.

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

À partir de l’API Analyser, étant donné les entrées suivantes, les sorties de l’analyseur personnalisé sont indiquées dans le tableau suivant.

Entrée Sortie
12345 [123, 1234, 12345, 234, 2345, 345]
(321) 555-0199 [321, 3215, 32155, 321555, 3215550, 32155501, 321555019, 3215550199, 215, 2155, 21555, 215550, ... ]

Tous les jetons de la colonne de sortie existent dans l’index. Si notre requête contient un de ces termes, le numéro de téléphone est renvoyé.

Recréer avec le nouvel analyseur

  1. Supprimez l’index actuel :

     ### Delete the index
     DELETE {{baseUrl}}/indexes/phone-numbers-index?api-version=2023-11-01 HTTP/1.1
         api-key: {{apiKey}}
    
  2. Recréez l’index avec le nouvel analyseur. Ce schéma d’index ajoute une définition d’analyseur personnalisé et une attribution d’analyseur personnalisé sur le champ de numéro de téléphone.

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

Tester l’analyseur personnalisé

Une fois l’index recréé, vous pouvez tester l’analyseur en utilisant la demande suivante :

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

Vous devez maintenant voir la collection de jetons résultant du numéro de téléphone :

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

Réviser l’analyseur personnalisé pour gérer les faux positifs

Après avoir exécuté quelques exemples de requêtes sur l’index à l’aide de l’analyseur personnalisé, vous constatez que le rappel a été amélioré et que tous les numéros de téléphone correspondants sont désormais retournés. Toutefois, le filtre de jetons n-gramme retourne également quelques faux positifs. Il s’agit d’un effet secondaire courant avec les fitres de jetons n-gramme.

Pour éviter les faux positifs, nous allons créer un analyseur distinct pour l’interrogation. Cet analyseur est identique au précédent, sauf qu’il omet le custom_ngram_filter.

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

Dans la définition de l’index, nous spécifions un indexAnalyzer et un searchAnalyzer.

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

Une fois ce changement effectué, tout est prêt. Voici les étapes suivantes :

  1. Supprimer l’index.

  2. Recréez l’index après avoir ajouté le nouvel analyseur personnalisé (phone_analyzer-search) et attribué cet analyseur à la propriété searchAnalyzer du champ phone-number.

  3. Rechargez le modèle.

  4. Retestez les requêtes pour vérifier que la recherche fonctionne comme prévu. Si vous utilisez l’exemple de fichier, cette étape crée le troisième index nommé phone-number-index-3.

Autres approches

L’analyseur décrit dans la section précédente est conçu pour optimiser la flexibilité de la recherche. Toutefois, il en résulte le stockage de nombreux termes potentiellement sans importance dans l’index.

L’exemple suivant montre un autre analyseur plus efficace dans la tokenisation, mais présente des inconvénients.

Prenons l’entrée 14255550100, l’analyseur ne peut pas segmenter logiquement le numéro de téléphone. Par exemple, il ne peut pas séparer le code de pays, 1, de l’indicatif régional, 425. Cette incohérence empêche le renvoi du numéro de téléphone si l’utilisateur n’inclut pas le code de pays dans sa recherche.

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

Vous pouvez voir dans l’exemple suivant que le numéro de téléphone est divisé en blocs recherchés normalement par les utilisateurs.

Entrée Sortie
(321) 555-0199 [321, 555, 0199, 321555, 5550199, 3215550199]

Selon vos besoins, cette approche peut être plus efficace pour résoudre le problème.

Éléments importants à retenir

Dans ce tutoriel, vous avez vu le processus de création et de test d’un analyseur personnalisé. Vous avez créé un index, indexé les données, puis interrogé l’index pour voir les résultats de la recherche retournés. Ensuite, vous avez utilisé l’API Analyser pour voir le processus d’analyse lexicale en action.

Bien que l’analyseur défini dans ce tutoriel offre une solution simple de recherche de numéros de téléphone, vous pouvez suivre ce même processus afin de créer un analyseur personnalisé pour n’importe quel scénario partageant les mêmes caractéristiques.

Nettoyer les ressources

Quand vous travaillez dans votre propre abonnement, il est judicieux à la fin d’un projet de supprimer les ressources dont vous n’avez plus besoin. Les ressources laissées en cours d’exécution peuvent vous coûter de l’argent. Vous pouvez supprimer les ressources une par une, ou choisir de supprimer le groupe de ressources afin de supprimer l’ensemble des ressources.

Vous pouvez rechercher et gérer les ressources dans le portail à l’aide des liens Toutes les ressources ou Groupes de ressources situés dans le volet de navigation de gauche.

Étapes suivantes

Maintenant que vous savez comment créer un analyseur personnalisé, examinons les différents filtres, générateurs de jetons et analyseurs disponibles pour créer une expérience de recherche enrichie.