Search
Full-text product search, autocomplete suggestions, and faceted filtering with OpenSearch.
Search Guide
The open-search service provides full-text product search, autocomplete suggestions, and faceted attribute filtering. It wraps an OpenSearch index that stays current with your catalog — typically less than an hour behind, even after bulk updates.
Basic Search
Server Action
import { createSiteActions } from "@simpleapps-com/augur-server";
const actions = createSiteActions(api, {
search: { defaultSourceFields: "display_desc" },
});
const results = await actions.search.itemSearch({
q: "safety gloves",
limit: 12,
offset: 0,
sortBy: "relevance",
});
// results.items → TProductItem[]
// results.totalResults → 47Client Hook
import { useProductSearch } from "@simpleapps-com/augur-hooks";
function SearchResults({ query }) {
const { data, isLoading } = useProductSearch({
q: query,
limit: 12,
offset: 0,
sortBy: "relevance",
});
if (isLoading) return <Skeleton />;
return (
<div>
<p>{data.totalResults} results</p>
{data.items.map((item) => (
<ProductCard key={item.invMastUid} item={item} />
))}
</div>
);
}API Route
For non-React consumers or client-side fetching:
// app/api/items/search/route.ts
export async function GET(req) {
const { searchParams } = new URL(req.url);
const q = searchParams.get("q") || "";
const limit = parseInt(searchParams.get("limit") || "12", 10);
const offset = parseInt(searchParams.get("offset") || "0", 10);
const filters = searchParams.get("filters") || undefined;
const result = await augurServices.openSearch.itemSearch.list({
q,
searchType: "query",
size: limit,
from: offset,
filters,
edgeCache: 1,
});
return Response.json(result, {
headers: { "Cache-Control": "max-age=600, stale-while-revalidate=3600" },
});
}Suggestions / Autocomplete
The suggestions endpoint powers typeahead search boxes. It returns popular queries matching the user's input.
API Route
// app/api/items/suggestions/route.ts
export async function GET(req) {
const { searchParams } = new URL(req.url);
const q = searchParams.get("q") || "";
const limit = parseInt(searchParams.get("limit") || "10", 10);
const result = await augurServices.openSearch.suggestions.suggest.list({
q,
limit,
offset: 0,
edgeCache: 8,
});
return Response.json(result);
}Client Hook
import { useSearchSuggestions } from "@simpleapps-com/augur-hooks";
function SearchBox() {
const [query, setQuery] = useState("");
const { data: suggestions } = useSearchSuggestions(query, {
enabled: query.length >= 2,
});
return (
<div>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
{suggestions?.map((s) => (
<div key={s.suggestionsUid}>{s.suggestionsString}</div>
))}
</div>
);
}Suggestions are cached aggressively (edgeCache: 8 = 8 hours) since they change infrequently.
Faceted Filtering
Search results can be narrowed using attribute filters. The attributes endpoint returns available facets based on the current query.
Loading Filter Options
const attributes = await actions.search.getSearchAttributes(query);
// attributes → TAttribute[]Each attribute contains:
{
attributeUid: 42,
attributeDesc: "Material",
values: [
{ attributeValueUid: 101, attributeValue: "Leather" },
{ attributeValueUid: 102, attributeValue: "Nitrile" },
]
}Filter Format
Filters are an array of [attributeUid, attributeValueUid] tuples:
// Single filter: Material = Leather
const filters = [["42", "101"]];
// Multi-select within an attribute: Material = Leather OR Nitrile
const filters = [["42", "101"], ["42", "102"]];
// Across attributes: Material = Leather AND Size = Large
const filters = [["42", "101"], ["55", "200"]];Multiple values for the same attribute are OR'd together. Different attributes are AND'd.
Applying Filters
const results = await actions.search.itemSearch({
q: "safety gloves",
limit: 12,
offset: 0,
sortBy: "relevance",
filters: [["42", "101"], ["55", "200"]],
});URL Serialization
A common pattern is serializing filters into the URL for shareable search links:
// Serialize
const url = `/item-search?q=${query}&filters=${JSON.stringify(filters)}`;
// Parse
const filters = JSON.parse(searchParams.get("filters") || "[]");Pagination
Offset Pagination (Desktop)
Use limit and offset for traditional page-based navigation:
function SearchPagination({ totalResults, limit, offset, onPageChange }) {
const totalPages = Math.ceil(totalResults / limit);
const currentPage = Math.floor(offset / limit) + 1;
return (
<div>
{Array.from({ length: totalPages }, (_, i) => (
<button key={i} onClick={() => onPageChange(i * limit)}>
{i + 1}
</button>
))}
</div>
);
}Infinite Scroll (Mobile)
Use useItemSearchInfinite for mobile-friendly infinite scroll:
import { useItemSearchInfinite } from "@simpleapps-com/augur-hooks";
function MobileSearchResults({ query, filters, categoryUid }) {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useItemSearchInfinite(
{ q: query, limit: 12, offset: 0, sortBy: "relevance", filters },
categoryUid,
);
const items = data?.pages.flatMap((page) => page.data) ?? [];
return (
<div>
{items.map((item) => (
<ProductCard key={item.invMastUid} item={item} />
))}
{hasNextPage && (
<button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
Load More
</button>
)}
</div>
);
}Enriching Results
Search results contain product metadata (item ID, description, images, categories) but not live pricing or stock data. Fetch those separately for visible items:
function SearchResults({ items, customerId }) {
const itemIds = items.map((item) => item.itemId);
const invMastUids = items.map((item) => item.invMastUid);
// Batch fetch pricing for visible items
const { data: prices } = useQuery({
queryKey: ["searchPrices", itemIds, customerId],
queryFn: () => batchGetItemPrices(itemIds, customerId, 1),
enabled: itemIds.length > 0,
});
return items.map((item) => (
<ProductCard
key={item.invMastUid}
item={item}
price={prices?.[item.itemId]}
/>
));
}This keeps search fast (OpenSearch handles the query) while pricing and stock are fetched in parallel from their respective services.
Server-Side Prefetching
Prefetch search results on the server for instant page loads:
// app/item-search/page.tsx
import { getQueryClient } from "@simpleapps-com/augur-hooks/server";
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
export default async function SearchPage({ searchParams }) {
const queryClient = getQueryClient();
const q = searchParams.q || "";
await queryClient.prefetchQuery({
queryKey: ["productSearch", { q, limit: 12, offset: 0 }],
queryFn: () => actions.search.itemSearch({ q, limit: 12, offset: 0 }),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<SearchComponent query={q} />
</HydrationBoundary>
);
}Caching Strategy
| Layer | TTL | Notes |
|---|---|---|
| Cloudflare edge | 1 hour | edgeCache: 1 on search calls |
| Redis | 10 min | Server-side withServerCache for search results |
| Suggestions edge | 8 hours | edgeCache: 8 — suggestions change infrequently |
| Attributes Redis | 1 hour | Facet options cached server-side |
| React Query | Managed by hooks | Stale time from cache config |
Index Freshness
The OpenSearch index is continuously updated with current catalog data. Under normal conditions, the index is less than one hour behind the source system. After bulk catalog changes, the index may take up to a day to fully catch up.