Quickstart: Zoeken in volledige tekst met behulp van de Azure SDK's

Meer informatie over het gebruik van de Azure.Search.Documents-clientbibliotheek in een Azure SDK om een zoekindex te maken, laden en er query's op uit te voeren met behulp van voorbeeldgegevens voor zoeken in volledige tekst. Zoeken in volledige tekst maakt gebruik van Apache Lucene voor indexering en query's en een BM25-classificatie-algoritme voor scoreresultaten.

Deze quickstart bevat stappen voor de volgende SDK's:

Vereisten

  • Een Azure-account met een actief abonnement. Gratis een account maken

  • Een Azure AI-Search-service. Maak een service als u er nog geen hebt. U kunt een gratis laag gebruiken voor deze quickstart.

  • Een API-sleutel en service-eindpunt. Meld u aan bij Azure Portal en zoek uw zoekservice.

    Kopieer in Overzicht de URL en sla deze op in Kladblok voor een latere stap. Een eindpunt ziet er bijvoorbeeld uit als https://mydemo.search.windows.net.

    Kopieer en sla in Sleutels een beheerderssleutel op voor volledige rechten voor het maken en verwijderen van objecten. Er zijn twee uitwisselbare primaire en secundaire sleutels. Kies een van beide.

    Get an HTTP endpoint and access key

Een index maken, laden en er query's op uitvoeren

Kies een programmeertaal voor de volgende stap. De clientbibliotheken Azure.Search.Documents zijn beschikbaar in Azure SDK's voor .NET, Python, Java en JavaScript.

Bouw een consoletoepassing met behulp van de clientbibliotheek Azure.Search.Documents om een zoekindex te maken, laden en er query's op uit te voeren. U kunt ook de broncode downloaden om te beginnen met een voltooid project of deze stappen volgen om uw eigen project te maken.

Uw omgeving instellen

  1. Start Visual Studio en maak een nieuw project voor een console-app.

  2. Selecteer onder Hulpprogramma's>NuGet-pakketbeheer de optie NuGet-pakketten voor oplossing beheren....

  3. Selecteer Bladeren.

  4. Zoek naar het pakket Azure.Search.Documents en selecteer versie 11.0 of hoger.

  5. Selecteer Installeren aan de rechterkant om de assembly toe te voegen aan uw project en oplossing.

Een zoekclient maken

  1. Wijzig in Program.cs de naamruimte in AzureSearch.SDK.Quickstart.v11 en voeg vervolgens de volgende using-instructies toe.

    using Azure;
    using Azure.Search.Documents;
    using Azure.Search.Documents.Indexes;
    using Azure.Search.Documents.Indexes.Models;
    using Azure.Search.Documents.Models;
    
  2. Maak twee clients: SearchIndexClient maakt de index en SearchClient laadt en voert query's uit op een bestaande index. Beide hebben het service-eindpunt en een beheerder-API-sleutel nodig voor verificatie van rechten voor maken/verwijderen.

    Omdat de code de URI voor u uitbouwt, geeft u alleen de naam van de zoekservice op in de eigenschap ServiceName.

     static void Main(string[] args)
     {
         string serviceName = "<your-search-service-name>";
         string apiKey = "<your-search-service-admin-api-key>";
         string indexName = "hotels-quickstart";
    
         // Create a SearchIndexClient to send create/delete index commands
         Uri serviceEndpoint = new Uri($"https://{serviceName}.search.windows.net/");
         AzureKeyCredential credential = new AzureKeyCredential(apiKey);
         SearchIndexClient adminClient = new SearchIndexClient(serviceEndpoint, credential);
    
         // Create a SearchClient to load and query documents
         SearchClient srchclient = new SearchClient(serviceEndpoint, indexName, credential);
         . . . 
     }
    

Een index maken

In deze quickstart wordt een index van hotels gemaakt die u met hotelgegevens laadt en waarop u query's uitvoert. In deze stap definieert u de velden in de index. Elke definitie bevat een naam, gegevenstype en kenmerken die bepalen hoe het veld wordt gebruikt.

In dit voorbeeld worden synchrone methoden van de Azure.Search.Documents-bibliotheek gebruikt voor eenvoud en leesbaarheid. Voor productiescenario's moet u echter asynchrone methoden gebruiken om uw app op een schaalbare en responsieve manier te laten werken. U kunt bijvoorbeeld CreateIndexAsync gebruiken in plaats van CreateIndex.

  1. Voeg een lege klassedefinitie toe aan uw project: Hotel.cs

  2. Kopieer de volgende code in Hotel.cs om de structuur van een hoteldocument te definiëren. Kenmerken in het veld bepalen hoe deze worden gebruikt in een toepassing. Het IsFilterable-kenmerk moet bijvoorbeeld worden toegewezen aan elk veld dat een filterexpressie ondersteunt.

    using System;
    using System.Text.Json.Serialization;
    using Azure.Search.Documents.Indexes;
    using Azure.Search.Documents.Indexes.Models;
    
    namespace AzureSearch.Quickstart
    {
        public partial class Hotel
        {
            [SimpleField(IsKey = true, IsFilterable = true)]
            public string HotelId { get; set; }
    
            [SearchableField(IsSortable = true)]
            public string HotelName { get; set; }
    
            [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnLucene)]
            public string Description { get; set; }
    
            [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.FrLucene)]
            [JsonPropertyName("Description_fr")]
            public string DescriptionFr { get; set; }
    
            [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
            public string Category { get; set; }
    
            [SearchableField(IsFilterable = true, IsFacetable = true)]
            public string[] Tags { get; set; }
    
            [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
            public bool? ParkingIncluded { get; set; }
    
            [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
            public DateTimeOffset? LastRenovationDate { get; set; }
    
            [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
            public double? Rating { get; set; }
    
            [SearchableField]
            public Address Address { get; set; }
        }
    }
    

    In de clientbibliotheek van Azure.Search.Documents kunt u velddefinities stroomlijnen met behulp van de velden SearchableField en SimpleField. Beide zijn afgeleiden van een SearchField en kunnen uw code vereenvoudigen:

    • SimpleField kan elk gegevenstype zijn, is altijd niet-doorzoekbaar (wordt genegeerd voor zoekopdrachten in volledige tekst) en kan worden opgehaald (is niet verborgen). Andere kenmerken zijn standaard uitgeschakeld, maar kunnen wel worden ingeschakeld. U kunt een SimpleField gebruiken voor document-id's of velden die alleen worden gebruikt in filters, facetten of scoreprofielen. Als dat het geval is, moet u ervoor zorgen dat u alle kenmerken toepast die nodig zijn voor het scenario, zoals IsKey = true voor een document-id. Zie SimpleFieldAttribute.cs in broncode voor meer informatie.

    • SearchableField moet een tekenreeks zijn die altijd kan worden doorzocht en opgehaald. Andere kenmerken zijn standaard uitgeschakeld, maar kunnen wel worden ingeschakeld. Omdat dit veldtype kan worden doorzocht, worden synoniemen en het volledige gamma van analyse-eigenschappen ondersteund. Zie SearchableFieldAttribute.cs in broncode voor meer informatie.

    Ongeacht of u de basis-API van SearchField of een van de hulpmodellen gebruikt, u moet filter-, facet- en sorteerkenmerken expliciet inschakelen. Bijvoorbeeld IsFilterable, IsSortableen IsFacetable moet expliciet worden voorzien van een kenmerk, zoals weergegeven in het bovenstaande voorbeeld.

  3. Voeg een tweede lege klassedefinitie toe aan uw project: Address.cs. Kopieer de volgende code naar de klasse.

    using Azure.Search.Documents.Indexes;
    
     namespace AzureSearch.Quickstart
     {
         public partial class Address
         {
             [SearchableField(IsFilterable = true)]
             public string StreetAddress { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string City { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string StateProvince { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string PostalCode { get; set; }
    
             [SearchableField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
             public string Country { get; set; }
         }
     }
    
  4. Maak nog twee klassen: Hotel.Methods.cs en Address.Methods.cs voor ToString()-onderdrukkingen. Deze klassen worden gebruikt voor het weergeven van zoekresultaten in de console-uitvoer. De inhoud van deze klassen wordt niet opgegeven in dit artikel, maar u kunt de code kopiëren uit bestanden in GitHub.

  5. Maak in Program.cs een SearchIndex-object en roep vervolgens de methode CreateIndex aan om de index uit te drukken in uw zoekservice. De index bevat ook een SearchSuggester om automatisch aanvullen in te schakelen voor de opgegeven velden.

     // Create hotels-quickstart index
     private static void CreateIndex(string indexName, SearchIndexClient adminClient)
     {
         FieldBuilder fieldBuilder = new FieldBuilder();
         var searchFields = fieldBuilder.Build(typeof(Hotel));
    
         var definition = new SearchIndex(indexName, searchFields);
    
         var suggester = new SearchSuggester("sg", new[] { "HotelName", "Category", "Address/City", "Address/StateProvince" });
         definition.Suggesters.Add(suggester);
    
         adminClient.CreateOrUpdateIndex(definition);
     }
    

Documenten laden

Azure AI Search zoekt naar inhoud die is opgeslagen in de service. In deze stap laadt u JSON-documenten die overeenkomen met de hotelindex die u zojuist hebt gemaakt.

In Azure AI Search zijn zoekdocumenten gegevensstructuren die zowel invoer zijn voor het indexeren als uitvoeren van query's. Als u de documenten hebt verkregen via een externe gegevensbron, bestaat de documentinvoer mogelijk uit rijen in een database, blobs in Blob Storage of JSON-documenten op een schijf. In dit voorbeeld nemen we de korte route en gaan we JSON-documenten voor vier hotels in de code zelf insluiten.

Bij het uploaden van documenten moet u een IndexDocumentsBatch-object gebruiken. Een IndexDocumentsBatch object bevat een verzameling acties, die elk een document en een eigenschap bevatten die Azure AI Search vertelt welke actie moet worden uitgevoerd (uploaden, samenvoegen, verwijderen en samenvoegen).

  1. In Program.cs maakt u een matrix met documenten en indexacties en vervolgens geeft u de matrix door aan IndexDocumentsBatch. De documenten hieronder voldoen aan de index hotel-quickstart, zoals gedefinieerd door de hotelklasse.

    // Upload documents in a single Upload request.
    private static void UploadDocuments(SearchClient searchClient)
    {
        IndexDocumentsBatch<Hotel> batch = IndexDocumentsBatch.Create(
            IndexDocumentsAction.Upload(
                new Hotel()
                {
                    HotelId = "1",
                    HotelName = "Secret Point Motel",
                    Description = "The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time's Square and the historic centre of the city, as well as other places of interest that make New York one of America's most attractive and cosmopolitan cities.",
                    DescriptionFr = "L'hôtel est idéalement situé sur la principale artère commerciale de la ville en plein cœur de New York. A quelques minutes se trouve la place du temps et le centre historique de la ville, ainsi que d'autres lieux d'intérêt qui font de New York l'une des villes les plus attractives et cosmopolites de l'Amérique.",
                    Category = "Boutique",
                    Tags = new[] { "pool", "air conditioning", "concierge" },
                    ParkingIncluded = false,
                    LastRenovationDate = new DateTimeOffset(1970, 1, 18, 0, 0, 0, TimeSpan.Zero),
                    Rating = 3.6,
                    Address = new Address()
                    {
                        StreetAddress = "677 5th Ave",
                        City = "New York",
                        StateProvince = "NY",
                        PostalCode = "10022",
                        Country = "USA"
                    }
                }),
            IndexDocumentsAction.Upload(
                new Hotel()
                {
                    HotelId = "2",
                    HotelName = "Twin Dome Motel",
                    Description = "The hotel is situated in a  nineteenth century plaza, which has been expanded and renovated to the highest architectural standards to create a modern, functional and first-class hotel in which art and unique historical elements coexist with the most modern comforts.",
                    DescriptionFr = "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
                    Category = "Boutique",
                    Tags = new[] { "pool", "free wifi", "concierge" },
                    ParkingIncluded = false,
                    LastRenovationDate = new DateTimeOffset(1979, 2, 18, 0, 0, 0, TimeSpan.Zero),
                    Rating = 3.60,
                    Address = new Address()
                    {
                        StreetAddress = "140 University Town Center Dr",
                        City = "Sarasota",
                        StateProvince = "FL",
                        PostalCode = "34243",
                        Country = "USA"
                    }
                }),
            IndexDocumentsAction.Upload(
                new Hotel()
                {
                    HotelId = "3",
                    HotelName = "Triple Landscape Hotel",
                    Description = "The Hotel stands out for its gastronomic excellence under the management of William Dough, who advises on and oversees all of the Hotel’s restaurant services.",
                    DescriptionFr = "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
                    Category = "Resort and Spa",
                    Tags = new[] { "air conditioning", "bar", "continental breakfast" },
                    ParkingIncluded = true,
                    LastRenovationDate = new DateTimeOffset(2015, 9, 20, 0, 0, 0, TimeSpan.Zero),
                    Rating = 4.80,
                    Address = new Address()
                    {
                        StreetAddress = "3393 Peachtree Rd",
                        City = "Atlanta",
                        StateProvince = "GA",
                        PostalCode = "30326",
                        Country = "USA"
                    }
                }),
            IndexDocumentsAction.Upload(
                new Hotel()
                {
                    HotelId = "4",
                    HotelName = "Sublime Cliff Hotel",
                    Description = "Sublime Cliff Hotel is located in the heart of the historic center of Sublime in an extremely vibrant and lively area within short walking distance to the sites and landmarks of the city and is surrounded by the extraordinary beauty of churches, buildings, shops and monuments. Sublime Cliff is part of a lovingly restored 1800 palace.",
                    DescriptionFr = "Le sublime Cliff Hotel est situé au coeur du centre historique de sublime dans un quartier extrêmement animé et vivant, à courte distance de marche des sites et monuments de la ville et est entouré par l'extraordinaire beauté des églises, des bâtiments, des commerces et Monuments. Sublime Cliff fait partie d'un Palace 1800 restauré avec amour.",
                    Category = "Boutique",
                    Tags = new[] { "concierge", "view", "24-hour front desk service" },
                    ParkingIncluded = true,
                    LastRenovationDate = new DateTimeOffset(1960, 2, 06, 0, 0, 0, TimeSpan.Zero),
                    Rating = 4.60,
                    Address = new Address()
                    {
                        StreetAddress = "7400 San Pedro Ave",
                        City = "San Antonio",
                        StateProvince = "TX",
                        PostalCode = "78216",
                        Country = "USA"
                    }
                })
            );
    
        try
        {
            IndexDocumentsResult result = searchClient.IndexDocuments(batch);
        }
        catch (Exception)
        {
            // If for some reason any documents are dropped during indexing, you can compensate by delaying and
            // retrying. This simple demo just logs the failed document keys and continues.
            Console.WriteLine("Failed to index some of the documents: {0}");
        }
    }
    

    Nadat u het IndexDocumentsBatch-object hebt geïnitialiseerd, kunt u het naar de index sturen door IndexDocuments aan te roepen op uw SearchClient-object.

  2. Voeg de volgende regels toe aan Main(). Documenten worden geladen met SearchClient, maar voor de bewerking zijn ook beheerdersrechten op de service vereist, die meestal is gekoppeld aan SearchIndexClient. Een manier om deze bewerking in te stellen is het ophalen van SearchClient via SearchIndexClient (adminClient in dit voorbeeld).

     SearchClient ingesterClient = adminClient.GetSearchClient(indexName);
    
     // Load documents
     Console.WriteLine("{0}", "Uploading documents...\n");
     UploadDocuments(ingesterClient);
    
  3. Omdat dit een console-app is waarmee alle opdrachten opeenvolgend worden uitgevoerd, moet u een wachttijd van 2 seconden tussen indexeren en query's toevoegen.

    // Wait 2 seconds for indexing to complete before starting queries (for demo and console-app purposes only)
    Console.WriteLine("Waiting for indexing...\n");
    System.Threading.Thread.Sleep(2000);
    

    De vertraging van 2 seconden compenseert de indexering (die asynchroon is), zodat alle documenten kunnen worden geïndexeerd voordat de query's worden uitgevoerd. Codering in een vertraging is doorgaans alleen nodig in demo's, testen en voorbeeldtoepassingen.

Een index doorzoeken

U kunt queryresultaten ophalen zodra het eerste document wordt geïndexeerd, maar wacht met het daadwerkelijk testen van uw index totdat alle documenten zijn geïndexeerd.

In deze sectie worden twee functies toegevoegd: querylogica en resultaten. Gebruik voor query's de methode Search. Deze methode gebruikt zoektekst (de querytekenreeks) en andere opties.

De SearchResults-klasse vertegenwoordigt de resultaten.

  1. Maak in Program.cs een WriteDocuments-methode waarmee zoekresultaten op de console worden afgedrukt.

    // Write search results to console
    private static void WriteDocuments(SearchResults<Hotel> searchResults)
    {
        foreach (SearchResult<Hotel> result in searchResults.GetResults())
        {
            Console.WriteLine(result.Document);
        }
    
        Console.WriteLine();
    }
    
    private static void WriteDocuments(AutocompleteResults autoResults)
    {
        foreach (AutocompleteItem result in autoResults.Results)
        {
            Console.WriteLine(result.Text);
        }
    
        Console.WriteLine();
    }
    
  2. Maak een RunQueries-methode om query's uit te voeren en resultaten te retourneren. Resultaten zijn Hotel-objecten. In dit voorbeeld ziet u de handtekening en de eerste query van de methode. Deze query demonstreert de Select-parameter waarmee u het resultaat kunt samenstellen met behulp van geselecteerde velden uit het document.

    // Run queries, use WriteDocuments to print output
    private static void RunQueries(SearchClient srchclient)
    {
        SearchOptions options;
        SearchResults<Hotel> response;
    
        // Query 1
        Console.WriteLine("Query #1: Search on empty term '*' to return all documents, showing a subset of fields...\n");
    
        options = new SearchOptions()
        {
            IncludeTotalCount = true,
            Filter = "",
            OrderBy = { "" }
        };
    
        options.Select.Add("HotelId");
        options.Select.Add("HotelName");
        options.Select.Add("Address/City");
    
        response = srchclient.Search<Hotel>("*", options);
        WriteDocuments(response);
    
  3. In de tweede query kunt u op een term zoeken, een filter toevoegen waarmee documenten worden geselecteerd met een hogere beoordeling dan 4 en de documenten vervolgens op beoordeling sorteren in aflopende volgorde. Een filter is een Booleaanse uitdrukking die wordt geëvalueerd over IsFilterable-velden in een index. Filterquery's bevatten waarden of sluiten ze uit. Er is dus geen relevantiescore gekoppeld aan een filterquery.

    // Query 2
    Console.WriteLine("Query #2: Search on 'hotels', filter on 'Rating gt 4', sort by Rating in descending order...\n");
    
    options = new SearchOptions()
    {
        Filter = "Rating gt 4",
        OrderBy = { "Rating desc" }
    };
    
    options.Select.Add("HotelId");
    options.Select.Add("HotelName");
    options.Select.Add("Rating");
    
    response = srchclient.Search<Hotel>("hotels", options);
    WriteDocuments(response);
    
  4. De derde query demonstreert searchFields, die wordt gebruikt om een zoekbewerking in volledige tekst naar specifieke velden te bepalen.

    // Query 3
    Console.WriteLine("Query #3: Limit search to specific fields (pool in Tags field)...\n");
    
    options = new SearchOptions()
    {
        SearchFields = { "Tags" }
    };
    
    options.Select.Add("HotelId");
    options.Select.Add("HotelName");
    options.Select.Add("Tags");
    
    response = srchclient.Search<Hotel>("pool", options);
    WriteDocuments(response);
    
  5. De vierde query demonstreert facetten, die kunnen worden gebruikt voor het structureren van een facetnavigatiestructuur.

     // Query 4
     Console.WriteLine("Query #4: Facet on 'Category'...\n");
    
     options = new SearchOptions()
     {
         Filter = ""
     };
    
     options.Facets.Add("Category");
    
     options.Select.Add("HotelId");
     options.Select.Add("HotelName");
     options.Select.Add("Category");
    
     response = srchclient.Search<Hotel>("*", options);
     WriteDocuments(response);
    
  6. In de vijfde query retourneert u een specifiek document. Een zoekbewerking voor een document is een typisch antwoord op een OnClick-gebeurtenis in een resultatenset.

     // Query 5
     Console.WriteLine("Query #5: Look up a specific document...\n");
    
     Response<Hotel> lookupResponse;
     lookupResponse = srchclient.GetDocument<Hotel>("3");
    
     Console.WriteLine(lookupResponse.Value.HotelId);
    
  7. De laatste query toont de syntaxis voor automatisch aanvullen, waarbij een gedeeltelijke gebruikersinvoer van 'sa' wordt gesimuleerd die wordt omgezet in twee mogelijke overeenkomsten in de sourceFields die zijn gekoppeld aan de suggester die u in de index hebt gedefinieerd.

     // Query 6
     Console.WriteLine("Query #6: Call Autocomplete on HotelName that starts with 'sa'...\n");
    
     var autoresponse = srchclient.Autocomplete("sa", "sg");
     WriteDocuments(autoresponse);
    
  8. Voeg RunQueries toe aan Main().

    // Call the RunQueries method to invoke a series of queries
    Console.WriteLine("Starting queries...\n");
    RunQueries(srchclient);
    
    // End the program
    Console.WriteLine("{0}", "Complete. Press any key to end this program...\n");
    Console.ReadKey();
    

De vorige query's tonen meerdere manieren om termen in een query te koppelen: zoekopdracht in volledige tekst, filters en automatisch aanvullen.

Zoekopdrachten in volledige tekst en filters worden uitgevoerd met behulp van de methode SearchClient.Search. Een zoekquery kan worden doorgegeven in de searchText-tekenreeks, terwijl een filterexpressie kan worden doorgegeven in de eigenschap Filter van de klasse Search Options. Als u wilt filteren zonder te zoeken, geeft u "*" door voor de searchText-parameter van de Zoekmethode. Als u wilt zoeken zonder te filteren, laat u de Filter eigenschap uitgeschakeld of geeft u een SearchOptions instantie helemaal niet door.

Het programma uitvoeren

Druk op F5 om de app opnieuw te bouwen en het programma in zijn geheel uit te voeren.

De uitvoer bevat berichten van Console.WriteLine, met toevoeging van query-informatie en -resultaten.

Resources opschonen

Wanneer u in uw eigen abonnement werkt, is het een goed idee om aan het einde van een project te bepalen of u de gemaakte resources nog nodig hebt. Resources die actief blijven, kunnen u geld kosten. U kunt resources afzonderlijk verwijderen, maar u kunt ook de resourcegroep verwijderen als u de volledige resourceset wilt verwijderen.

U kunt resources vinden en beheren in de portal via de koppeling Alle resources of Resourcegroepen in het navigatiedeelvenster aan de linkerkant.

Als u een gratis service gebruikt, moet u er rekening mee houden dat u beperkt bent tot drie indexen, indexeerfuncties en gegevensbronnen. U kunt afzonderlijke items in de portal verwijderen om onder de limiet te blijven.

Volgende stappen

In deze quickstart hebt u een set taken doorlopen om een index te maken, deze te laden met documenten en query's uit te voeren. In verschillende fasen hebben we korte routes genomen om de code te vereenvoudigen voor leesbaarheid en begrijpelijkheid. Nu u bekend bent met de basisconcepten, kunt u een zelfstudie uitproberen waarmee de Azure AI Search-API's in een web-app worden aangeroepen.