Självstudie: Skapa en anpassad analysator för telefonnummer

I söklösningar kan strängar som har komplexa mönster eller specialtecken vara en utmaning att arbeta med eftersom standardanalysverktyget tar bort eller feltolkar meningsfulla delar av ett mönster, vilket resulterar i en dålig sökupplevelse när användarna inte kan hitta den information de förväntade sig. Telefon tal är ett klassiskt exempel på strängar som är svåra att analysera. De finns i olika format och innehåller specialtecken som standardanalysatorn ignorerar.

Med telefonnummer som ämne tar den här självstudiekursen en närmare titt på problemen med mönstrade data och visar hur du löser problemet med hjälp av en anpassad analysator. Metoden som beskrivs här kan användas som den är för telefonnummer eller anpassas för fält med samma egenskaper (mönstrade, med specialtecken), till exempel URL:er, e-postmeddelanden, postnummer och datum.

I den här självstudien använder du en REST-klient och REST API:er för Azure AI Search för att:

  • Förstå problemet
  • Utveckla en första anpassad analysator för hantering av telefonnummer
  • Testa den anpassade analysatorn
  • Iterera anpassad analysdesign för att ytterligare förbättra resultaten

Förutsättningar

Följande tjänster och verktyg krävs för den här självstudien.

Ladda ned filer

Källkoden för den här självstudien är filen custom-analyzer.rest i GitHub-lagringsplatsen Azure-Samples/azure-search-rest-samples .

Kopiera en nyckel och EN URL

REST-anropen i den här självstudien kräver en slutpunkt för söktjänsten och en api-nyckel för administratörer. Du kan hämta dessa värden från Azure-portalen.

  1. Logga in på Azure-portalen, gå till sidan Översikt och kopiera URL:en. Här följer ett exempel på hur en slutpunkt kan se ut: https://mydemo.search.windows.net.

  2. Under Inställningar> Nycklar kopierar du en administratörsnyckel. Administratörsnycklar används för att lägga till, ändra och ta bort objekt. Det finns två utbytbara administratörsnycklar. Kopiera någon av dem.

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

En giltig API-nyckel upprättar förtroende per begäran mellan programmet som skickar begäran och söktjänsten som hanterar den.

Skapa ett första index

  1. Öppna en ny textfil i Visual Studio Code.

  2. Ange variabler till sökslutpunkten och API-nyckeln som du samlade in i föregående steg.

    @baseUrl = PUT-YOUR-SEARCH-SERVICE-URL-HERE
    @apiKey = PUT-YOUR-ADMIN-API-KEY-HERE
    
  3. Spara filen med ett .rest filnamnstillägg.

  4. Klistra in i följande exempel för att skapa ett litet index med namnet phone-numbers-index med två fält: id och phone_number. Vi har inte definierat någon analysator än, så standard.lucene analysatorn används som standard.

    ### 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. Välj Skicka begäran. Du bör ha ett HTTP/1.1 201 Created svar och svarstexten bör innehålla JSON-representationen av indexschemat.

  6. Läs in data i indexet med dokument som innehåller olika telefonnummerformat. Det här är dina testdata.

    ### 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. Nu ska vi prova några frågor som liknar vad en användare kan skriva. En användare kan söka efter (425) 555-0100 i valfritt antal format och fortfarande förvänta sig att resultat returneras. Börja med att söka i (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}}
    

    Frågan returnerar tre av fyra förväntade resultat, men returnerar också två oväntade resultat:

    {
        "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. Nu ska vi försöka igen utan formatering: 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}}
    

    Den här frågan är ännu värre och returnerar bara en av fyra korrekta matchningar.

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

Om du tycker att de här resultaten är förvirrande är du inte ensam. I nästa avsnitt ska vi gå in på varför vi får dessa resultat.

Granska hur analysverktyg fungerar

För att förstå dessa sökresultat måste vi förstå vad analysatorn gör. Därifrån kan vi testa standardanalysatorn med hjälp av Analys-API:et, vilket ger en grund för att utforma en analysator som bättre uppfyller våra behov.

En analysverktyg är en komponent i sökmotorn i fulltext som ansvarar för bearbetning av text i frågesträngar och indexerade dokument. Olika analysverktyg manipulerar text på olika sätt beroende på scenariot. I det här scenariot måste vi skapa en analysator som är skräddarsydd för telefonnummer.

Analysverktyg består av tre komponenter:

  • Teckenfilter som tar bort eller ersätter enskilda tecken från indatatexten.
  • En tokeniserare som bryter indatatexten i token, som blir nycklar i sökindexet.
  • Tokenfilter som ändrar token som skapas av tokenizern.

I följande diagram kan du se hur dessa tre komponenter fungerar tillsammans för att tokenisera en mening:

Diagram of Analyzer process to tokenize a sentence

Dessa token lagras sedan i ett inverterat index, vilket möjliggör snabba fulltextsökningar. Ett inverterat index möjliggör fulltextsökning genom att mappa alla unika termer som extraheras under lexikal analys till de dokument där de inträffar. Du kan se ett exempel i nästa diagram:

Example inverted index

All sökning handlar om att söka efter de termer som lagras i det inverterade indexet. När en användare utfärdar en fråga:

  1. Frågan parsas och frågetermerna analyseras.
  2. Det inverterade indexet genomsöks sedan efter dokument med matchande termer.
  3. Slutligen rangordnas de hämtade dokumenten efter bedömningsalgoritmen.

Diagram of Analyzer process ranking similarity

Om frågetermerna inte matchar termerna i ditt inverterade index returneras inte resultaten. Mer information om hur frågor fungerar finns i den här artikeln om fulltextsökning.

Kommentar

Partiella termfrågor är ett viktigt undantag till den här regeln. Dessa frågor (prefixfråga, jokerteckenfråga, regex-fråga) kringgår den lexikala analysprocessen till skillnad från vanliga termfrågor. Partiella termer sänks bara innan de matchas mot termer i indexet. Om en analysator inte har konfigurerats för att stödja den här typen av frågor får du ofta oväntade resultat eftersom matchande termer inte finns i indexet.

Testa analysverktyg med hjälp av Analys-API:et

Azure AI Search tillhandahåller ett Analys-API som gör att du kan testa analysverktyg för att förstå hur de bearbetar text.

Analys-API:et anropas med hjälp av följande begäran:

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:et returnerar de token som extraherats från texten med hjälp av analysatorn som du angav. Lucene-standardanalysatorn delar upp telefonnumret i tre separata token:

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

Omvänt tokeniseras telefonnumret 4255550100 som formateras utan skiljetecken till en enda token.

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

Svar:

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

Tänk på att både frågetermer och indexerade dokument genomgår analys. När vi tänker tillbaka på sökresultaten från föregående steg kan vi börja se varför dessa resultat returnerades.

I den första frågan returnerades oväntade telefonnummer eftersom en av deras token, 555, matchade ett av de termer som vi sökte i. I den andra frågan returnerades bara ett tal eftersom det var den enda posten som hade en token som matchade 4255550100.

Skapa en anpassad analys

Nu när vi förstår de resultat vi ser ska vi skapa en anpassad analysator för att förbättra tokeniseringslogiken.

Målet är att ge intuitiv sökning mot telefonnummer oavsett vilket format frågan eller den indexerade strängen finns i. För att uppnå det här resultatet anger vi ett teckenfilter, en tokenizer och ett tokenfilter.

Teckenfilter

Teckenfilter används för att bearbeta text innan den matas in i tokenizern. Vanliga användningsområden för teckenfilter är att filtrera bort HTML-element eller ersätta specialtecken.

För telefonnummer vill vi ta bort blanksteg och specialtecken eftersom inte alla telefonnummerformat innehåller samma specialtecken och blanksteg.

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

Filtret tar bort -+(). och blanksteg från indata.

Indata Utdata
(321) 555-0199 3215550199
321.555.0199 3215550199

Tokenizers

Tokenizers delar upp text i token och tar bort vissa tecken, till exempel skiljetecken, längs vägen. I många fall är målet med tokenisering att dela upp en mening i enskilda ord.

I det här scenariot använder vi en nyckelordstokeniserare, keyword_v2, eftersom vi vill avbilda telefonnumret som en enda term. Observera att detta inte är det enda sättet att lösa det här problemet. Se avsnittet Alternativa metoder nedan.

Nyckelordstokeniserare matar alltid ut samma text som den angavs som en enda term.

Indata Utdata
The dog swims. [The dog swims.]
3215550199 [3215550199]

Tokenfilter

Tokenfilter filtrerar bort eller ändrar de token som genereras av tokenizern. En vanlig användning av ett tokenfilter är att ge gemener alla tecken med hjälp av ett tokenfilter med gemener. En annan vanlig användning är att filtrera bort stoppord som the, andeller is.

Även om vi inte behöver använda något av dessa filter för det här scenariot använder vi ett nGram-tokenfilter för att tillåta partiella sökningar av telefonnummer.

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

NGramTokenFilterV2

Filtret nGram_v2 token delar upp token i n-gram av en viss storlek baserat på parametrarna minGram ochmaxGram.

För telefonanalysen anger minGram vi till 3 eftersom det är den kortaste delsträngen som vi förväntar oss att användarna ska söka efter. maxGram är inställd på att se till 20 att alla telefonnummer, även med tillägg, får plats i ett enda n-gram.

Den olyckliga bieffekten av n-gram är att vissa falska positiva kommer att returneras. Vi åtgärdar detta i ett senare steg genom att skapa en separat analysator för sökningar som inte innehåller tokenfiltret n-gram.

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

Analyzer

Med våra teckenfilter, tokenizer- och tokenfilter på plats är vi redo att definiera vår analys.

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

Med hjälp av följande indata från analys-API:et visas utdata från den anpassade analysatorn i följande tabell.

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

Alla token i utdatakolumnen finns i indexet. Om vår fråga innehåller någon av dessa villkor returneras telefonnumret.

Återskapa med hjälp av den nya analysatorn

  1. Ta bort det aktuella indexet:

     ### Delete the index
     DELETE {{baseUrl}}/indexes/phone-numbers-index?api-version=2023-11-01 HTTP/1.1
         api-key: {{apiKey}}
    
  2. Återskapa indexet med hjälp av den nya analysatorn. Det här indexschemat lägger till en anpassad analysatordefinition och en anpassad analystilldelning i fältet telefonnummer.

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

Testa den anpassade analysatorn

När du har återskapat indexet kan du nu testa analysatorn med hjälp av följande begäran:

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

Nu bör du se samlingen med token som är resultatet av telefonnumret:

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

Ändra den anpassade analysatorn för att hantera falska positiva identifieringar

När du har gjort några exempelfrågor mot indexet med den anpassade analysatorn upptäcker du att återkallandet har förbättrats och att alla matchande telefonnummer nu returneras. Men filtret för n-gram-token gör att även vissa falska positiva identifieringar returneras. Detta är en vanlig bieffekt av ett n-gram tokenfilter.

För att förhindra falska positiva identifieringar skapar vi en separat analysator för frågor. Den här analysatorn är identisk med den tidigare, förutom att den utelämnarcustom_ngram_filter.

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

I indexdefinitionen anger vi sedan både en indexAnalyzer och en searchAnalyzer.

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

Med den här ändringen är du redo. Här följer nästa steg:

  1. Ta bort indexet.

  2. Återskapa indexet när du har lagt till den nya anpassade analysatorn (phone_analyzer-search) och tilldela analysatorn till phone-number fältets searchAnalyzer egenskap.

  3. Ladda om data.

  4. Testa frågorna igen för att verifiera att sökningen fungerar som förväntat. Om du använder exempelfilen skapar det här steget det tredje indexet med namnet phone-number-index-3.

Alternativa metoder

Analysatorn som beskrivs i föregående avsnitt är utformad för att maximera flexibiliteten för sökning. Men det gör det på bekostnad av att lagra många potentiellt oviktiga termer i indexet.

I följande exempel visas en alternativ analysator som är effektivare vid tokenisering, men som har nackdelar.

Med en indata av 14255550100kan analysatorn inte logiskt segmentera telefonnumret. Det kan till exempel inte skilja landskoden, 1, från riktnumret, 425. Den här avvikelsen skulle leda till att telefonnumret inte returneras om en användare inte tog med någon landskod i sökningen.

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

I följande exempel ser du att telefonnumret är uppdelat i de segment som du normalt förväntar dig att en användare söker efter.

Indata Utdata
(321) 555-0199 [321, 555, 0199, 321555, 5550199, 3215550199]

Beroende på dina krav kan detta vara en effektivare metod för problemet.

Lärdomar

I den här självstudien demonstrerades processen för att skapa och testa en anpassad analysator. Du skapade ett index, indexerade data och frågade sedan mot indexet för att se vilka sökresultat som returnerades. Därifrån använde du Analysera API för att se den lexikala analysprocessen i praktiken.

Även om analysatorn som definieras i den här självstudien erbjuder en enkel lösning för sökning mot telefonnummer, kan samma process användas för att skapa en anpassad analysator för alla scenarion som har liknande egenskaper.

Rensa resurser

När du arbetar i din egen prenumeration är det en bra idé att ta bort de resurser som du inte längre behöver i slutet av ett projekt. Resurser som fortsätter att köras kostar pengar. Du kan ta bort enstaka resurser eller hela resursgruppen om du vill ta bort alla resurser.

Du kan hitta och hantera resurser i portalen med hjälp av länken Alla resurser eller Resursgrupper i det vänstra navigeringsfönstret.

Nästa steg

Nu när du är bekant med hur du skapar en anpassad analysator ska vi ta en titt på alla olika filter, tokenizers och analysverktyg som är tillgängliga för dig för att skapa en omfattande sökupplevelse.