Augur

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.

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 → 47

Client 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

LayerTTLNotes
Cloudflare edge1 houredgeCache: 1 on search calls
Redis10 minServer-side withServerCache for search results
Suggestions edge8 hoursedgeCache: 8 — suggestions change infrequently
Attributes Redis1 hourFacet options cached server-side
React QueryManaged by hooksStale 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.