Enhancing Sitecore-Powered Websites with Azure AI Search: Part 2 - Example Front End Implementation

SergeyYatsenko
Sitecore Technology MVP & Sr. Director
  • Twitter
  • LinkedIn

In Part 1, we explored building a custom website crawler using Azure Functions to index content for Azure AI Search. Now, in Part 2, we'll dive into creating a front-end implementation for search components using Next.js within a typical Sitecore XM Cloud setup. This guide will demonstrate how to leverage the power of Azure AI Search in your Sitecore-powered website's user interface.

FE Client Architecture

The solution consists of three main components:

1. Azure AI Search service - handles indexing and search operations

2. Next.js API routes - middleware layer between frontend and Azure Search

3. React components - user interface for search functionality

graph LR
A[Next.js Frontend] --> |API Requests| B[Next.js API Routes]
B -->|Search Queries| C[Azure AI Search]
C -->|Search Results| B
B -->|Formatted Data| A
D[Sitecore XM Cloud] -->|Content| E[Azure Function Crawler]
E -->|Indexed Content| C

style A fill:#FFB3BA
style B fill:#BAFFC9
style C fill:#BAE1FF
style D fill:#FFFFBA
style E fill:#FFD8B8

Implementation

1. Setting Up the Search Client

First, let's create a utility function to initialize the Azure Search client using the @azure/search-documents SDK:

// utils/searchClient.ts
import { SearchClient, AzureKeyCredential } from "@azure/search-documents";

const endpoint = process.env.AZURE_SEARCH_ENDPOINT || "";
const apiKey = process.env.AZURE_SEARCH_API_KEY || "";
const indexName = process.env.AZURE_SEARCH_INDEX_NAME || "";

// Initialize the search client
export const searchClient = new SearchClient(
  endpoint,
  indexName,
  new AzureKeyCredential(apiKey)
);


2. Creating the API Route

Next, we'll create an API route to handle search requests.
Using a Next.js API route to handle search requests offers several benefits:

  • Security: API credentials are kept server-side, preventing exposure to clients.
  • Simplification: Complex search logic can be abstracted away from the frontend.
  • Caching: Implement server-side caching for improved performance.
  • Customization: Easily modify search requests and responses as needed.
  • Rate Limiting: Implement rate limiting to prevent abuse of the search API.
  • Logging: Add server-side logging for better monitoring and debugging.
  • Error Handling: Centralized error handling for search-related issues.
// app/api/search/route.ts
import { NextResponse } from "next/server";
import { searchClient } from "@/utils/searchClient";

// Define the structure of a search result
interface SearchResult {
  id: string;
  title: string;
  description: string;
  url: string;
  // Additional fields can be added based on the index schema
}

export async function POST(request: Request) {
  try {
    // Parse the incoming request body
    const body = await request.json();
    const { searchQuery, filters, facets } = body;

    // Set up search options
    const searchOptions = {
      select: ["id", "title", "description", "url"], // Fields to return
      filter: filters, // Apply any filters
      facets: facets, // Include facets for refinement
      top: 10, // Limit results to 10
      skip: 0, // Start from the first result
      includeTotalCount: true, // Get total count of results
    };

    // Execute the search using Azure Search
    const searchResults = await searchClient.search(searchQuery, searchOptions);

    // Transform the results into a more usable format
    const results = [];
    for await (const result of searchResults.results) {
      results.push(result.document);
    }

    // Return the search results, total count, and facets
    return NextResponse.json({
      results,
      count: searchResults.count,
      facets: searchResults.facets,
    });
  } catch (error) {
    // Log the error and return a 500 status code
    console.error("Search error:", error);
    return NextResponse.json(
      { error: "Failed to perform search" },
      { status: 500 }
    );
  }

}

3. Building Example Search Component

Now, let's create a React component for the search interface:

// components/Search/SearchBox.tsx
"use client";

import { useState, useCallback } from "react";
import debounce from "lodash/debounce";

interface SearchBoxProps {
  onSearch: (query: string) => void;
  placeholder?: string;
}

export default function SearchBox({
  onSearch,
  placeholder = "Search...",
}: SearchBoxProps) {
  const [searchTerm, setSearchTerm] = useState("");

  // Debounce the search to prevent too many API calls
  const debouncedSearch = useCallback(
    debounce((query: string) => {
      onSearch(query);
    }, 300),
    []
  );

  const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
    const query = event.target.value;
    setSearchTerm(query);
    debouncedSearch(query);
  };

  return (
    <div className="relative">
      <input
        type="text"
        value={searchTerm}
        onChange={handleSearch}
        placeholder={placeholder}
        className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
      />
      {/* Add search icon */}
      <svg
        className="absolute right-3 top-2.5 h-5 w-5 text-gray-400"
        fill="none"
        stroke="currentColor"
        viewBox="0 0 24 24"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          strokeWidth={2}
          d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
        />
      </svg>
    </div>
  );

}

4. Creating the Search Results Component

// components/Search/SearchResults.tsx
"use client";

import { useState } from "react";

interface SearchResultProps {
  results: Array<{
    id: string;
    title: string;
    description: string;
    url: string;
  }>;
}

export default function SearchResults({ results }: SearchResultProps) {
  return (
    <div className="space-y-4">
      {results.map((result) => (
        <div
          key={result.id}
          className="p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow"
        >
          <h3 className="text-lg font-semibold text-blue-600">
            <a href={result.url}>{result.title}</a>
          </h3>
          <p className="mt-2 text-gray-600">{result.description}</p>
        </div>
      ))}
      {results.length === 0 && (
        <div className="text-center text-gray-500">No results found</div>
      )}
    </div>
  );

}

5. Putting It All Together

Finally, let's create a page that combines all these components:

// app/search/page.tsx
"use client";

import { useState } from "react";
import SearchBox from "@/components/Search/SearchBox";
import SearchResults from "@/components/Search/SearchResults";

export default function SearchPage() {
  const [searchResults, setSearchResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  const handleSearch = async (query: string) => {
    if (!query) {
      setSearchResults([]);
      return;
    }

    setIsLoading(true);
    try {
      const response = await fetch("/api/search", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          searchQuery: query,
        }),
      });

      const data = await response.json();
      setSearchResults(data.results);
    } catch (error) {
      console.error("Search failed:", error);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="max-w-4xl mx-auto px-4 py-8">
      <h1 className="text-3xl font-bold mb-8">Search</h1>
      <SearchBox onSearch={handleSearch} />

      {isLoading ? (
        <div className="mt-8 text-center">Loading...</div>
      ) : (
        <div className="mt-8">
          <SearchResults results={searchResults} />
        </div>
      )}
    </div>
  );

}

Performance Optimization

1. Server-Side Pagination: Implement pagination to handle large result sets efficiently.

2. Caching: Consider implementing caching for frequent searches.

3. Loading States: Show appropriate loading states during API calls.

Conclusion

This implementation provides a solid foundation for a modern search experience in a Sitecore CM Cloud environment using Next.js and Azure AI Search. The code is maintainable, type-safe, and follows best practices for both frontend and API development.

The solution can be extended further by adding features such as:

  • Advanced filtering
  • Faceted search
  • Search suggestions
  • Analytics integration
  • Custom ranking profiles

Useful links