C# uygulamasında Azure Yapay Zeka Arama sorgu tümleştirmesini keşfetme

Not

Azure Yapay Zeka Arama Azure portalı, REST API'leri ve Azure SDK’ları aracılığıyla kullanılabilir. Ayrıca kuruluş içeriğini Microsoft Foundry portalındaki aracılar için yeniden kullanılabilir, izin kullanan bilgi bankalarına dönüştüren yönetilen bilgi katmanı Foundry IQ'yu temel alır.

Önceki adımda, arama özellikli web sitesini Azure Container Apps için dağıtmıştınız. Bu makalede, arama tümleştirmesini oluşturan temel adımlar vurgulanır. Aramayı web uygulamanızla tümleştirmek için bir bilgi sayfası olarak düşünün.

Azure SDK Azure. Search.Documents

API, Azure Yapay Zeka Arama için Azure SDK kullanır:

API, arama hizmeti adını ve dizin adını kullanarak SDK aracılığıyla bulut tabanlı Azure Yapay Zeka Arama API'sinde kimlik doğrulaması yapar. Azure Container Apps kapsayıcı ortamı yapılandırma değerlerini sağlar. Yönetilen kimlik, varsayılan kimlik bilgisi yoludur.

Yönetilen kimlik doğrulaması

API'deki her Azure işlevi paylaşılan SearchClientFactory bir sınıf aracılığıyla kendi SearchClient işlevini oluşturur, böylece her işlev aynı şekilde kimlik doğrulaması yapar. Varsayılan olarak, fabrika bir DefaultAzureCredential oluşturur ve Azure Yapay Zeka Arama için belirteç istemek için bunu kullanır. Azure Container Apps'da kapsayıcı DefaultAzureCredential uygulamasına atanan yönetilen kimliğe çözümler.

aşağıdaki yöntemi SearchClientFactory.cs bu kimlik bilgilerini oluşturur. Kapsayıcı uygulamasının kullanıcı tarafından atanan yönetilen kimliği olduğunda, ortam değişkeninden AZURE_CLIENT_ID istemci kimliği'ne DefaultAzureCredentialOptions geçirilir, böylece belirteç alımı belirsiz olmaz.

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

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

    return new DefaultAzureCredential(options);
}

Bicep altyapısı, yönetilen kimlik erişimini sırasında azd upAzure Yapay Zeka Arama veri düzlemine atar. Bu rol ataması, API'nin good-books kapsayıcı ortamında bir sorgu anahtarı depolamadan dizini sorgulamasına olanak tanır.

Yerel ve dağıtılan kimlik bilgisi çözümlemesi karşılaştırması

Yerel olarak, ayarlanmamışsa AZURE_CLIENT_ID standart DefaultAzureCredential kimlik bilgileri zincirinden geçer ve oturum açmak için kullandığınız Azure CLI veya Visual Studio Code hesabı gibi oturum açmış geliştirici kimlik bilgilerinize çözümür. Azure Container Apps dağıtıldığında, Bicep altyapısı kullanıcı tarafından atanan yönetilen kimliğin istemci kimliğine ayarlıdırAZURE_CLIENT_ID, bu nedenle DefaultAzureCredential bir konağın açığa çıkarabileceği birden çok kimlik arasında belirsiz bir şekilde çözümlemek yerine bu kimliği hedefler.

Bunun yerine API anahtarlarını kullanmak için dağıtımdan önce olarak false ayarlayınUSE_KEYLESS_AUTH:

azd env set USE_KEYLESS_AUTH false
azd up

Anahtar kimlik doğrulamayı yalnızca ortamınız gerektirdiğinde kullanın.

Yerel geliştirme ayarları

Yerel geliştirme için örnek sample.local.settings.json dosya API'nin beklediği değerleri gösterir. Yalnızca geliştirme için yerel ayarları kullanın. Azure Container Apps dağıtım yapılandırması eşdeğer kapsayıcı ortamı değerlerini sağlar.

Setting Purpose Gerekli olduğunda
SearchServiceName Azure Yapay Zeka Arama hizmetinin adı. ile birleştirerek .search.windows.net hizmet uç noktası URI'sini oluşturur. Her zaman
SearchIndexName Sorgu için arama dizininin adı. Varsayılan olarak good-books ayarlanmamışsa olur. Optional
SEARCH_USE_KEY_AUTH Varsayılan değer false'tur, yönetilen kimliği kullanır. Yönetilen kimlik yerine API anahtarı kullanacak şekilde true ayarlayın. İsteğe bağlı anahtar kimlik doğrulaması
SearchApiKey Azure Yapay Zeka Arama için yönetici anahtarı. SEARCH_USE_KEY_AUTH, true olduğunda gereklidir
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "SearchServiceName": "",
    "SearchIndexName": "good-books"
  },
  "Host": {
    "CORS": "*"
  }
}

İşlev: Katalogda arama

Arama API'si bir arama terimi alır ve arama dizinindeki belgeler arasında arama yapıp eşleşme listesini döndürür. Öneri API'si aracılığıyla, kullanıcı türü olarak kısmi dizeler arama altyapısına gönderilir. API, arama dizinindeki belgelere dayanarak kitap başlıkları ve yazarlar gibi arama terimlerini önerir ve küçük bir eşleşme listesi döndürür.

Azure işlevi kapsayıcı ortamından arama yapılandırma bilgilerini çeker, Azure Yapay Zeka Arama istemcisini oluşturur ve sorguyu gerçekleştirir.

Arama önericisi, sgtoplu karşıya yükleme sırasında kullanılan şema dosyasında tanımlanır.

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);
        }
    }
}

İşlevi bağımsız olarak doğrulamak için, istek gövdesinde bir arama terimiyle çağırın /api/search ve yanıtın eşleşen kitap belgeleri, toplam sayısı ve model değerleri içerdiğini onaylayın.

İstemci: Katalogda arama yapma

Kullanıcı bir sorgu girdiğinde, model filtresini search değiştirildiğinde veya yeni bir sonuç sayfasına geçtiğinde React istemcisinin Arama sayfası Azure işlevini çağırır. İstemci arama metnini, geçerli sayfanın skip ve değerlerini ve top POST gövdesindeki seçili yazar veya dil filtrelerini adresine /api/searchgönderir. İşlev, sayfanın sonuç listesi, çağrı ve model filtrelerini işlemek için kullandığı eşleşen kitap belgelerinin, toplam sayısının ve model değerlerinin listesini döndürür. Aşağıdaki kod, \client\src\pages\Search\Search.jsx isteğinde bulunan ve yanıtı bileşen durumunda depolayan derlemelerde yer alır:

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>
  );
}

Bu tümleştirmeyi doğrulamak için web sitesinin arama çubuğuna bir arama terimi girin ve sonuç listesinin, sonuç sayısının ve modellerinin tüm güncelleştirmeleri onayladığını onaylayın.

İstemci: Katalogdan öneriler

Öneri işlevi API'si, Malzeme Kullanıcı Arabirimi Otomatik Tamamlama bileşeninin bir parçası olarak React \client\src\components\SearchBar\SearchBar.jsx uygulamasında çağrılır. Bu bileşen, eşleşen yazarları ve kitapları aramak için giriş metnini kullanır. Ardından bu olası eşleşmeleri açılan listede seçilebilir öğeler olarak görüntüler.

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>
  );
}

Bu tümleştirmeyi doğrulamak için web sitesinin arama çubuğuna metin girin ve eşleşen kitap başlıklarının ve yazarlarının otomatik tamamlama açılan listesinde göründüğünü onaylayın.

İşlev: Belirli bir belgeyi alma

Belge Arama API'si, bir kullanıcı arama sonuçlarından seçtikten sonra tek bir kitap için belgenin tamamını alır. İşlev, isteğin sorgu dizesinden bir kitap id okur, kimliği doğrulanmış SearchClientbir oluşturmak için kullanır SearchClientFactory ve dizinde good-books bu anahtarı aramak için öğesini çağırırGetDocumentAsync. Sonuçta elde edilen belgeyi bir nesneye sarmalanmış olarak LookupOutput döndürür.

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

Lookup işlevini bağımsız olarak doğrulamak için geçerli bir kitapla id çağrısı /api/lookup yapıp yanıtın kitabın tam belgesini döndürdüğünü onaylayın.

İstemci: Belirli bir belgeyi al

Kullanıcı arama sonuçlarından bir kitap seçtiğinde, Ayrıntılar sayfasında özet listesinde gösterilmeyen alanlar da dahil olmak üzere bu kitap için belgenin tamamı gerekir. Ayrıntılar sayfası, rota parametrelerinden kitabı id okur ve bileşen bağlandığında aracılığıyla /api/lookup Belge Arama API'sini çağırır. Döndürülen belgeyi bileşen durumunda depolar ve Sonuç ve Ham Veri sekmelerinde işler. içindeki \client\src\pages\Details\Details.jsx aşağıdaki kod, bileşen başlatma sırasında bu aramayı gerçekleştirir:

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>
  );
}

Bu tümleştirmeyi doğrulamak için arama sonuçlarından bir kitap seçin ve Ayrıntılar sayfasında kapak resmi, yazarlar ve derecelendirme gibi ayrıntılarının göründüğünü onaylayın.

API'yi destekleyen C# modelleri

Azure İşlevleri API'si ve toplu içeri aktarma projesi bir dizi C# model sınıfını paylaşır. Bu sınıflar arama metni, sayfalama değerleri ve filtreler gibi istemcinin gönderdiği istek gövdelerini tanımlar. Ayrıca arama sonuçları, model değerleri ve tek bir aranan belge gibi istemcinin beklediği yanıt şekillerini de tanımlar. Bu modellerin tek bir dosyada tutulması arama, öneri ve belge arama uç noktalarının React istemcisinin beklentileriyle tutarlı kalmasını sağlar. içinde Models.cstanımlanan aşağıdaki modeller, bu uygulamadaki işlevleri destekler:

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

Sonraki adım

Azure Yapay Zeka Arama geliştirme hakkında bilgi edinmeye devam etmek için dizin oluşturma hakkında sonraki öğreticiyi deneyin: