4 – Explorer le code de recherche Python
Dans les leçons précédentes, vous avez ajouté la recherche à une application web statique. Cette leçon met en évidence les étapes essentielles qui établissent l’intégration. Si vous recherchez un aide-mémoire sur l’intégration de la recherche dans votre application Python, cet article explique ce que vous devez savoir.
L’application est disponible :
Kit SDK Azure : azure-search-documents
L’application de fonction utilise le Kit de développement logiciel (SDK) Azure pour la Recherche Azure AI :
L’application de fonction s’authentifie via le kit SDK auprès de l’API informatique Recherche Azure AI à l’aide du nom de la ressource, de la clé API et du nom de l’index. Les secrets sont stockés dans les paramètres de l’application web statique et extraits dans Azure Function en tant que variables d’environnement.
Configurer des secrets dans un fichier de configuration
Les variables d’environnement des paramètres de l’application de fonction Azure sont extraites à partir d’un fichier, __init__.py
, et sont partagées entre les trois fonctions API.
import os
def azure_config():
configs = {}
configs["search_facets"] = os.environ.get("SearchFacets", "")
configs["search_index_name"] = os.environ.get("SearchIndexName", "")
configs["search_service_name"] = os.environ.get("SearchServiceName", "")
configs["search_api_key"] = os.environ.get("SearchApiKey", "")
return configs
Azure Function : Rechercher dans le catalogue
L’API Recherche accepte un terme de recherche et effectue la recherche parmi les documents dans l’index de recherche, en retournant une liste de correspondances.
Azure Functions extrait les informations de configuration de la recherche et exécute la requête.
import logging
import azure.functions as func
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient
from shared_code import azure_config
import json
environment_vars = azure_config()
# Set Azure Search endpoint and key
endpoint = f'https://{environment_vars["search_service_name"]}.search.windows.net'
key = environment_vars["search_api_key"]
# Your index name
index_name = "good-books"
# Create Azure SDK client
search_client = SearchClient(endpoint, index_name, AzureKeyCredential(key))
# returns obj like {authors: 'array', language_code:'string'}
def read_facets(facetsString):
facets = facetsString.split(",")
output = {}
for x in facets:
if x.find("*") != -1:
newVal = x.replace("*", "")
output[newVal] = "array"
else:
output[x] = "string"
return output
# creates filters in odata syntax
def create_filter_expression(filter_list, facets):
i = 0
filter_expressions = []
return_string = ""
separator = " and "
while i < len(filter_list):
field = filter_list[i]["field"]
value = filter_list[i]["value"]
if facets[field] == "array":
print("array")
filter_expressions.append(f"{field}/any(t: search.in(t, '{value}', ','))")
else:
print("value")
filter_expressions.append(f"{field} eq '{value}'")
i += 1
return_string = separator.join(filter_expressions)
return return_string
def new_shape(docs):
old_api_shape = list(docs)
client_side_expected_shape = []
for item in old_api_shape:
new_document = {}
new_document["score"] = item["@search.score"]
new_document["highlights"] = item["@search.highlights"]
new_api_shape = {}
new_api_shape["id"] = item["id"]
new_api_shape["goodreads_book_id"] = item["goodreads_book_id"]
new_api_shape["best_book_id"] = item["best_book_id"]
new_api_shape["work_id"] = item["work_id"]
new_api_shape["books_count"] = item["books_count"]
new_api_shape["isbn"] = item["isbn"]
new_api_shape["isbn13"] = item["isbn13"]
new_api_shape["authors"] = item["authors"]
new_api_shape["original_publication_year"] = item["original_publication_year"]
new_api_shape["original_title"] = item["original_title"]
new_api_shape["title"] = item["title"]
new_api_shape["language_code"] = item["language_code"]
new_api_shape["average_rating"] = item["average_rating"]
new_api_shape["ratings_count"] = item["ratings_count"]
new_api_shape["work_ratings_count"] = item["work_ratings_count"]
new_api_shape["work_text_reviews_count"] = item["work_text_reviews_count"]
new_api_shape["ratings_1"] = item["ratings_1"]
new_api_shape["ratings_2"] = item["ratings_2"]
new_api_shape["ratings_3"] = item["ratings_3"]
new_api_shape["ratings_4"] = item["ratings_4"]
new_api_shape["ratings_5"] = item["ratings_5"]
new_api_shape["image_url"] = item["image_url"]
new_api_shape["small_image_url"] = item["small_image_url"]
new_document["document"] = new_api_shape
client_side_expected_shape.append(new_document)
return list(client_side_expected_shape)
bp=func.Blueprint()
@bp.function_name("search")
@bp.route(route="search", methods=[func.HttpMethod.GET, func.HttpMethod.POST] )
def main(req: func.HttpRequest) -> func.HttpResponse:
# variables sent in body
req_body = req.get_json()
q = req_body.get("q")
top = req_body.get("top") or 8
skip = req_body.get("skip") or 0
filters = req_body.get("filters") or []
facets = environment_vars["search_facets"]
facetKeys = read_facets(facets)
search_filter = ""
if len(filters):
search_filter = create_filter_expression(filters, facetKeys)
if q:
logging.info(f"/Search q = {q}")
search_results = search_client.search(
search_text=q,
top=top,
skip=skip,
facets=facetKeys,
filter=search_filter,
include_total_count=True,
)
returned_docs = new_shape(search_results)
# format the React app expects
full_response = {}
full_response["count"] = search_results.get_count()
full_response["facets"] = search_results.get_facets()
full_response["results"] = returned_docs
return func.HttpResponse(
body=json.dumps(full_response), mimetype="application/json", status_code=200
)
else:
return func.HttpResponse("No query param found.", status_code=200)
Client : recherche dans le catalogue
Appelez Azure Function dans le client React à l’aide du code suivant.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
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;
useEffect(() => {
setIsLoading(true);
setSkip((currentPage-1) * top);
const body = {
q: q,
top: top,
skip: skip,
filters: filters
};
axios.post( '/api/search', body)
.then(response => {
console.log(JSON.stringify(response.data))
setResults(response.data.results);
setFacets(response.data.facets);
setResultCount(response.data.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) => {
//console.log(searchTerm);
setQ(searchTerm);
}
var body;
if (isLoading) {
body = (
<div className="col-md-9">
<CircularProgress />
</div>);
} else {
body = (
<div className="col-md-9">
<Results documents={results} top={top} skip={skip} count={resultCount}></Results>
<Pager className="pager-style" currentPage={currentPage} resultCount={resultCount} resultsPerPage={resultsPerPage} setCurrentPage={setCurrentPage}></Pager>
</div>
)
}
return (
<main className="main main--search container-fluid">
<div className="row">
<div className="col-md-3">
<div className="search-bar">
<SearchBar postSearchHandler={postSearchHandler} q={q}></SearchBar>
</div>
<Facets facets={facets} filters={filters} setFilters={setFilters}></Facets>
</div>
{body}
</div>
</main>
);
}
Azure Function : suggestions à partir du catalogue
L’API Suggest
prend un terme de recherche lors de la saisie et suggère des termes de recherche tels que des titres et des auteurs de livres dans les documents de l’index de recherche, renvoyant une petite liste de correspondances.
Le générateur de suggestions de recherche, sg
, est défini dans le fichier de schéma utilisé lors du chargement en bloc.
import logging
import azure.functions as func
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient
from shared_code import azure_config
import json
environment_vars = azure_config()
# curl --header "Content-Type: application/json" \
# --request POST \
# --data '{"q":"code","top":"5", "suggester":"sg"}' \
# http://localhost:7071/api/Suggest
# Set Azure Search endpoint and key
service_name = environment_vars["search_service_name"]
endpoint = f"https://{service_name}.search.windows.net"
key = environment_vars["search_api_key"]
# Your index name
index_name = "good-books"
# Create Azure SDK client
search_client = SearchClient(endpoint, index_name, AzureKeyCredential(key))
bp=func.Blueprint()
@bp.function_name("suggest")
@bp.route(route="suggest", methods=[func.HttpMethod.GET, func.HttpMethod.POST] )
def main(req: func.HttpRequest) -> func.HttpResponse:
# variables sent in body
req_body = req.get_json()
q = req_body.get("q")
top = req_body.get("top") or 5
suggester = req_body.get("suggester") or "sg"
if q:
logging.info("/Suggest q = %s", q)
suggestions = search_client.suggest(search_text=q, suggester_name=suggester, top=top)
# format the React app expects
full_response = {}
full_response["suggestions"] = suggestions
logging.debug(suggestions)
return func.HttpResponse(
body=json.dumps(full_response), mimetype="application/json", status_code=200
)
else:
return func.HttpResponse("No query param found.", status_code=200)
Client : suggestions à partir du catalogue
L’API de fonction Suggest est appelée dans l’application React au niveau de client\src\components\SearchBar\SearchBar.js
dans le cadre de l’initialisation du composant :
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import Suggestions from './Suggestions/Suggestions';
import "./SearchBar.css";
export default function SearchBar(props) {
let [q, setQ] = useState("");
let [suggestions, setSuggestions] = useState([]);
let [showSuggestions, setShowSuggestions] = useState(false);
const onSearchHandler = () => {
props.postSearchHandler(q);
setShowSuggestions(false);
}
const suggestionClickHandler = (s) => {
document.getElementById("search-box").value = s;
setShowSuggestions(false);
props.postSearchHandler(s);
}
const onEnterButton = (event) => {
if (event.keyCode === 13) {
onSearchHandler();
}
}
const onChangeHandler = () => {
var searchTerm = document.getElementById("search-box").value;
setShowSuggestions(true);
setQ(searchTerm);
// use this prop if you want to make the search more reactive
if (props.searchChangeHandler) {
props.searchChangeHandler(searchTerm);
}
}
useEffect(_ =>{
const timer = setTimeout(() => {
const body = {
q: q,
top: 5,
suggester: 'sg'
};
if (q === '') {
setSuggestions([]);
} else {
axios.post( '/api/suggest', body)
.then(response => {
console.log(JSON.stringify(response.data))
setSuggestions(response.data.suggestions);
} )
.catch(error => {
console.log(error);
setSuggestions([]);
});
}
}, 300);
return () => clearTimeout(timer);
}, [q, props]);
var suggestionDiv;
if (showSuggestions) {
suggestionDiv = (<Suggestions suggestions={suggestions} suggestionClickHandler={(s) => suggestionClickHandler(s)}></Suggestions>);
} else {
suggestionDiv = (<div></div>);
}
return (
<div >
<div className="input-group" onKeyDown={e => onEnterButton(e)}>
<div className="suggestions" >
<input
autoComplete="off" // setting for browsers; not the app
type="text"
id="search-box"
className="form-control rounded-0"
placeholder="What are you looking for?"
onChange={onChangeHandler}
defaultValue={props.q}
onBlur={() => setShowSuggestions(false)}
onClick={() => setShowSuggestions(true)}>
</input>
{suggestionDiv}
</div>
<div className="input-group-btn">
<button className="btn btn-primary rounded-0" type="submit" onClick={onSearchHandler}>
Search
</button>
</div>
</div>
</div>
);
};
Azure Function : accéder à un document spécifique
L’Lookup
API prend un ID et retourne l’objet de document à partir de l’index de recherche.
import logging
import azure.functions as func
from azure.core.credentials import AzureKeyCredential
from azure.search.documents import SearchClient
from shared_code import azure_config
import json
environment_vars = azure_config()
# Set Azure Search endpoint and key
endpoint = f'https://{environment_vars["search_service_name"]}.search.windows.net'
key = environment_vars["search_api_key"]
# Your index name
index_name = "good-books"
# Create Azure SDK client
search_client = SearchClient(endpoint, index_name, AzureKeyCredential(key))
bp = func.Blueprint()
@bp.function_name("lookup")
@bp.route(route="lookup", methods=[func.HttpMethod.GET, func.HttpMethod.POST])
def main(req: func.HttpRequest) -> func.HttpResponse:
# http://localhost:7071/api/Lookup?id=100
docid = req.params.get("id")
if docid:
logging.info(f"/Lookup id = {docid}")
returnedDocument = search_client.get_document(key=docid)
full_response = {}
full_response["document"] = returnedDocument
return func.HttpResponse(
body=json.dumps(full_response), mimetype="application/json", status_code=200
)
else:
return func.HttpResponse("No doc id param found.", status_code=200)
Client : accéder à un document spécifique
Cette API de fonction est appelée dans l’application React à l’emplacement client\src\pages\Details\Detail.js
dans le cadre de l’initialisation du composant :
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 axios from 'axios';
import "./Details.css";
export default function Details() {
let { id } = useParams();
const [document, setDocument] = useState({});
const [selectedTab, setTab] = useState(0);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
// console.log(id);
axios.get('/api/lookup?id=' + id)
.then(response => {
console.log(JSON.stringify(response.data))
const doc = response.data.document;
setDocument(doc);
setIsLoading(false);
})
.catch(error => {
console.log(error);
setIsLoading(false);
});
}, [id]);
// View default is loading with no active tab
let detailsBody = (<CircularProgress />),
resultStyle = "nav-link",
rawStyle = "nav-link";
if (!isLoading && document) {
// View result
if (selectedTab === 0) {
resultStyle += " active";
detailsBody = (
<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>
);
}
// View raw data
else {
rawStyle += " active";
detailsBody = (
<div className="card-body text-left">
<pre><code>
{JSON.stringify(document, null, 2)}
</code></pre>
</div>
);
}
}
return (
<main className="main main--details container fluid">
<div className="card text-center result-container">
<div className="card-header">
<ul className="nav nav-tabs card-header-tabs">
<li className="nav-item"><button className={resultStyle} onClick={() => setTab(0)}>Result</button></li>
<li className="nav-item"><button className={rawStyle} onClick={() => setTab(1)}>Raw Data</button></li>
</ul>
</div>
{detailsBody}
</div>
</main>
);
}
Étapes suivantes
Commentaires
https://aka.ms/ContentUserFeedback.
Bientôt disponible : Tout au long de 2024, nous allons supprimer progressivement GitHub Issues comme mécanisme de commentaires pour le contenu et le remplacer par un nouveau système de commentaires. Pour plus d’informations, consultezEnvoyer et afficher des commentaires pour