4 - Esplorare il codice di ricerca Python
Nelle lezioni precedenti è stata aggiunta la ricerca a un'app Web statica. Questa lezione illustra i passaggi essenziali per stabilire l'integrazione. Se si sta cercando un foglio informativo su come integrare la ricerca nell'app Python, questo articolo spiega cosa è necessario sapere.
L'applicazione è disponibile:
Azure SDK azure-search-documents
L'app per le funzioni usa Azure SDK per Ricerca intelligenza artificiale di Azure:
L'app per le funzioni esegue l'autenticazione tramite l'SDK per l'API ricerca di intelligenza artificiale di Azure basata sul cloud usando il nome della risorsa, la chiave API e il nome dell'indice. I segreti vengono archiviati nelle impostazioni dell'app Web statica ed è stato eseguito il pull nella funzione come variabili di ambiente.
Configurare i segreti in un file di configurazione
Le variabili di ambiente delle impostazioni dell'app per le funzioni di Azure vengono estratte da un file, __init__.py
, condivise tra le tre funzioni 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
Funzione di Azure: eseguire ricerche nel catalogo
L'API di ricerca accetta un termine di ricerca e cerca nei documenti nell'indice di ricerca, restituendo un elenco di corrispondenze.
La funzione di Azure esegue il pull delle informazioni di configurazione della ricerca e soddisfa la query.
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: ricerca dal catalogo
Chiamare la funzione di Azure nel client React con il codice seguente.
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>
);
}
Funzione di Azure: Suggerimenti dal catalogo
L'API Suggest
accetta un termine di ricerca mentre un utente digita e suggerisce termini di ricerca, ad esempio titoli di libri e autori nei documenti nell'indice di ricerca, restituendo un piccolo elenco di corrispondenze.
Lo strumento di suggerimento di ricerca, sg
, è definito nel file di schema usato durante il caricamento bulk.
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: Suggerimenti dal catalogo
L'API della funzione Suggest viene chiamata nell'app client\src\components\SearchBar\SearchBar.js
React nell'ambito dell'inizializzazione dei componenti:
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>
);
};
Funzione di Azure: ottenere un documento specifico
L'API Lookup
accetta un ID e restituisce l'oggetto documento dall'indice di ricerca.
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: Ottenere un documento specifico
Questa API di funzione viene chiamata nell'app React nell'ambito client\src\pages\Details\Detail.js
dell'inizializzazione dei componenti:
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>
);
}