Use the Search SDK and API

This guide is for developers. It shows how to add Uniform Search to a Next.js app. It also shows how to call the Search SDK and the Search API from your own code.

Goals

  • Add a search page to your Next.js App Router project in 30 minutes or less.
  • Build custom search interfaces with the Search SDK and its React hooks.
  • Send search requests to the Search API from any HTTP client.

Prerequisites

  • A Uniform project with Uniform Search turned on. To get Uniform Search, request activation.
  • Content in the search index. Refer to Configure search and Operate search.
  • Team admin access to the Uniform Search tool, or a team admin who can give you the search API key.
  • A Next.js project that uses the App Router and the Uniform SDK for the Next.js App Router. The project must have a component resolver.
  • Tailwind CSS in the project. Without Tailwind CSS, the search components work, but they have no styles.
  • A Uniform API key and project ID for the Uniform CLI.

note

The search components are for the Next.js App Router only. For other frameworks, use the Search SDK or the Search API directly.

Your front end uses 4 parts of Uniform Search:

PartWhat it is
Search APIThe HTTP API that runs search queries on the search index of your project.
Search API keyThe read-only key that your front end sends to the Search API. The key starts with ufs. and works for one project only.
Search SDKThe npm package @uniformdev/search. It contains a search client for any JavaScript runtime and React hooks for the browser.
Search componentsThe Canvas components for search, such as Search Engine, Search Box, and Search Results. The create-uniform-search tool copies their React code into your project.

Business users build search pages from the search components in Canvas. The search components call the Search API through the Search SDK. For custom interfaces, you can use the Search SDK or the Search API directly.

You can add Uniform Search in 2 ways:

  • Ask an AI coding assistant to do the work. This is the fastest path.
  • Do the steps by hand.

In both ways, you first get the search URL and the search API key from the Uniform Search tool.

The Connect drawer in the Uniform Search tool shows the search URL of your project. It also makes the search API key.

  1. In your Uniform project, select Tools > Uniform Search in the project navigation.

  2. In the status strip, click Connect.

    The status strip of the Uniform Search tool on the Overview tab, with a red outline around the Connect button on the right.
    The Connect button in the status strip opens the Connect drawer.

    The Connect your frontend drawer opens.

  3. Click Generate key.

    The Connect your frontend drawer with no key yet. The Search API key field shows No key generated yet, with a short text about the search-only key and a Generate key button below it.
    Generate a search API key in the Connect drawer.

    warning

    The drawer shows the full key 1 time only. You cannot get the key again later. If you lose the key, you must rotate it.

  4. Click Copy as .env to copy the search URL and the search API key.

    The Connect your frontend drawer right after Generate key, with a red Copy this key now callout, the new search API key partly masked, the Copy as .env button with the filled environment variables, and the I have stored the key button.
    Copy the new key before you close the drawer.
  5. Paste the values into the .env.local file of your Next.js project.

  6. Click I’ve stored the key.

The copied values look like this:

.env.local

NEXT_PUBLIC_UNIFORM_SEARCH_API_URL=https://YOUR_SEARCH_API_HOST NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY=YOUR_SEARCH_API_KEY

If your project has more than one locale, add the default locale of your project:

.env.local

NEXT_PUBLIC_UNIFORM_DEFAULT_LOCALE=en-US
VariableNeededValue
NEXT_PUBLIC_UNIFORM_SEARCH_API_URLYesThe Search URL from the Connect drawer. Use the base URL only. The Search SDK adds /api/search.
NEXT_PUBLIC_UNIFORM_SEARCH_API_KEYYesThe search API key from the Connect drawer.
NEXT_PUBLIC_UNIFORM_DEFAULT_LOCALEOnly for localized projectsThe locale to search when the page URL has no locale segment, for example on /. For a project with no locales, do not set this variable.

info

You do not need a project ID for search. The search API key identifies your project.

Next.js adds NEXT_PUBLIC_ values to the JavaScript bundle when it builds the app. After you change a value, build and deploy the app again.

The Uniform agent skills include a skill for Uniform Search. The skill teaches your AI coding assistant the correct steps for your project.

  1. Install the Uniform agent skills for your coding assistant. Refer to Uniform agent skills.
  2. Add the search URL and the search API key to .env.local. Refer to Get the search URL and the search API key.
  3. Open your Next.js project in your coding assistant.
  4. Enter a prompt, for example: "Add Uniform Search to my app."

The assistant runs create-uniform-search, installs the Search SDK, and registers the search components. It also checks the environment variables and the theme. The assistant does not push the component definitions. It gives you the command. Do the steps in Push the component definitions and Publish a search page.

The create-uniform-search tool copies the search components, the helpers, and the component definitions into your project.

  1. Find the base folder of the @/* import alias in tsconfig.json. For ./*, the base is .. For ./src/*, the base is src.

  2. Go to the folder that contains the package.json of your project.

  3. Run the tool. Replace BASE with the base folder and LOCALE with the default locale of your project.

    npx -y create-uniform-search@latest --dir . --components --no-deploy --src-root BASE --locale LOCALE
  4. Install the Search SDK:

    npm install @uniformdev/search@latest
  5. Make sure that your project has the packages @uniformdev/next-app-router-client and @uniformdev/context. The search components import them.

  6. Import the theme file from your global stylesheet. For Tailwind CSS 4, add this line after @import "tailwindcss";:

    app/globals.css

    @import "../styles/search-theme.css";

    tip

    For Tailwind CSS 3, copy the mono colors from styles/search-theme.css into theme.extend.colors.mono in tailwind.config.js.

  7. Download the Uniform Context manifest. The behavior relevancy helpers read the enrichment categories from this file.

    npx uniform context manifest download --output ./lib/uniform/manifest.json
  8. If your project does not use cacheComponents: true in next.config, change components/search/Recommendations.tsx to fetch the project map paths directly. Refer to Recommendations without cache components.

  9. Do a type check and correct the errors that it shows:

    npx tsc --noEmit
OptionDescription
--dir PATHThe project folder. The default is the current folder.
--src-root PATHThe base folder for the components and helpers. It must be the base of the @/* alias, usually . or src. The alias --components-dir does the same.
--components, --no-componentsCopy, or do not copy, the components and the component definitions.
--deploy, --no-deployPush, or do not push, the component definitions to your Uniform project. The push runs only when the tool finds UNIFORM_API_KEY and UNIFORM_PROJECT_ID.
--skill, --no-skillInstall, or do not install, a Claude Code skill in .claude/skills/add-uniform-search. If you use the Uniform agent skills, use --no-skill.
--locale CODEThe locale of the component patterns in the package. If you do not set it, the tool reads the default locale from your Uniform project. If it cannot, it asks you, or uses en.
-y, --yesUse safe defaults and ask no questions: copy the components, do not push, do not install the skill. The tool does not overwrite files that are already in the project.
--dry-runShow what the tool will do. The tool writes no files.
-h, --helpShow the help.
-v, --versionShow the version.

warning

In a CI environment, or when there is no terminal, the tool asks no questions. Without --yes, it then overwrites files that are already in the project.

The tool reads UNIFORM_API_KEY and UNIFORM_PROJECT_ID from .env, then from .env.local, then from the environment. If your project is not on https://uniform.app, set UNIFORM_CLI_BASE_URL to the host of your Uniform project.

PathContents
BASE/components/search/The React code of the search components, the result card renderers, and the UI parts.
BASE/lib/search/The helpers: searchClient.ts, projectMapClient.ts, typoTolerance.ts, retrieval.ts, enrichmentCategories.ts, and cachedProjectMapPaths.ts.
BASE/styles/search-theme.cssThe Tailwind CSS theme with the mono colors that the components use.
search-components.jsonThe component definitions, the block types, the Uniform Search component category, and 2 component patterns: Search Engine and Search Autocomplete.
uniformsearch.config.jsThe Uniform CLI configuration that pushes search-components.json.

The tool does not install the Search SDK, add environment variables, import the theme, or change your component resolver. When the tool stops, it shows the next steps.

lib/search/searchClient.ts makes the search client from the 2 environment variables:

lib/search/searchClient.ts

import { createSearchClient } from '@uniformdev/search'; export const { performSearch } = createSearchClient({ apiUrl: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_URL || '', apiKey: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY, });

The Recommendations component uses lib/search/cachedProjectMapPaths.ts. This helper uses the 'use cache' directive, and Next.js builds it only when cacheComponents is true. When you turn on cacheComponents, the change applies to all routes of your app.

If you do not want to turn on cacheComponents, do these steps:

  1. In components/search/Recommendations.tsx, replace the import of getCachedPathsByNodeId with this code:

    components/search/Recommendations.tsx

    import { fetchPathsByNodeId } from '@/lib/search/projectMapClient'; const SEARCH_API_URL = process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_URL ?? '';
  2. In the same file, replace the line that calls getCachedPathsByNodeId with this line:

    components/search/Recommendations.tsx

    const pathsByNodeId = locale && SEARCH_API_URL ? await fetchPathsByNodeId(SEARCH_API_URL, locale) : {};
  3. Delete lib/search/cachedProjectMapPaths.ts.

fetchPathsByNodeId keeps the paths in memory for each locale. Each server process sends 1 request for each locale.

The search components use the compat component shape. Register them with createAdapterResolveComponentFunction in adapted mode. Keep the type IDs exactly as shown. They must match the component definitions.

components/searchMappings.ts

import { createAdapterResolveComponentFunction } from '@uniformdev/next-app-router/compat'; import type { ComponentType } from 'react'; import SearchEngine from '@/components/search/SearchEngine'; import SearchBox from '@/components/search/SearchBox'; import SearchAutocomplete from '@/components/search/SearchAutocomplete'; import SearchList from '@/components/search/SearchList'; import SearchPagination from '@/components/search/SearchPagination'; import SearchSorting from '@/components/search/SearchSorting'; import FacetContainer from '@/components/search/FacetContainer'; import SearchFacet from '@/components/search/SearchFacet'; import Recommendations from '@/components/search/Recommendations'; const adapted = (type: string, component: ComponentType<any>) => ({ type, mode: 'adapted' as const, component, }); export const searchMappings = { searchEngine: adapted('searchEngine', SearchEngine), searchBox: adapted('searchBox', SearchBox), searchAutocomplete: adapted('searchAutocomplete', SearchAutocomplete), searchList: adapted('searchList', SearchList), searchPagination: adapted('searchPagination', SearchPagination), searchSorting: adapted('searchSorting', SearchSorting), facetContainer: adapted('facetContainer', FacetContainer), searchFacet: adapted('searchFacet', SearchFacet), recommendations: adapted('recommendations', Recommendations), }; export const resolveSearchComponent = createAdapterResolveComponentFunction({ mappings: searchMappings, });

If your project already uses createAdapterResolveComponentFunction, add searchMappings to its mappings.

If your project uses a plain resolveComponent function, send the search types to the adapter first. Do not call the adapter as a fallback. The adapter returns a "Not implemented" component for types that it does not know.

components/resolveComponent.ts

import type { ResolveComponentFunction } from '@uniformdev/next-app-router'; import { resolveSearchComponent, searchMappings } from './searchMappings'; export const resolveComponent: ResolveComponentFunction = (args) => { if (args.component.type in searchMappings) return resolveSearchComponent(args); // Keep your project's mapping here, unchanged. return { component: componentMap[args.component.type] ?? DefaultNotFoundComponent }; };

The search components are client components. Do not remove the 'use client' directive. The Recommendations component is a server component.

The push adds the search component definitions, the block types, the category, and the patterns to your Uniform project.

info

The push only creates the items that are not in your project. It does not change or delete items. You can run it again with no risk.

  1. Add UNIFORM_API_KEY and UNIFORM_PROJECT_ID to the .env file of your project. The API key must have permission to create components, content types, and patterns.

  2. Go to the folder that contains uniformsearch.config.js.

  3. Optional: show the changes before you push them:

    npx @uniformdev/cli sync push --config ./uniformsearch.config.js --what-if
  4. Push the definitions:

    npx @uniformdev/cli sync push --config ./uniformsearch.config.js
  5. In Uniform, open the component definition of your page. Add Search Engine to the allowed components of the content slot.

  6. If you want a search box in your site header, add Search Autocomplete to the allowed components of the header slot.

  7. Open the Search Engine pattern. Make sure that the titles of the Order By items in Search Sort are in the language of your site. Change them if necessary.

  1. In Uniform, create a composition for your search page, for example at the path /search.

  2. Add the Search Engine pattern to the content slot. You can also add a Search Engine component and fill its slots.

  3. On the Search Engine component, set Query By and Entry Url Mapping.

  4. Add a Search Facet to the Facet Container for each field that visitors can filter by.

  5. Look at the preview and make sure that results show.

  6. Publish the composition.

  7. Build and deploy your app.

For all component parameters, refer to Build search experiences.

Rotate the key when you think that someone else has it, or when your security policy tells you to. The old key continues to work for 24 hours. In this time, update all front ends that use it.

  1. In the Uniform Search tool, click Connect.

  2. Click Rotate API key.

  3. In the Rotate the search key? dialog, click Rotate key.

    The Rotate the search key? confirmation dialog. The text says that the current key keeps working for 24 hours, and there are Cancel and Rotate key buttons.
    The old key continues to work for 24 hours after you rotate it.
  4. Copy the new key.

  5. Update NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY in all front ends. Then build and deploy them.

  6. Optional: to stop the old key before the 24 hours end, click Revoke now. Then confirm.

    The Connect your frontend drawer after a key rotation, with the new masked key and a yellow caution callout that shows the date until the previous key keeps working and a Revoke now button.
    Revoke the previous key when all front ends use the new key.

warning

After you click Revoke now, the old key stops at once. Front ends that still use the old key get 401 Unauthorized.

The Search SDK is the npm package @uniformdev/search. It needs React 18 or later only for the React entry point.

npm install @uniformdev/search

Entry points#

Import pathContentsWhere it runs
@uniformdev/searchThe search client, the types, the constants, and the helpers.Browser and server. It does not import React.
@uniformdev/search/reactSearchProvider, the React hooks, and the URL resolver providers.Browser only, in client components.

createSearchClient(config) returns a search client with 2 functions: performSearch and trackClick.

Config propertyTypeDescription
apiUrlstringThe search URL from the Connect drawer. Needed.
apiKeystringThe search API key. The client sends it in the x-api-key header.
import { createSearchClient } from '@uniformdev/search'; const client = createSearchClient({ apiUrl: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_URL ?? '', apiKey: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY, }); const result = await client.performSearch({ search: 'duvet', perPage: 10, locale: 'en-us', facetBy: 'brand', filters: { 'price[lte]': 50 }, }); console.log(result.data.total, result.data.items);

performSearch(params) sends a POST request to /api/search and returns the response. The function does not throw. If the request fails, it writes the status to the console, for example Search API error: 401. It then returns an empty result.

trackClick(params) reports a click on a result. Refer to Report clicks on results.

performSearch accepts these parameters. The Search API accepts the same fields in the request body.

ParameterTypeDescription
searchstringThe query text. An empty value matches all documents.
pagenumberThe page number. The first page is 0.
perPagenumberThe number of results on each page. The default is 10.
localestringThe locale of the search collection, for example en-us. Needed for localized projects.
queryBystringA comma-separated list of the fields to search. Results that match earlier fields rank higher. The default is all indexed text fields.
filtersRecord<string, unknown>Filters with operators. Refer to Filters.
baseFilterBystringA filter expression that applies to all requests, for example type:=product. The Base Filters parameter of Search Engine makes this value.
facetBystringA comma-separated list of the facet fields. The response has counts for these fields. The fields must be facets in the schema.
maxFacetValuesnumberThe maximum number of values for each facet.
orderBystringThe sort order. Refer to Sort order.
predefinedSortPredefinedSortAn editor-defined sort that applies before orderBy. Refer to Sort order.
enrichmentBoostEnrichmentBoostThe reduced visitor scores for behavior relevancy. Refer to Behavior relevancy.
mode'keyword' | 'semantic' | 'hybrid'The retrieval mode. Refer to Retrieval modes.
semanticRationumberThe weight of the semantic half in hybrid mode, from 0 to 1. The default comes from the project settings.
maxDistancenumberSemantic matches farther than this distance are not in the results. A lower value is stricter. The default comes from the project settings.
similarTostringA document ID. The results are the documents that are most similar to this document. The API ignores search.
maxTyposnumberThe maximum number of typos for each word: 0, 1, or 2.
minLengthFor1TyponumberThe minimum word length for 1 typo.
minLengthFor2TyposnumberThe minimum word length for 2 typos.
typoFallbackThresholdnumberIf a query has fewer results than this value, the search tries again with more typos.
prioritizeExactMatchbooleanPut exact matches above matches with typos.
diversityLambdanumberThe balance between relevance and diversity for curations that diversify results. 1 is no diversity. 0 is maximum diversity.

If you do not set a typo parameter, the search index uses its default value.

ExportDescription
resolveEnrichmentBoost({ scores, categories, maxSignals })Reduces the visitor scores to the signals for behavior relevancy.
getHighlightMatch(hit, fieldPath, fieldValue?)Returns the highlight of a field as { html } or { values }, or undefined when the field did not match. The HTML keeps only the <mark> tags.
sanitizeHighlightHtml(html)Escapes all HTML except <mark> and </mark>.
createDefaultUrlResolver(configs, options)Makes a function that returns the URL of a result. Refer to Result URLs.
buildOrderByQuery(orderBy)Converts an Order By item into an orderBy value, for example price_ASC.
toPredefinedSortParam(value)Converts the value of the Predefined Sort parameter into a predefinedSort request value.
resolveActivePredefinedSort(predefinedSort, currentOrderBy, defaultOrderBy)Returns the predefined sort only while the default sort order is active.
getSearchParamsFromUrl(url)Reads the query string of a URL into an object. Repeated keys become arrays.
flattenBlockParams(items, locale?)Converts a Uniform $block parameter value into plain objects.

SearchProvider keeps the search state for a search page. It sends the search requests and gives the results to the components inside it. The Search Engine component adds a SearchProvider for you. Do not add one to layout.tsx.

PropTypeDescription
performSearch(params: SearchParams) => Promise<CollectionResult>The search function. Needed. Usually performSearch from createSearchClient.
queryBystring[]The fields to search, in order of priority.
baseFilterStringstringA filter expression for all requests. The provider sends it as baseFilterBy.
localestringThe locale of the search collection, for example en-us.
searchDebounceMsnumberThe time in milliseconds from the last keystroke to the request. The default is 300.
maxFacetValuesnumberThe maximum number of values for each facet. The default is 100.
enrichmentBoostEnrichmentBoostSignal[]The reduced visitor scores. The provider sends them only while the sort order is behavior relevancy.
childrenReactNodeThe search interface.

SearchProvider also accepts orderBy and pageSizes. These props are for older compositions only. For new code, register the options from the child components.

The provider keeps the state in the URL query string. Visitors can share and bookmark a search:

URL keyValue
searchThe query text.
pageThe page number. In the URL, the first page is 1.
pageSizeThe number of results on each page.
orderByThe sort order.
The facet field key, for example brand1 key for each selected facet value.

When a visitor selects a value in a facet, the provider also calculates the facet counts without the filter of that facet. Thus the facet continues to show all of its options.

The provider reads the URL in the browser. It gets the results after the page loads in the browser.

useSearch() returns the search state and the functions that change it. Use it in a component inside SearchProvider. Outside a provider, it throws an error.

FieldTypeDescription
resultsPagination<SearchHit>The current results: items, page, perPage, total, and totalPages.
facetsFacets | nullThe facet counts, by field and value.
isLoadingbooleantrue while a request runs.
searchBoxValue, setSearchQuery(value)The query text and the function that changes it.
page, setPage(page)The current page and the function that changes it. The first page is 0.
pageSize, setPageSize(size)The page size and the function that changes it.
orderByOptions, selectedOrderBy, setOrderBy(value)The sort options, the current sort order, and the function that changes it.
registerFilterOption(facet), unregisterFilterOption(fieldKey)Add or remove a facet. A facet is { fieldKey, type, title }. type is 'select', 'multiSelect', or 'range'.
selectedFilters, setSelectedFilters(map)Record<string, string[]>The selected facet values, by field.
clearFilters()Removes the query text and all selected values.
currentFiltersRecord<string, unknown>The selected facet values as a filters object.
formatResultsSummary(template)Replaces {page}, {perPage}, {totalItems}, and {totalPages} in a text. {page} starts at 0.
performSearch, queryBy, locale, baseFilterStringThe values of the provider, for other components that send their own requests.

The provider requests facet counts only for the facets that you register. This example shows a search box, a facet, and a list of results:

components/ProductSearch.tsx

'use client'; import { useEffect } from 'react'; import { createSearchClient } from '@uniformdev/search'; import { SearchProvider, useSearch } from '@uniformdev/search/react'; const { performSearch } = createSearchClient({ apiUrl: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_URL ?? '', apiKey: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY, }); export function ProductSearch() { return ( <SearchProvider performSearch={performSearch} queryBy={['title', 'shortDescription']} baseFilterString="type:=product" locale="en-us" > <SearchInput /> <BrandFacet /> <SortSelect /> <Results /> </SearchProvider> ); } function SearchInput() { const { searchBoxValue, setSearchQuery } = useSearch(); return ( <input type="search" value={searchBoxValue} onChange={(event) => setSearchQuery(event.target.value)} placeholder="Search products" /> ); } function BrandFacet() { const { registerFilterOption, unregisterFilterOption, facets, selectedFilters, setSelectedFilters } = useSearch(); useEffect(() => { registerFilterOption({ fieldKey: 'brand', type: 'multiSelect', title: 'Brand' }); return () => unregisterFilterOption('brand'); }, [registerFilterOption, unregisterFilterOption]); const selected = selectedFilters.brand ?? []; const toggle = (value: string) => { const next = selected.includes(value) ? selected.filter((v) => v !== value) : [...selected, value]; setSelectedFilters({ ...selectedFilters, brand: next }); }; return ( <fieldset> <legend>Brand</legend> {Object.entries(facets?.brand ?? {}).map(([value, count]) => ( <label key={value}> <input type="checkbox" checked={selected.includes(value)} onChange={() => toggle(value)} /> {value} ({count}) </label> ))} </fieldset> ); } function SortSelect() { const { selectedOrderBy, setOrderBy } = useSearch(); return ( <select value={selectedOrderBy} onChange={(event) => setOrderBy(event.target.value)}> <option value="">Relevance</option> <option value="price_ASC">Price: low to high</option> <option value="created_DESC">Newest first</option> <option value="behavior">Recommended for you</option> </select> ); } function Results() { const { results, isLoading } = useSearch(); if (isLoading) return <p>Loading…</p>; if (results.total === 0) return <p>No results found.</p>; return ( <ul> {results.items.map((hit) => ( <li key={hit.id}>{String(hit.title ?? hit.id)}</li> ))} </ul> ); }

useAutocomplete(options) gives the state and the ARIA attributes for a search box with suggestions. It does not need SearchProvider. Use it in a site header or a dialog.

OptionTypeDescription
performSearch(params: SearchParams) => Promise<CollectionResult>The search function. Needed.
queryBystring[]The fields to search.
baseFilterBystringA filter expression for all requests.
filtersRecord<string, unknown>Filters with operators.
localestringThe locale of the search collection.
perPagenumberThe maximum number of suggestions. The default is 6.
minQueryLengthnumberThe number of characters before the first request. The default is 1.
debounceMsnumberThe time in milliseconds from the last keystroke to the request. The default is 150.
openOnFocusbooleanOpen the list when the input gets focus. The default is false.
onSelect(item: SearchHit) => voidRuns when the visitor selects a suggestion.
onSubmit(query: string) => voidRuns when the visitor pushes Enter and no suggestion is active.
maxTypos, minLengthFor1Typo, minLengthFor2Typos, typoFallbackThreshold, prioritizeExactMatchThe typo parameters. Refer to Search parameters.

The hook returns query, setQuery, suggestions, isLoading, isOpen, activeIndex, activeItem, open, close, clear, and selectItem. It also returns the prop getters getLabelProps, getInputProps, getListboxProps, and getItemProps. The prop getters add the WAI-ARIA combobox attributes and the keyboard support: Arrow Up, Arrow Down, Home, End, Enter, and Escape.

components/HeaderSearch.tsx

'use client'; import { useRouter } from 'next/navigation'; import { useAutocomplete } from '@uniformdev/search/react'; import { performSearch } from '@/lib/search/searchClient'; export function HeaderSearch() { const router = useRouter(); const { isOpen, suggestions, getLabelProps, getInputProps, getListboxProps, getItemProps } = useAutocomplete({ performSearch, queryBy: ['title'], locale: 'en-us', minQueryLength: 2, onSelect: (hit) => router.push(String(hit.path ?? '/')), onSubmit: (query) => router.push(`/search?search=${encodeURIComponent(query)}`), }); return ( <div> <label {...getLabelProps()}>Search</label> <input {...getInputProps({ placeholder: 'Search the site' })} /> {isOpen && ( <ul {...getListboxProps()}> {suggestions.map((item, index) => ( <li key={item.id} {...getItemProps({ item, index })}> {String(item.title ?? item.id)} </li> ))} </ul> )} </div> ); }

The search page reads the search URL key. Thus /search?search=QUERY opens the search page with the query.

useSimilarItems(docId, options) returns the documents that are most similar in meaning to one document. Use it for "related articles" or "similar products". It does not need SearchProvider.

OptionTypeDescription
performSearch(params: SearchParams) => Promise<CollectionResult>The search function. Needed.
limitnumberThe number of items. The default is 6.
localestringThe locale of the search collection.
baseFilterBystringA filter expression, for example to get only 1 content type.
filtersRecord<string, unknown>Filters with operators.
maxDistancenumberItems farther than this distance are not in the results.
enabledbooleanSet to false to stop the request. The default is true.

The hook returns { items, loading, error, degraded, refetch }. The source document is not in items.

components/SimilarProducts.tsx

'use client'; import { useSimilarItems } from '@uniformdev/search/react'; import { performSearch } from '@/lib/search/searchClient'; export function SimilarProducts({ documentId }: { documentId: string }) { const { items, loading } = useSimilarItems(documentId, { performSearch, limit: 4, locale: 'en-us', baseFilterBy: 'type:=product', }); if (loading || items.length === 0) return null; return ( <ul> {items.map((item) => ( <li key={item.id}>{String(item.title ?? item.id)}</li> ))} </ul> ); }

This hook needs semantic search. If semantic search is off, the results are keyword results and degraded is true. To turn on semantic search, contact your Uniform representative.

useSearchPagination(siblingCount?) reads the state of SearchProvider. It returns { pages, currentPage, hasPrev, hasNext, goToPage, goToPrev, goToNext, isLoading }.

pages contains page numbers that start at 1. It also contains the DOTS value, '...', for gaps. currentPage and goToPage use page numbers that start at 0.

components/Pager.tsx

'use client'; import { useSearchPagination, DOTS } from '@uniformdev/search/react'; export function Pager() { const { pages, currentPage, hasPrev, hasNext, goToPage, goToPrev, goToNext } = useSearchPagination(1); return ( <nav aria-label="Search results pages"> <button onClick={goToPrev} disabled={!hasPrev}>Previous</button> {pages.map((p, i) => p === DOTS ? ( <span key={`dots-${i}`}>…</span> ) : ( <button key={p} onClick={() => goToPage(Number(p) - 1)} aria-current={Number(p) - 1 === currentPage ? 'page' : undefined} > {p} </button> ) )} <button onClick={goToNext} disabled={!hasNext}>Next</button> </nav> ); }

usePagination({ currentPage, totalCount, perPage, siblingCount }) calculates the same page list from your own values. It does not need SearchProvider.

A result does not always have a URL. The URL resolver makes the URL of each result from the Entry Url Mapping parameter.

createDefaultUrlResolver(configs, { pathsByNodeId, locale }) returns a function (hit) => string | undefined. Each config is a SearchItemConfig:

PropertyTypeDescription
sourcestringThe source of the result, for example entry.
typestringThe content type of the result, for example article.
nodeIdstringThe project map node of the page for this type. The resolver uses the path of this node.
urlTemplatestringA path with tokens, for example /blog/:slug. The resolver uses it when there is no nodeId.
tokenMappingRecord<string, string>The result field for each token, for example { slug: 'slug' }.

The resolver replaces each :token with the mapped field of the result. For a composition, the resolver uses the path field of the result.

To change the URLs of all results in a part of the page, wrap it in SearchItemUrlResolverProvider. useUrlResolver() returns your resolver, or the default resolver of the Search Engine component.

components/SearchUrls.tsx

'use client'; import type { ReactNode } from 'react'; import type { SearchHit } from '@uniformdev/search'; import { SearchItemUrlResolverProvider } from '@uniformdev/search/react'; const resolver = (hit: SearchHit) => hit.type === 'product' && typeof hit.slug === 'string' ? `/shop/${hit.slug}` : undefined; export function SearchUrls({ children }: { children: ReactNode }) { return <SearchItemUrlResolverProvider resolver={resolver}>{children}</SearchItemUrlResolverProvider>; }

The scaffolded components get the project map paths from the Search API with the search API key. You do not need to call this endpoint yourself.

Behavior relevancy puts the results that match the interests of the visitor first. It uses the enrichment scores from Uniform Context. For information about enrichments, refer to Enrichments.

To use behavior relevancy in code:

  1. Get the visitor scores with useScores() from @uniformdev/next-app-router-client.
  2. Reduce the scores with resolveEnrichmentBoost. It keeps the highest value in each category and the 3 highest categories.
  3. Give the result to the enrichmentBoost prop of SearchProvider.
  4. Set the sort order to behavior.

resolveEnrichmentBoost accepts these options:

OptionTypeDescription
scoresRecord<string, number> | undefinedThe visitor scores. The keys have the form CATEGORY_VALUE.
categoriesstring[]The enrichment category IDs, for example Object.keys(manifest.project.pz.enr).
maxSignalsnumberThe number of categories to keep. The default is 3.

components/PersonalizedSearch.tsx

'use client'; import { useMemo, type ReactNode } from 'react'; import { useScores } from '@uniformdev/next-app-router-client'; import { resolveEnrichmentBoost } from '@uniformdev/search'; import { SearchProvider } from '@uniformdev/search/react'; import { performSearch } from '@/lib/search/searchClient'; import { ENRICHMENT_CATEGORIES } from '@/lib/search/enrichmentCategories'; export function PersonalizedSearch({ children }: { children: ReactNode }) { const scores = useScores(); // Memoize the signals. A new array on each render starts a new search request. const enrichmentBoost = useMemo( () => resolveEnrichmentBoost({ scores, categories: ENRICHMENT_CATEGORIES }), [scores] ); return ( <SearchProvider performSearch={performSearch} locale="en-us" enrichmentBoost={enrichmentBoost}> {children} </SearchProvider> ); }

The Search Engine component does these steps for you. Authors turn on behavior relevancy in Canvas. They add an Order By item with the field Behavior relevancy (Uniform Context) to Search Sort, or they select it in Predefined Sort.

note

Documents that were indexed before your project used enrichments have no enrichment data. Start a re-index to add it. When a request sorts by behavior relevancy with visitor scores, it uses keyword retrieval only.

If a visitor has no scores, the results have the default relevance order. The facet counts and the total do not change.

The Recommendations component shows the content that matches the interests of the visitor. It is a server component in components/search/Recommendations.tsx.

The component reads the visitor scores from the ufvd cookie on the server. It sends 1 search request with an empty query and the behavior sort order. It renders inside <Suspense>, so the rest of the page can render first.

This example shows the same request in your own server component:

components/ForYou.tsx

import { cookies } from 'next/headers'; import { CookieTransitionDataStore } from '@uniformdev/context'; import { resolveEnrichmentBoost } from '@uniformdev/search'; import { performSearch } from '@/lib/search/searchClient'; import { ENRICHMENT_CATEGORIES } from '@/lib/search/enrichmentCategories'; export async function ForYou() { const scoreCookie = (await cookies()).get('ufvd')?.value; const store = new CookieTransitionDataStore({ serverCookieValue: scoreCookie }); const scores = (store.data?.scores ?? {}) as Record<string, number>; const values = resolveEnrichmentBoost({ scores, categories: ENRICHMENT_CATEGORIES }); const result = await performSearch({ search: '', page: 0, perPage: 4, locale: 'en-us', filters: { 'type[eq]': 'product' }, orderBy: 'behavior', ...(values.length > 0 ? { enrichmentBoost: { values } } : {}), }); return ( <ul> {result.data.items.map((hit) => ( <li key={hit.id}>{String(hit.title ?? hit.id)}</li> ))} </ul> ); }

Put the component inside <Suspense>. It reads a cookie, so Next.js renders it for each request.

trackClick(params) reports a click on a result to search analytics. The data shows in the Analytics tab of the Uniform Search tool. The function does not throw and does not stop the navigation.

ParameterTypeDescription
docIdstringThe ID of the result. Needed.
localestringThe locale of the search collection.
userIdstringA stable visitor ID for aggregation.

The scaffolded Search Results component does not report clicks. To report clicks, add trackClick to your result list:

components/ResultLink.tsx

'use client'; import type { ReactNode } from 'react'; import { createSearchClient, type SearchHit } from '@uniformdev/search'; const { trackClick } = createSearchClient({ apiUrl: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_URL ?? '', apiKey: process.env.NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY, }); export function ResultLink({ hit, href, locale, children }: { hit: SearchHit; href: string; locale?: string; children: ReactNode; }) { return ( <a href={href} onClickCapture={() => trackClick({ docId: String(hit.id), locale })}> {children} </a> ); }

If analytics or click reports are off for the project, the Search API accepts the click and does not record it. For the analytics settings, refer to Operate search.

Use the Search API when you cannot use the Search SDK, for example from another language.

All requests go to the search URL from the Connect drawer, for example https://YOUR_SEARCH_API_HOST. Send the search API key in the x-api-key header.

EndpointDescription
POST /api/searchRuns a search query.
POST /api/trackReports a click on a result.

POST /api/search#

Send a JSON body with the Content-Type: application/json header. All fields are optional. For the full list, refer to Search parameters.

FieldTypeExample
searchstring"duvet"
pagenumber0
perPagenumber10
localestring"en-us"
queryBystring"title,shortDescription"
filtersobject{ "brand[in]": ["acme"], "price[lte]": 50 }
baseFilterBystring"type:=product"
facetBystring"brand,price"
maxFacetValuesnumber20
orderBystring"price_ASC"
predefinedSortobject{ "type": "field", "field": "created", "direction": "desc" }
enrichmentBoostobject{ "values": [{ "cat": "int", "key": "duvets", "score": 60 }] }
modestring"hybrid"
semanticRationumber0.3
maxDistancenumber0.75
similarTostring"DOCUMENT_ID"
maxTyposnumber1
minLengthFor1Typonumber6
minLengthFor2Typosnumber12
typoFallbackThresholdnumber3
prioritizeExactMatchbooleantrue
diversityLambdanumber0.5

filters is an object. Each key is a field name with an operator. If there is more than 1 key, a document must match all keys.

KeyValueMatches
field[eq]A string or a numberDocuments where the field is equal to the value.
field[in]An arrayDocuments where the field is equal to 1 of the values.
field[gte]A numberDocuments where the field is equal to or more than the value.
field[lte]A numberDocuments where the field is equal to or less than the value.
fieldA string or a numberDocuments where the field is equal to the value. This is the same as [eq].

For a range, send [gte] and [lte] for the same field. The Search API escapes all filter values.

{ "filters": { "type[eq]": "product", "brand[in]": ["acme", "globex"], "price[gte]": 10, "price[lte]": 50 } }

baseFilterBy and filters apply together. A document must match both.

orderBy valueSort order
Empty or not setRelevance.
FIELD_ASCThe field, from low to high. For example price_ASC.
FIELD_DESCThe field, from high to low. For example created_DESC.
behaviorBehavior relevancy. Send enrichmentBoost with it.

You can sort only by fields that are sortable in the schema. For more information, refer to Configure search.

predefinedSort is a sort that an editor sets on the Search Sort component. It applies before orderBy. orderBy then sorts the documents that have the same position. It has 3 forms:

{ "type": "field", "field": "created", "direction": "desc" }
{ "type": "behavior" }
{ "type": "conditional", "direction": "desc", "rules": [ { "filter": "brand:=Nike", "score": 2 }, { "filter": "brand:=Adidas", "score": 1 } ] }

In the conditional form, documents that match a rule with a higher score come first. With "direction": "asc", the matched documents come last. A document gets the highest score of the rules that it matches. The Search API ignores a predefined sort that is not valid.

A request can have a maximum of 3 sort clauses. A request can have only 1 conditional sort. If a conditional predefined sort and a behavior sort order are both in a request, the predefined sort wins. The response then contains a warning.

{ "orderBy": "behavior", "enrichmentBoost": { "values": [ { "cat": "int", "key": "duvet-covers", "score": 60 }, { "cat": "brand", "key": "acme", "score": 40 } ], "maxSignals": 3 } }
  • cat is the enrichment category ID. key is the value ID in the category.
  • The Search API uses a maximum of 3 signals. maxSignals cannot increase this limit.
  • Scores must be more than 0. The Search API changes scores more than 100 to 100.
  • The Search API ignores signals that are not valid. It does not return an error.
  • If there are no signals, the results have the default relevance order.

Send the reduced signals only. Use resolveEnrichmentBoost from the Search SDK, or send the highest value of each category.

modeResult
keywordResults match the words of the query.
semanticResults match the meaning of the query. The results are in order of distance.
hybridKeyword and semantic results in 1 list.

If you do not set mode, the Search API uses hybrid when semantic search is on for the project. Otherwise it uses keyword. In most cases, do not set mode. Use keyword when meaning-based matches are wrong, for example for a lookup of part numbers.

Semantic search is not available in some conditions. Then the Search API returns keyword results with HTTP status 200, and degraded is true.

  • In hybrid mode, the Search API ignores a field orderBy and adds a warning.
  • A behavior sort order with signals, or a valid predefined sort, turns off semantic retrieval for the request.
  • similarTo returns the documents that are most similar to one document. The source document is not in the results. The ID can contain only letters, digits, ., _, :, and -.

To turn on semantic search, contact your Uniform representative.

{ "data": { "items": [ { "id": "2f6c1b9e-0000-0000-0000-000000000000", "_collection": "entries", "source": "entry", "type": "product", "title": "Organic duvet cover", "slug": "organic-duvet-cover", "_highlight": { "title": { "snippet": "Organic <mark>duvet</mark> cover" } }, "_textMatch": 578730123365187700 } ], "page": 0, "perPage": 10, "total": 1, "totalPages": 1 }, "facets": { "brand": { "acme": 1 } }, "mode": "keyword", "degraded": false, "warnings": [] }
FieldTypeDescription
data.itemsSearchHit[]The results on this page.
data.pagenumberThe page number. The first page is 0.
data.perPagenumberThe number of results on each page.
data.totalnumberThe number of documents that match.
data.totalPagesnumberThe number of pages.
facetsobjectThe counts for each facet field, by value.
metadataobjectThe custom data of a curation that applies to the query. Use it for banners and promotions. Not in the response when no curation has data.
modestringThe retrieval mode that the Search API used. Only for projects with semantic search.
degradedbooleantrue when semantic retrieval was necessary but not available, so the results are keyword results.
warningsstring[]Notes about parameters that the Search API changed or ignored.

Each result has the stored fields of the document from your schema, and these fields:

FieldDescription
idThe document ID.
_collectionentries, compositions, assets, or the ID of an external source.
sourceentry, composition, asset, or the ID of an external source.
typeThe content type or composition type.
_highlightThe highlights, by field. The matched words are in <mark> tags.
_highlightsThe highlights as an array. Each item has a field.
_textMatchThe keyword score. A higher value is a better match.
_vectorDistanceThe semantic distance. A lower value is a better match. Only when semantic retrieval ran.
_matchedBykeyword, vector, or both. Only when semantic retrieval ran.

Localized fields have their plain names in the result. The response does not contain the vector data.

curl

curl -X POST "https://YOUR_SEARCH_API_HOST/api/search" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_SEARCH_API_KEY" \ -d '{ "search": "duvet", "page": 0, "perPage": 10, "locale": "en-us", "facetBy": "brand,price", "filters": { "brand[in]": ["acme"], "price[gte]": 10, "price[lte]": 50 }, "orderBy": "created_DESC" }'

fetch

const response = await fetch('https://YOUR_SEARCH_API_HOST/api/search', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': 'YOUR_SEARCH_API_KEY', }, body: JSON.stringify({ search: 'duvet', perPage: 10, locale: 'en-us', facetBy: 'brand', }), }); if (!response.ok) { throw new Error(`Search failed with status ${response.status}`); } const { data, facets } = await response.json();

POST /api/track#

Reports a click on a result. Use the same header and key as for search.

FieldTypeDescription
docIdstringThe ID of the result. Needed.
localestringThe locale of the search collection.
userIdstringA stable visitor ID.

curl

curl -X POST "https://YOUR_SEARCH_API_HOST/api/track" \ -H "Content-Type: application/json" \ -H "x-api-key: YOUR_SEARCH_API_KEY" \ -d '{ "docId": "DOCUMENT_ID", "locale": "en-us" }'

The response is { "success": true, "recorded": true }. When analytics or click reports are off, recorded is false.

StatusBodyCause
200The resultThe request is correct. Semantic problems also return 200, with degraded: true.
400{ "error": "..." }The similarTo value is not a valid document ID, or /api/track has no docId.
401{ "error": "Unauthorized" }The request has no key, or the key is wrong or revoked. Or the body has a projectId of a different project.
403{ "error": "Forbidden" }The origin of the request is not an allowed origin.
404{ "error": "Collection not found" }There is no search collection for this project and locale.
500{ "error": "Internal error" }An error occurred in Uniform Search. Try again later.
  • The search API key is read-only. It can search the index, report clicks, and read the page paths of the project map. It cannot change content or settings.
  • The search API key works for 1 project only. A request with a projectId of a different project gets 401.
  • The search API key can be in browser code. It is safe in NEXT_PUBLIC_ variables.
  • Keep push API keys on the server. A push API key can write documents to the search index. Never put a push API key in browser code or in a NEXT_PUBLIC_ variable. For push sources, refer to Configure search.
  • Uniform Search accepts browser requests only from the allowed origins of your deployment. To add the domain of your site, contact your Uniform representative.
  • Only team admins can open the Uniform Search tool and make or rotate keys.
  • Search requests use POST. Thus a shared cache does not keep a copy of the results. This is important for behavior relevancy: the results of one visitor never go to a different visitor. Do not add a public cache in front of search requests.
  • The first results page can render on the server. Call performSearch in a server component, as in the Recommendations example.
  • SearchProvider runs in the browser. It gets the results after the page loads.
  • SearchProvider waits 300 milliseconds after the last keystroke before it sends a request. useAutocomplete waits 150 milliseconds. You can change these values.
  • SearchProvider and the hooks ignore responses that come back after a newer request.
  • The project map paths stay in memory for each locale. The Recommendations component can also keep them in the Next.js cache for some minutes.
  • When authors publish content, Uniform Search updates the search index. Your app does not need to do anything.
ProblemCauseSolution
The browser console shows Search API error: 401.The request has no search API key, or the key is wrong or revoked.Copy the key again from the Connect drawer. If you cannot find it, rotate the key. Then build and deploy the app again.
All requests get 401 after an update of old code.The code sends a projectId or NEXT_PUBLIC_UNIFORM_PROJECT_ID of a different project.Remove projectId from createSearchClient and from the requests. The key identifies the project.
Requests get 401 24 hours after a key rotation.The front end still uses the previous key.Update NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY with the new key. Then build and deploy the app again.
The browser console shows Search API error: 403.The domain of your site is not an allowed origin.Contact your Uniform representative and give the domains of your site.
No results, and the browser console shows Search API error: 404.There is no search collection for the locale of the request.For a localized project, set NEXT_PUBLIC_UNIFORM_DEFAULT_LOCALE. You can also put a locale such as en-us in the first URL segment. Make sure that a re-index has run for this locale.
No results for a localized project on /en/... paths.The Search Engine component reads the locale from the URL only in the form xx-yy.Set NEXT_PUBLIC_UNIFORM_DEFAULT_LOCALE.
No results and no error.The query or the filters match no documents.Run the same query in the Search tab of the Uniform Search tool. Send the locale in lowercase, for example en-us.
A new environment variable has no effect.Next.js adds NEXT_PUBLIC_ values when it builds the app.Build and deploy the app again.
No facet counts in the response.No facet is registered, or the field is not a facet in the schema.Call registerFilterOption for each facet. Set the field as a facet on the Schema tab.
Results have degraded: true.Semantic search is not available. The results are keyword results.Look at warnings in the response. If a warning tells you to re-index, start a re-index. For other causes, contact your Uniform representative.
Behavior relevancy has no effect.The documents have no enrichment data, the visitor has no scores, or the request has no enrichmentBoost.Start a re-index. Make sure that the Context manifest contains your enrichment categories.
Composition results have no links.The project map request failed.Make sure that NEXT_PUBLIC_UNIFORM_SEARCH_API_KEY is set. Look for project map fetch failed in the browser console.
The type check shows Cannot find module '@/lib/uniform/manifest.json'.The Context manifest is not in the project.Download the manifest. Refer to Scaffold the search components by hand.
The build shows an error about "use cache".cacheComponents is off.Refer to Recommendations without cache components.
The type check shows has no exported member 'toPredefinedSortParam'.The Search SDK is older than the components.Run npm install @uniformdev/search@latest.
A search component shows "Not implemented".The resolver sends a type to the adapter that is not in searchMappings.Send only the search types to the adapter. Refer to Register the components in the resolver.
The Search tab with the query rain jacket in the Query field on the left. The Search Results panel on the right shows 7 results, and the first results are Ember Rain Jacket and Larch Rain Jacket with their fields and the matched words highlighted.
Test a query in the Search tab to compare it with the results in your app.