Azure AI Keresés lekérdezésintegráció felfedezése C#-alkalmazásokban

Note

Azure AI Keresés a Azure portálon, REST API-kon és Azure SDK-k keresztül érhető el. Emellett a Foundry IQ-t is támogatja, amely a vállalati tartalmakat újrafelhasználható, engedélytudatos tudásbázisokká alakítja át az Microsoft Foundry portálon található ügynökök számára.

Az előző lépésben üzembe helyezte a keresésre alkalmas webhelyet a Azure Container Apps. Ez a cikk a keresési integráció alapvető lépéseit ismerteti. Gondoljon rá úgy, mint egy csalási lapra, amely integrálja a keresést a webalkalmazásba.

Azure SDK Azure. Search.Documents

Az API a Azure AI Keresés Azure SDK használja:

Az API az SDK-val hitelesíti a felhőalapú Azure AI Keresés API-t a keresési szolgáltatás nevével és az index nevével. A Azure Container Apps a tárolókörnyezet biztosítja a konfigurációs értékeket. A felügyelt identitás az alapértelmezett hitelesítőadat-elérési út.

Felügyelt identitás hitelesítése

Az API minden Azure függvénye SearchClient egy megosztott SearchClientFactory osztályon keresztül hozza létre, így minden függvény ugyanúgy hitelesít. Alapértelmezés szerint a gyár létrehoz egyDefaultAzureCredential, és arra használja, hogy jogkivonatokat kérjen Azure AI Keresés. A Azure Container Apps DefaultAzureCredential feloldja a tárolóalkalmazáshoz rendelt felügyelt identitást.

Az alábbi metódus hozza SearchClientFactory.cs létre ezt a hitelesítő adatot. Ha a tárolóalkalmazás felhasználó által hozzárendelt felügyelt identitással rendelkezik, a rendszer átadja DefaultAzureCredentialOptions a AZURE_CLIENT_ID környezeti változó ügyfél-azonosítóját, hogy a jogkivonatok beszerzése ne legyen egyértelmű.

private static DefaultAzureCredential CreateManagedIdentityCredential()
{
    var options = new DefaultAzureCredentialOptions();

    if (!string.IsNullOrWhiteSpace(ManagedIdentityClientId))
    {
        options.ManagedIdentityClientId = ManagedIdentityClientId;
    }

    return new DefaultAzureCredential(options);
}

A Bicep infrastruktúra hozzárendeli a felügyelt identitáshoz való hozzáférést a Azure AI Keresés adatsíkhoz a Azure AI Keresés soránazd up. Ezzel a szerepkör-hozzárendeléssel az API lekérdezési kulcs tárolása nélkül kérdezheti le az good-books indexet a tárolókörnyezetben.

Helyi és üzembe helyezett hitelesítő adatok feloldása

AZURE_CLIENT_ID Ha a rendszer nem rendelkezik, helyileg visszaesik a szokásos hitelesítőadat-láncba, DefaultAzureCredential és feloldja a bejelentkezett fejlesztői hitelesítő adatokat, például a bejelentkezéshez használt Azure CLI vagy Visual Studio Code fiókot. A Azure Container Apps üzembe helyezésekor a Bicep infrastruktúra a felhasználó által hozzárendelt felügyelt identitás ügyfél-azonosítójára van beosztvaAZURE_CLIENT_ID, így DefaultAzureCredential az adott identitást célozza meg ahelyett, hogy a gazdagép által közzétehető, kétértelműen oldható fel.

Az API-kulcsok használatához állítsa be USE_KEYLESS_AUTH az üzembe helyezés előtt:false

azd env set USE_KEYLESS_AUTH false
azd up

Csak akkor használjon kulcshitelesítést, ha a környezet megköveteli.

Helyi fejlesztési beállítások

A helyi fejlesztéshez a mintafájl sample.local.settings.json az API által várt értékeket jeleníti meg. Csak a helyi beállításokat használja a fejlesztéshez. A Azure Container Apps az üzembe helyezési konfiguráció egyenértékű tárolókörnyezeti értékeket biztosít.

Setting Alkalmazás célja Kötelező, ha
SearchServiceName A Azure AI Keresés szolgáltatás neve. Kombinálva .search.windows.net a szolgáltatásvégpont URI-jának létrehozásához. Mindig
SearchIndexName A lekérdezéshez használt keresési index neve. Az alapértelmezett érték, good-books ha nincs bekapcsolva. Optional
SEARCH_USE_KEY_AUTH Az alapértelmezett érték hamis, felügyelt identitást használ. Úgy van beállítva, hogy true a felügyelt identitás helyett API-kulcsot használjon. Választható kulcshitelesítés
SearchApiKey Rendszergazdai kulcs Azure AI Keresés. Kötelező, ha a(z) SEARCH_USE_KEY_AUTH értéke true.
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "SearchServiceName": "",
    "SearchIndexName": "good-books"
  },
  "Host": {
    "CORS": "*"
  }
}

Függvény: Keresés a katalógusban

A Keresési API egy keresési kifejezést vesz fel, és a keresési indexben lévő dokumentumok között keres, és visszaadja a találatok listáját. A Javaslat API-val a rendszer felhasználótípusként részleges sztringeket küld a keresőmotornak. Az API keresési kifejezéseket, például könyvcímeket és szerzőket javasol a keresési index dokumentumai alapján, és egy kis találatlistát ad vissza.

A Azure függvény lekéri a keresési konfiguráció adatait a tárolókörnyezetből, létrehozza a Azure AI Keresés ügyfelet, és teljesíti a lekérdezést.

A keresési javaslatot sga tömeges feltöltés során használt sémafájl határozza meg.

using Azure;
using Azure.Core.Serialization;
using Azure.Identity;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using WebSearch.Models;
using SearchFilter = WebSearch.Models.SearchFilter;

namespace WebSearch.Function
{
    public class Search
    {
        private readonly ILogger<Lookup> _logger;

        public Search(ILogger<Lookup> logger)
        {
            _logger = logger;
        }

        [Function("search")]
        public async Task<HttpResponseData> RunAsync(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req, 
            FunctionContext executionContext)
        {
            string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
            var data = JsonSerializer.Deserialize<RequestBodySearch>(requestBody);

            // Azure AI Search (managed identity by default; API key only when SEARCH_USE_KEY_AUTH=true)
            SearchClient searchClient = SearchClientFactory.CreateSearchClient();

            SearchOptions options = new()

            {
                Size = data.Size,
                Skip = data.Skip,
                IncludeTotalCount = true,
                Filter = CreateFilterExpression(data.Filters)
            };
            options.Facets.Add("authors");
            options.Facets.Add("language_code");

            SearchResults<SearchDocument> searchResults = searchClient.Search<SearchDocument>(data.SearchText, options);

            var facetOutput = new Dictionary<string, IList<FacetValue>>();
            foreach (var facetResult in searchResults.Facets)
            {
                facetOutput[facetResult.Key] = facetResult.Value
                           .Select(x => new FacetValue { value = x.Value.ToString(), count = x.Count })

                           .ToList();
            }

            // Data to return 
            var output = new SearchOutput
            {
                Count = searchResults.TotalCount,
                Results = searchResults.GetResults().ToList(),
                Facets = facetOutput
            };
            
            var response = req.CreateResponse(HttpStatusCode.Found);

            // Serialize data
            var serializer = new JsonObjectSerializer(
                new JsonSerializerOptions(JsonSerializerDefaults.Web));
            await response.WriteAsJsonAsync(output, serializer);

            return response;
        }

        public static string CreateFilterExpression(List<SearchFilter> filters)
        {
            if (filters is null or { Count: <= 0 })
            {
                return null;
            }

            List<string> filterExpressions = new();


            List<SearchFilter> authorFilters = filters.Where(f => f.field == "authors").ToList();
            List<SearchFilter> languageFilters = filters.Where(f => f.field == "language_code").ToList();

            List<string> authorFilterValues = authorFilters.Select(f => f.value).ToList();

            if (authorFilterValues.Count > 0)
            {
                string filterStr = string.Join(",", authorFilterValues);
                filterExpressions.Add($"{"authors"}/any(t: search.in(t, '{filterStr}', ','))");
            }

            List<string> languageFilterValues = languageFilters.Select(f => f.value).ToList();
            foreach (var value in languageFilterValues)
            {
                filterExpressions.Add($"language_code eq '{value}'");
            }

            return string.Join(" and ", filterExpressions);
        }
    }
}

A függvény egymástól függetlenül történő ellenőrzéséhez hívjon meg /api/search egy keresési kifejezést a kérelem törzsében, és győződjön meg arról, hogy a válasz tartalmazza a megfelelő könyvdokumentumokat, a teljes számokat és az aspektusértékeket.

Ügyfél: Keresés a katalógusban

A React-ügyfél Keresési lapja meghívja a search Azure függvényt, amikor a felhasználó beír egy lekérdezést, módosít egy aspektusszűrőt, vagy új találati lapra vált. Az ügyfél elküldi a keresési szöveget, az aktuális lap skip és top értékek, valamint a POST törzsben lévő összes kijelölt szerző vagy nyelvszűrőt /api/search. A függvény visszaadja az egyező könyvdokumentumok listáját, a teljes darabszámot és a aspektusértékeket, amelyeket az oldal az eredménylista, a lapozó és a aspektusszűrők megjelenítésére használ. A következő kód a buildekben \client\src\pages\Search\Search.jsx , amelyek a választ összetevő állapotban kérik és tárolják:

import React, { useEffect, useState, Suspense } from 'react';
import fetchInstance from '../../url-fetch';
import CircularProgress from '@mui/material/CircularProgress';
import { useLocation, useNavigate } from "react-router-dom";

import Results from '../../components/Results/Results';
import Pager from '../../components/Pager/Pager';
import Facets from '../../components/Facets/Facets';
import SearchBar from '../../components/SearchBar/SearchBar';

import "./Search.css";

export default function Search() {

  let location = useLocation();
  const navigate = useNavigate();

  const [results, setResults] = useState([]);
  const [resultCount, setResultCount] = useState(0);
  const [currentPage, setCurrentPage] = useState(1);
  const [q, setQ] = useState(new URLSearchParams(location.search).get('q') ?? "*");
  const [top] = useState(new URLSearchParams(location.search).get('top') ?? 8);
  const [skip, setSkip] = useState(new URLSearchParams(location.search).get('skip') ?? 0);
  const [filters, setFilters] = useState([]);
  const [facets, setFacets] = useState({});
  const [isLoading, setIsLoading] = useState(true);

  let resultsPerPage = top;

  // Handle page changes in a controlled manner
  function handlePageChange(newPage) {
    setCurrentPage(newPage);
  }

  // Calculate skip value and fetch results when relevant parameters change
  useEffect(() => {
    // Calculate skip based on current page
    const calculatedSkip = (currentPage - 1) * top;
    
    // Only update if skip has actually changed
    if (calculatedSkip !== skip) {
      setSkip(calculatedSkip);
      return; // Skip the fetch since skip will change and trigger another useEffect
    }
    
    // Proceed with fetch
    setIsLoading(true);
    
    const body = {
      q: q,
      top: top,
      skip: skip,
      filters: filters
    };

    
    fetchInstance('/api/search', { body, method: 'POST' })
      .then(response => {
        setResults(response.results);
        setFacets(response.facets);
        setResultCount(response.count);
        setIsLoading(false);
      })
      .catch(error => {
        console.log(error);
        setIsLoading(false);
      });
  }, [q, top, skip, filters, currentPage]);

  // pushing the new search term to history when q is updated
  // allows the back button to work as expected when coming back from the details page
  useEffect(() => {
    navigate('/search?q=' + q);
    setCurrentPage(1);
    setFilters([]);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [q]);


  let postSearchHandler = (searchTerm) => {
    setQ(searchTerm);
  }


  // filters should be applied across entire result set, 
  // not just within the current page
  const updateFilterHandler = (newFilters) => {

    // Reset paging
    setSkip(0);
    setCurrentPage(1);

    // Set filters
    setFilters(newFilters);
  };

  return (
    <main className="main main--search container-fluid">
      <div className="row">
        <div className="search-bar-column col-md-3">
          <div className="search-bar-column-container">
            <SearchBar postSearchHandler={postSearchHandler} query={q} width={false}></SearchBar>
          </div>
          <Facets facets={facets} filters={filters} setFilters={updateFilterHandler}></Facets>
        </div>
        <div className="search-bar-results">
          {isLoading ? (
            <div className="col-md-9">
              <CircularProgress />
            </div>
          ) : (
            <div className="search-results-container">
              <Results documents={results} top={top} skip={skip} count={resultCount} query={q}></Results>
              <Pager className="pager-style" currentPage={currentPage} resultCount={resultCount} resultsPerPage={resultsPerPage} onPageChange={handlePageChange}></Pager>
            </div>
          )}
        </div>
      </div>
    </main>
  );
}

Az integráció ellenőrzéséhez adjon meg egy keresési kifejezést a webhely keresősávjában, és győződjön meg arról, hogy a találatok listája, az eredmények száma és az összes frissítés aspektusa.

Ügyfél: Javaslatok a katalógusból

A Javaslat függvény API-jának meghívása a React alkalmazásban \client\src\components\SearchBar\SearchBar.jsx az Anyag felhasználói felület automatikus kiegészítési összetevőjének részeként történik. Ez az összetevő a bemeneti szöveget használja azoknak a szerzőknek és könyveknek a kereséséhez, amelyek megfelelnek. Ezután ezeket a lehetséges találatokat kiválasztható elemként jeleníti meg a legördülő listában.

import React, { useState, useEffect } from 'react';
import { TextField, Autocomplete, Button, Box } from '@mui/material';
import fetchInstance from '../../url-fetch';
import './SearchBar.css';

export default function SearchBar({ postSearchHandler, query, width }) {
  const [q, setQ] = useState(() => query || '');
  const [suggestions, setSuggestions] = useState([]);

  const search = (value) => {
    postSearchHandler(value);
  };

  useEffect(() => {
    if (q) {

      const body = { q, top: 5, suggester: 'sg' };

      fetchInstance('/api/suggest', { body, method: 'POST' })
      .then(response => {
        setSuggestions(response.suggestions.map(s => s.text));
      })
      .catch(error => {
        console.log(error);
        setSuggestions([]);
      });
    }
  }, [q]);


  const onInputChangeHandler = (event, value) => {
    setQ(value);
  };


  const onChangeHandler = (event, value) => {

    setQ(value);
    search(value);
  };

  const onEnterButton = (event) => {
    // if enter key is pressed
    if (event.key === 'Enter') {
      search(q);
    }
  };

  return (
    <div
      className={width ? "search-bar search-bar-wide" : "search-bar search-bar-narrow"}
    >
      <Box className="search-bar-box">
        <Autocomplete
          className="autocomplete"
          freeSolo
          value={q}
          options={suggestions}
          onInputChange={onInputChangeHandler}
          onChange={onChangeHandler}
          disableClearable
          renderInput={(params) => (
            <TextField
              {...params}
              id="search-box"
              className="form-control rounded-0"
              placeholder="What are you looking for?"
              onBlur={() => setSuggestions([])}
              onClick={() => setSuggestions([])}
              onKeyDown={onEnterButton}
            />
          )}
        />
        <div className="search-button" >
          <Button variant="contained" color="primary" onClick={() => {
            search(q)
          }
          }>
            Search
          </Button>
        </div>
      </Box>
    </div>
  );
}

Az integráció ellenőrzéséhez írja be a szöveget a webhely keresősávjára, és győződjön meg arról, hogy a megfelelő könyvcímek és szerzők megjelennek az automatikus kiegészítés legördülő listában.

Függvény: Adott dokumentum lekérése

A Dokumentumkeresés API lekéri egy könyv teljes dokumentumát, miután egy felhasználó kiválasztotta azt a keresési eredmények közül. A függvény beolvassa a könyvet id a kérés lekérdezési sztringéből, hitelesítettet SearchClienthoz létre, SearchClientFactory és meghívjaGetDocumentAsync, hogy keresse meg a kulcsot az good-books indexben. Az eredményül kapott dokumentumot egy objektumba LookupOutput csomagolva adja vissza.

using Azure;
using Azure.Core.Serialization;
using Azure.Identity;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Text.Json;
using WebSearch.Models;

namespace WebSearch.Function
{
    public class Lookup
    {
        private readonly ILogger<Lookup> _logger;

        public Lookup(ILogger<Lookup> logger)
        {
            _logger = logger;
        }


        [Function("lookup")]
        public async Task<HttpResponseData> RunAsync(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, 
            FunctionContext executionContext)
        {

            // Get Document Id
            var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
            string documentId = query["id"].ToString();

            // Azure AI Search (managed identity by default; API key only when SEARCH_USE_KEY_AUTH=true)
            SearchClient searchClient = SearchClientFactory.CreateSearchClient();

            var getDocumentResponse = await searchClient.GetDocumentAsync<SearchDocument>(documentId);

            // Data to return 
            var output = new LookupOutput
            {
                Document = getDocumentResponse.Value
            };

            var response = req.CreateResponse(HttpStatusCode.Found);

            // Serialize data
            var serializer = new JsonObjectSerializer(
                new JsonSerializerOptions(JsonSerializerDefaults.Web));
            await response.WriteAsJsonAsync(output, serializer);

            return response;
        }
    }
}

A Keresés függvény független ellenőrzéséhez hívjon /api/lookup meg egy érvényes könyvet id , és erősítse meg, hogy a válasz visszaadja a könyv teljes dokumentumát.

Ügyfél: Adott dokumentum lekérése

Amikor egy felhasználó kiválaszt egy könyvet a keresési eredmények közül, a Részletek lapnak szüksége van a könyv teljes dokumentumára, beleértve az összefoglaló listában nem szereplő mezőket is. A Részletek oldal beolvassa a könyvet id az útvonalparaméterekből, és meghívja a Document Lookup API-t /api/lookup , amikor az összetevő csatlakozik. A visszaadott dokumentumot összetevőállapotban tárolja, és az Eredmény és a Nyers adatok lapon jeleníti meg. Az összetevő inicializálása során a \client\src\pages\Details\Details.jsx következő kód végzi el a keresést:

import React, { useState, useEffect } from "react";
import { useParams } from 'react-router-dom';
import Rating from '@mui/material/Rating';
import CircularProgress from '@mui/material/CircularProgress';
import Tabs from '@mui/material/Tabs';
import Tab from '@mui/material/Tab';
import Box from '@mui/material/Box';

import fetchInstance from '../../url-fetch';

import "./Details.css";


function CustomTabPanel(props) {
  const { children, value, index, ...other } = props;

  return (
    <div
      className="tab-panel"
      role="tabpanel"
      hidden={value !== index}
      id={`simple-tabpanel-${index}`}
      aria-labelledby={`simple-tab-${index}`}
      {...other}
       // Ensure it takes full width
    >
      {value === index && <Box className="tab-panel-value">{children}</Box>}
    </div>
  );
}

export default function BasicTabs() {
  const { id } = useParams();
  const [document, setDocument] = useState({});
  const [value, setValue] = React.useState(0);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    setIsLoading(true);
    fetchInstance('/api/lookup', { query: { id } })
      .then(response => {
        console.log(JSON.stringify(response))
        const doc = response.document;
        setDocument(doc);
        setIsLoading(false);
      })
      .catch(error => {
        console.log(error);
        setIsLoading(false);
      });

  }, [id]);

  const handleChange = (event, newValue) => {
    setValue(newValue);
  };


  if (isLoading || !id || Object.keys(document).length === 0) {
    return (
      <div className="loading-container">
        <CircularProgress />
        <p>Loading...</p>
      </div>
    );
  }

  return (
    <Box className="details-box-parent">
      <Box className="details-tab-box-header">
        <Tabs value={value} onChange={handleChange} aria-label="book-details-tabs">
          <Tab label="Result" />
          <Tab label="Raw Data" />
        </Tabs>
      </Box>
      <CustomTabPanel value={value} index={0} className="tab-panel box-content">
        <div className="card-body">
          <h5 className="card-title">{document.original_title}</h5>
          <img className="image" src={document.image_url} alt="Book cover"></img>
          <p className="card-text">{document.authors?.join('; ')} - {document.original_publication_year}</p>
          <p className="card-text">ISBN {document.isbn}</p>
          <Rating name="half-rating-read" value={parseInt(document.average_rating)} precision={0.1} readOnly></Rating>
          <p className="card-text">{document.ratings_count} Ratings</p>
        </div>
      </CustomTabPanel>
      <CustomTabPanel value={value} index={1} className="tab-panel">
        <div className="card-body text-left card-text details-custom-tab-panel-json-div" >
          <pre><code>
            {JSON.stringify(document, null, 2)}
          </code></pre>
        </div>
      </CustomTabPanel>
    </Box>
  );
}

Az integráció ellenőrzéséhez válasszon ki egy könyvet a keresési eredmények közül, és győződjön meg arról, hogy a részletek, köztük a fedőkép, a szerzők és a minősítés is megjelennek a Részletek lapon.

Az API-t támogató C#-modellek

A Azure Functions API és a tömeges importálási projekt C#-modellosztályok készletével rendelkezik. Ezek az osztályok határozzák meg az ügyfél által küldött kéréstörzseket, például a keresési szöveget, a lapozási értékeket és a szűrőket. Emellett meghatározzák az ügyfél által várt válaszalakzatokat is, például a keresési eredményeket, a aspektusértékeket és egy áttekintett dokumentumot. Ha ezeket a modelleket egy fájlban tartja, a keresési, javaslati és dokumentumkeresési végpontok összhangban maradnak a React-ügyfél elvárásaival. A következő, ebben az Models.csalkalmazásban definiált modellek támogatják az alkalmazás funkcióit:

using Azure.Search.Documents.Models;
using System.Text.Json.Serialization;

namespace WebSearch.Models
{
    public class RequestBodyLookUp
    {
        [JsonPropertyName("id")]
        public string Id { get; set; }
    }

    public class RequestBodySuggest
    {
        [JsonPropertyName("q")]
        public string SearchText { get; set; }

        [JsonPropertyName("top")]
        public int Size { get; set; }

        [JsonPropertyName("suggester")]
        public string SuggesterName { get; set; }
    }

    public class RequestBodySearch
    {
        [JsonPropertyName("q")]
        public string SearchText { get; set; }

        [JsonPropertyName("skip")]
        public int Skip { get; set; }

        [JsonPropertyName("top")]
        public int Size { get; set; }

        [JsonPropertyName("filters")]
        public List<SearchFilter> Filters { get; set; }
    }

    public class SearchFilter
    {
        public string field { get; set; }
        public string value { get; set; }
    }

    public class FacetValue
    {
        public string value { get; set; }
        public long? count { get; set; }
    }

    class SearchOutput
    {
        [JsonPropertyName("count")]
        public long? Count { get; set; }
        [JsonPropertyName("results")]
        public List<SearchResult<SearchDocument>> Results { get; set; }
        [JsonPropertyName("facets")]
        public Dictionary<String, IList<FacetValue>> Facets { get; set; }
    }
    class LookupOutput
    {
        [JsonPropertyName("document")]
        public SearchDocument Document { get; set; }
    }
    public class BookModel
    {
        public string id { get; set; }
        public decimal? goodreads_book_id { get; set; }
        public decimal? best_book_id { get; set; }
        public decimal? work_id { get; set; }
        public decimal? books_count { get; set; }
        public string isbn { get; set; }
        public string isbn13 { get; set; }
        public string[] authors { get; set; }
        public decimal? original_publication_year { get; set; }
        public string original_title { get; set; }
        public string title { get; set; }
        public string language_code { get; set; }
        public double? average_rating { get; set; }
        public decimal? ratings_count { get; set; }
        public decimal? work_ratings_count { get; set; }
        public decimal? work_text_reviews_count { get; set; }
        public decimal? ratings_1 { get; set; }
        public decimal? ratings_2 { get; set; }
        public decimal? ratings_3 { get; set; }
        public decimal? ratings_4 { get; set; }
        public decimal? ratings_5 { get; set; }
        public string image_url { get; set; }
        public string small_image_url { get; set; }
    }
}

Következő lépés

A Azure AI Keresés fejlesztésének folytatásához próbálja ki az indexelésről szóló következő oktatóanyagot: