Erkunden Azure KI-Suche Abfrageintegration in einer C#-App

Note

Azure KI-Suche ist über das Azure Portal, REST-APIs und Azure SDKs verfügbar. Es unterstützt auch Foundry IQ, die verwaltete Wissensschicht, die Unternehmensinhalte in wiederverwendbare, berechtigungsfähige Wissensbasen für Agenten im Microsoft Foundry-Portal transformiert.

Im vorherigen Schritt haben Sie die suchfähige Website für Azure Container Apps bereitgestellt. In diesem Artikel werden die wesentlichen Schritte beschrieben, mit denen die Suchintegration hergestellt wird. Stellen Sie sich dies als Spickzettel für die Integration der Suche in Ihre Web-App vor.

Azure SDK Azure. Search.Documents

Die API verwendet die Azure SDK für Azure KI-Suche:

Die API authentifiziert sich über das SDK für die cloudbasierte Azure KI-Suche-API mithilfe des Suchdienstnamens und des Indexnamens. In Azure Container Apps stellt die Containerumgebung die Konfigurationswerte bereit. Verwaltete Identität ist der Standardmäßige Anmeldeinformationspfad.

Verwaltete Identitätsauthentifizierung

Jede Azure Funktion in der API erstellt die SearchClient Funktion über eine freigegebene SearchClientFactory Klasse, sodass jede Funktion auf die gleiche Weise authentifiziert wird. Standardmäßig erstellt die Factory eine DefaultAzureCredential und verwendet diese, um Token für Azure KI-Suche anzufordern. In Azure Container Apps wird die verwaltete Identität aufgelöst, DefaultAzureCredential die der Container-App zugewiesen ist.

Mit der folgenden Methode werden SearchClientFactory.cs diese Anmeldeinformationen erstellt. Wenn die Container-App über eine vom Benutzer zugewiesene verwaltete Identität verfügt, wird die Client-ID aus der AZURE_CLIENT_ID Umgebungsvariable übergeben DefaultAzureCredentialOptions , sodass der Tokenerwerb nicht mehrdeutig ist.

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

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

    return new DefaultAzureCredential(options);
}

Die Bicep-Infrastruktur weist den verwalteten Identitätszugriff auf die Azure KI-Suche Datenebene während der Zeit azd upzu. Mit dieser Rollenzuweisung kann die API den good-books Index abfragen, ohne einen Abfrageschlüssel in der Containerumgebung zu speichern.

Lokale und bereitgestellte Auflösung von Anmeldeinformationen

AZURE_CLIENT_ID Wenn dies nicht festgelegt ist, DefaultAzureCredential wird die Standardanmeldeinformationskette erneut durchlaufen und auf Ihre anmeldeinformationen für angemeldete Entwickler aufgelöst, z. B. das Azure CLI- oder Visual Studio Code Konto, das Sie für die Anmeldung verwendet haben. Wenn sie für Azure Container Apps bereitgestellt wird, wird die Bicep-Infrastruktur auf die Client-ID der vom Benutzer zugewiesenen verwalteten Identität festgelegtAZURE_CLIENT_ID, sodass DefaultAzureCredential diese Identität speziell ausgerichtet ist, anstatt mehrdeutig zwischen mehreren Identitäten aufzulösen, die ein Host verfügbar machen kann.

Um stattdessen API-Schlüssel zu verwenden, legen Sie vor USE_KEYLESS_AUTH der Bereitstellung fest false :

azd env set USE_KEYLESS_AUTH false
azd up

Verwenden Sie die Schlüsselauthentifizierung nur, wenn Ihre Umgebung sie benötigt.

Lokale Entwicklungseinstellungen

Für die lokale Entwicklung zeigt die Beispieldatei sample.local.settings.json die Werte an, die die API erwartet. Verwenden Sie nur lokale Einstellungen für die Entwicklung. In Azure Container Apps stellt die Bereitstellungskonfiguration die entsprechenden Containerumgebungswerte bereit.

Setting Purpose Erforderlich, wenn
SearchServiceName Name des Azure KI-Suche-Diensts. Kombiniert mit .search.windows.net dem Erstellen des Dienstendpunkt-URI. Immer
SearchIndexName Name des zu abfragenden Suchindexes. Wenn dies nicht festgelegt ist, wird good-books standardmäßig festgelegt. Fakultativ
SEARCH_USE_KEY_AUTH Der Standardwert ist "false", verwendet verwaltete Identität. Legen Sie fest, dass true anstelle der verwalteten Identität ein API-Schlüssel verwendet werden soll. Optionale Schlüsselauthentifizierung
SearchApiKey Administratorschlüssel für Azure KI-Suche. Erforderlich, wenn SEARCH_USE_KEY_AUTH ist true
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "SearchServiceName": "",
    "SearchIndexName": "good-books"
  },
  "Host": {
    "CORS": "*"
  }
}

Funktion: Durchsuchen des Katalogs

Die Search-API akzeptiert einen Suchbegriff, durchsucht die Dokumente im Suchindex und gibt eine Liste mit Treffern zurück. Über die Suggest-API werden partielle Zeichenfolgen während der Benutzereingabe an die Suchmaschine gesendet. Die API schlägt Suchbegriffe wie Buchtitel und Autoren basierend auf Dokumenten im Suchindex vor und gibt eine kleine Liste von Übereinstimmungen zurück.

Die Azure-Funktion ruft die Suchkonfigurationsinformationen aus der Containerumgebung ab, erstellt den Azure KI-Suche Client und erfüllt die Abfrage.

Der Suchvorschlag (sg) wird in der Schemadatei definiert, die während des Massenuploads verwendet wird.

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

Um die Funktion unabhängig zu überprüfen, rufen Sie /api/search einen Suchbegriff im Anforderungstext auf, und bestätigen Sie, dass die Antwort übereinstimmende Buchdokumente, eine Gesamtanzahl und Facetwerte enthält.

Client: Durchsuchen des Katalogs

Die Suchseite des React-Clients ruft die search Azure-Funktion auf, wenn der Benutzer eine Abfrage eingibt, einen Facetfilter ändert oder zu einer neuen Ergebnisseite wechselt. Der Client sendet den Suchtext, die Werte und Werte der aktuellen Seite skip sowie top alle ausgewählten Autoren- oder Sprachfilter im POST-Textkörper an /api/search. Die Funktion gibt eine Liste übereinstimmender Buchdokumente, eine Gesamtanzahl und Facetwerte zurück, die von der Seite zum Rendern der Ergebnisliste, des Pager- und Facetfilters verwendet werden. Der folgende Code in \client\src\pages\Search\Search.jsx Builds, die die Antwort anfordern und im Komponentenzustand speichern:

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

Um diese Integration zu überprüfen, geben Sie einen Suchbegriff in die Suchleiste der Website ein, und bestätigen Sie, dass die Ergebnisliste, die Ergebnisanzahl und die Facets alle aktualisiert werden.

Client: Vorschläge aus dem Katalog

Die Suggest-Funktions-API wird in der React-App \client\src\components\SearchBar\SearchBar.jsx als Teil der Komponente Material UI AutoVervollständigen aufgerufen. Diese Komponente verwendet den Eingabetext, um nach Autoren und Büchern zu suchen, die übereinstimmen. Anschließend werden diese möglichen Übereinstimmungen als auswählbare Elemente in der Dropdownliste angezeigt.

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

Um diese Integration zu überprüfen, geben Sie Text in die Suchleiste der Website ein, und bestätigen Sie, dass übereinstimmende Buchtitel und Autoren im Dropdownmenü "AutoVervollständigen" angezeigt werden.

Funktion: Abrufen eines bestimmten Dokuments

Die Dokumentsuche-API ruft das vollständige Dokument für ein einzelnes Buch ab, nachdem ein Benutzer es aus den Suchergebnissen auswählt. Die Funktion liest ein Buch id aus der Abfragezeichenfolge der Anforderung, verwendet SearchClientFactory zum Erstellen einer authentifizierten SearchClientZeichenfolge und Aufrufe GetDocumentAsync zum Nachschlagen dieses Schlüssels im good-books Index. Es gibt das resultierende Dokument zurück, das in ein LookupOutput Objekt eingeschlossen ist.

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

Um die Nachschlagefunktion unabhängig zu überprüfen, rufen Sie /api/lookup ein gültiges Buch id auf, und bestätigen Sie, dass die Antwort das vollständige Dokument dieses Buchs zurückgibt.

Client: Abrufen eines bestimmten Dokuments

Wenn ein Benutzer ein Buch aus den Suchergebnissen auswählt, benötigt die Seite "Details" das vollständige Dokument für dieses Buch, einschließlich der Felder, die nicht in der Zusammenfassungsliste angezeigt werden. Auf der Seite "Details" wird das Buch id aus den Routenparametern gelesen und die Dokumentsuche-API aufgerufen, wenn /api/lookup die Komponente bereitgestellt wird. Es speichert das zurückgegebene Dokument im Komponentenzustand und rendert es auf den Registerkarten "Ergebnis " und " Rohdaten ". Der folgende Code führt \client\src\pages\Details\Details.jsx diese Suche während der Komponenteninitialisierung aus:

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

Um diese Integration zu überprüfen, wählen Sie ein Buch aus den Suchergebnissen aus, und bestätigen Sie, dass ihre Details, einschließlich Deckblattbild, Autoren und Bewertung, auf der Seite "Details" angezeigt werden.

C#-Modelle, die die API unterstützen

Die Azure Functions-API und das Massenimportprojekt teilen eine Reihe von C#-Modellklassen. Diese Klassen definieren die Vom Client gesendeten Anforderungstexte, z. B. Suchtext, Pagingwerte und Filter. Sie definieren auch die Antwort-Shapes, die der Client erwartet, z. B. Suchergebnisse, Facetwerte und ein einzelnes nachschlagende Dokument. Durch die Beibehaltung dieser Modelle in einer Datei wird sichergestellt, dass die Endpunkte für Die Suche, Vorschläge und Dokumentsuche den Erwartungen des React-Clients entsprechen. Die folgenden Modelle, die in Models.csdefiniert sind, unterstützen die Funktionen in dieser App:

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

Nächster Schritt

Wenn Sie weiter über Azure KI-Suche Entwicklung erfahren möchten, probieren Sie dieses nächste Lernprogramm zur Indizierung aus: