4 - Explore the Python search code
In the previous lessons, you added search to a Static Web App. This lesson highlights the essential steps that establish integration. If you're looking for a cheat sheet on how to integrate search into your Python app, this article explains what you need to know.
The application is available:
Azure SDK azure-search-documents
The Function app uses the Azure SDK for Azure AI Search:
The Function app authenticates through the SDK to the cloud-based Azure AI Search API using your resource name, API key, and index name. The secrets are stored in the Static Web App settings and pulled in to the Function as environment variables.
Configure secrets in a configuration file
The Azure Function app settings environment variables are pulled in from a file, __init__.py
, shared between the three API functions.
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: Search the catalog
The Search API takes a search term and searches across the documents in the Search Index, returning a list of matches.
The Azure Function pulls in the search configuration information, and fulfills the 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: Search from the catalog
Call the Azure Function in the React client with the following code.
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 from the catalog
The Suggest
API takes a search term while a user is typing and suggests search terms such as book titles and authors across the documents in the search index, returning a small list of matches.
The search suggester, sg
, is defined in the schema file used during bulk upload.
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 from the catalog
The Suggest function API is called in the React app at client\src\components\SearchBar\SearchBar.js
as part of component initialization:
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: Get specific document
The Lookup
API takes an ID and returns the document object from the Search Index.
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: Get specific document
This function API is called in the React app at client\src\pages\Details\Detail.js
as part of component initialization:
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>
);
}
Next steps
Feedback
https://aka.ms/ContentUserFeedback.
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see:Submit and view feedback for