Loading...
Loading...
Apply when implementing search functionality, faceted navigation, or autocomplete in a headless VTEX storefront. Covers product_search, autocomplete_suggestions, facets, banners, correction_search, and top_searches endpoints, plus analytics event collection. Use for any custom frontend that integrates VTEX Intelligent Search API for product discovery and search result rendering.
npx skill4agent add vtexdocs/ai-skills headless-intelligent-searchfromtohttps://{accountName}.{environment}.com.br/api/io/_v/api/intelligent-search/{endpoint}| Endpoint | Method | Purpose |
|---|---|---|
| GET | Search products by query and/or facets |
| GET | Get available filters for a query |
| GET | Get term and product suggestions while typing |
| GET | Get the 10 most popular search terms |
| GET | Get spelling correction for a misspelled term |
| GET | Get suggested terms similar to the search term |
| GET | Get banners configured for a query |
facets/{facetKey1}/{facetValue1}/{facetKey2}/{facetValue2}category-1category-2brandpriceproductClusterIdshttps://sp.vtex.com/event-api/v1/{accountName}/eventFrontend (Browser)
│
├── GET /api/io/_v/api/intelligent-search/product_search/...
│ └── Returns: products, facets, pagination info
│
├── GET /api/io/_v/api/intelligent-search/facets/...
│ └── Returns: available filters with counts
│
├── GET /api/io/_v/api/intelligent-search/autocomplete_suggestions?query=...
│ └── Returns: suggested terms + suggested products
│
└── POST https://sp.vtex.com/event-api/v1/{account}/event
└── Sends: search impressions, clicks, add-to-cart eventssp.vtex.com/event-api// search-analytics.ts — sends events to Intelligent Search Events API
const ACCOUNT_NAME = "mystore";
const EVENTS_URL = `https://sp.vtex.com/event-api/v1/${ACCOUNT_NAME}/event`;
interface SearchEvent {
type: "search.query" | "search.click" | "search.add_to_cart";
text: string;
misspelled: boolean;
match: number;
operator: string;
locale: string;
agent: string;
url: string;
products?: Array<{ productId: string; position: number }>;
}
export async function sendSearchEvent(event: SearchEvent): Promise<void> {
try {
await fetch(EVENTS_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(event),
keepalive: true, // ensures event is sent even during navigation
});
} catch (error) {
// Analytics failures should not break the UI
console.warn("Failed to send search event:", error);
}
}
// Usage: call after rendering search results
function onSearchResultsRendered(query: string, products: Product[]): void {
sendSearchEvent({
type: "search.query",
text: query,
misspelled: false,
match: products.length,
operator: "and",
locale: "en-US",
agent: "my-headless-store",
url: window.location.href,
products: products.map((p, i) => ({
productId: p.productId,
position: i + 1,
})),
});
}// Search works but NO analytics events are sent — search ranking degrades
async function searchProducts(query: string): Promise<Product[]> {
const response = await fetch(
`https://mystore.vtexcommercestable.com.br/api/io/_v/api/intelligent-search/product_search/?query=${query}&locale=en-US`
);
const data = await response.json();
return data.products;
// Missing: no call to sendSearchEvent() — Intelligent Search cannot learn
}fromtoto - from/product_search/fromto// Properly paginated search with from/to parameters
interface SearchOptions {
query: string;
page: number;
pageSize: number;
locale: string;
facets?: string;
}
async function searchProducts(options: SearchOptions): Promise<SearchResponse> {
const { query, page, pageSize, locale, facets = "" } = options;
// Calculate zero-based from/to (inclusive)
const from = page * pageSize;
const to = from + pageSize - 1;
const params = new URLSearchParams({
query,
locale,
from: String(from),
to: String(to),
});
const baseUrl = `https://${ACCOUNT}.vtexcommercestable.com.br`;
const facetPath = facets ? `/${facets}` : "";
const url = `${baseUrl}/api/io/_v/api/intelligent-search/product_search${facetPath}?${params}`;
const response = await fetch(url);
return response.json();
}
// Usage
const results = await searchProducts({
query: "running shoes",
page: 0,
pageSize: 20,
locale: "en-US",
facets: "category-1/shoes",
});// No pagination — returns default small result set, no way to load more
async function searchProducts(query: string): Promise<SearchResponse> {
const response = await fetch(
`https://mystore.vtexcommercestable.com.br/api/io/_v/api/intelligent-search/product_search/?query=${query}`
// Missing: from, to, locale parameters
);
return response.json();
}// Frontend — calls Intelligent Search directly (no BFF needed)
const VTEX_SEARCH_BASE = `https://${ACCOUNT}.vtexcommercestable.com.br/api/io/_v/api/intelligent-search`;
export async function getAutocomplete(term: string, locale: string): Promise<AutocompleteResponse> {
const params = new URLSearchParams({ query: term, locale });
const response = await fetch(`${VTEX_SEARCH_BASE}/autocomplete_suggestions?${params}`);
return response.json();
}
export async function getTopSearches(locale: string): Promise<TopSearchesResponse> {
const params = new URLSearchParams({ locale });
const response = await fetch(`${VTEX_SEARCH_BASE}/top_searches?${params}`);
return response.json();
}
export async function getFacets(facetPath: string, query: string, locale: string): Promise<FacetsResponse> {
const params = new URLSearchParams({ query, locale });
const response = await fetch(`${VTEX_SEARCH_BASE}/facets/${facetPath}?${params}`);
return response.json();
}// BFF proxy for Intelligent Search — unnecessary overhead
// server/routes/search.ts
router.get("/api/bff/search", async (req, res) => {
const { query, from, to, locale } = req.query;
// This just forwards to VTEX with no added value
const response = await fetch(
`https://mystore.vtexcommercestable.com.br/api/io/_v/api/intelligent-search/product_search/?query=${query}&from=${from}&to=${to}&locale=${locale}`
);
const data = await response.json();
res.json(data); // Added latency for no benefit
});// lib/intelligent-search-client.ts
const ACCOUNT = "mystore";
const ENVIRONMENT = "vtexcommercestable";
const BASE_URL = `https://${ACCOUNT}.${ENVIRONMENT}.com.br/api/io/_v/api/intelligent-search`;
interface ProductSearchParams {
query?: string;
from?: number;
to?: number;
locale: string;
facets?: string;
sort?: "price:asc" | "price:desc" | "orders:desc" | "name:asc" | "name:desc" | "release:desc" | "discount:desc";
hideUnavailableItems?: boolean;
}
interface SearchProduct {
productId: string;
productName: string;
brand: string;
brandId: number;
link: string;
linkText: string;
categories: string[];
priceRange: {
sellingPrice: { highPrice: number; lowPrice: number };
listPrice: { highPrice: number; lowPrice: number };
};
items: Array<{
itemId: string;
name: string;
images: Array<{ imageUrl: string; imageLabel: string }>;
sellers: Array<{
sellerId: string;
sellerName: string;
commertialOffer: {
Price: number;
ListPrice: number;
AvailableQuantity: number;
};
}>;
}>;
}
interface ProductSearchResponse {
products: SearchProduct[];
recordsFiltered: number;
correction?: { misspelled: boolean };
fuzzy: string;
operator: string;
translated: boolean;
pagination: {
count: number;
current: { index: number; proxyUrl: string };
before: Array<{ index: number; proxyUrl: string }>;
after: Array<{ index: number; proxyUrl: string }>;
perPage: number;
next: { index: number; proxyUrl: string };
previous: { index: number; proxyUrl: string };
first: { index: number; proxyUrl: string };
last: { index: number; proxyUrl: string };
};
}
export async function productSearch(params: ProductSearchParams): Promise<ProductSearchResponse> {
const { facets = "", ...queryParams } = params;
const searchParams = new URLSearchParams();
if (queryParams.query) searchParams.set("query", queryParams.query);
if (queryParams.from !== undefined) searchParams.set("from", String(queryParams.from));
if (queryParams.to !== undefined) searchParams.set("to", String(queryParams.to));
searchParams.set("locale", queryParams.locale);
if (queryParams.sort) searchParams.set("sort", queryParams.sort);
if (queryParams.hideUnavailableItems) searchParams.set("hideUnavailableItems", "true");
const facetPath = facets ? `/${facets}` : "";
const url = `${BASE_URL}/product_search${facetPath}?${searchParams}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Search failed: ${response.status}`);
}
return response.json();
}// lib/facets.ts
interface FacetValue {
id: string;
quantity: number;
name: string;
key: string;
value: string;
selected: boolean;
href: string;
}
interface Facet {
type: "TEXT" | "NUMBER" | "PRICERANGE";
name: string;
hidden: boolean;
quantity: number;
values: FacetValue[];
}
interface FacetsResponse {
facets: Facet[];
breadcrumb: Array<{ name: string; href: string }>;
queryArgs: {
query: string;
map: string;
};
}
export async function getFacets(
facetPath: string,
query: string,
locale: string
): Promise<FacetsResponse> {
const params = new URLSearchParams({ query, locale });
const url = `${BASE_URL}/facets/${facetPath}?${params}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Facets fetch failed: ${response.status}`);
}
return response.json();
}
// Build facet path from selected filters
export function buildFacetPath(selectedFilters: Record<string, string[]>): string {
const parts: string[] = [];
for (const [key, values] of Object.entries(selectedFilters)) {
for (const value of values) {
parts.push(`${key}/${value}`);
}
}
return parts.join("/");
}// lib/autocomplete.ts
interface AutocompleteSuggestion {
term: string;
count: number;
attributes: Array<{
key: string;
value: string;
labelKey: string;
labelValue: string;
}>;
}
interface AutocompleteResponse {
searches: AutocompleteSuggestion[];
}
export async function getAutocompleteSuggestions(
query: string,
locale: string
): Promise<AutocompleteResponse> {
const params = new URLSearchParams({ query, locale });
const url = `${BASE_URL}/autocomplete_suggestions?${params}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Autocomplete failed: ${response.status}`);
}
return response.json();
}
// Debounced autocomplete for use in search inputs
export function createDebouncedAutocomplete(delayMs: number = 300) {
let timeoutId: ReturnType<typeof setTimeout> | null = null;
return function debouncedAutocomplete(
query: string,
locale: string,
callback: (suggestions: AutocompleteResponse) => void
): void {
if (timeoutId) clearTimeout(timeoutId);
if (query.length < 2) {
callback({ searches: [] });
return;
}
timeoutId = setTimeout(async () => {
const suggestions = await getAutocompleteSuggestions(query, locale);
callback(suggestions);
}, delayMs);
};
}// search-page.ts — framework-agnostic search orchestration
import { productSearch, ProductSearchResponse } from "./lib/intelligent-search-client";
import { getFacets, buildFacetPath, FacetsResponse } from "./lib/facets";
import { createDebouncedAutocomplete } from "./lib/autocomplete";
import { sendSearchEvent } from "./search-analytics";
interface SearchState {
query: string;
page: number;
pageSize: number;
locale: string;
selectedFilters: Record<string, string[]>;
sort?: string;
results: ProductSearchResponse | null;
facets: FacetsResponse | null;
}
const state: SearchState = {
query: "",
page: 0,
pageSize: 24,
locale: "en-US",
selectedFilters: {},
results: null,
facets: null,
};
const debouncedAutocomplete = createDebouncedAutocomplete(300);
// Execute search with current state
async function executeSearch(): Promise<void> {
const facetPath = buildFacetPath(state.selectedFilters);
const [searchResults, facetResults] = await Promise.all([
productSearch({
query: state.query,
from: state.page * state.pageSize,
to: (state.page * state.pageSize) + state.pageSize - 1,
locale: state.locale,
facets: facetPath,
}),
getFacets(facetPath, state.query, state.locale),
]);
state.results = searchResults;
state.facets = facetResults;
// Send analytics event after results are rendered
sendSearchEvent({
type: "search.query",
text: state.query,
misspelled: searchResults.correction?.misspelled ?? false,
match: searchResults.recordsFiltered,
operator: searchResults.operator,
locale: state.locale,
agent: "my-headless-store",
url: window.location.href,
products: searchResults.products.map((p, i) => ({
productId: p.productId,
position: state.page * state.pageSize + i + 1,
})),
});
}
// Handle search input
function onSearchInput(query: string): void {
debouncedAutocomplete(query, state.locale, (suggestions) => {
// Render autocomplete dropdown — implementation depends on your UI framework
renderAutocomplete(suggestions);
});
}
// Handle search submit
function onSearchSubmit(query: string): void {
state.query = query;
state.page = 0;
state.selectedFilters = {};
executeSearch();
}
// Handle filter toggle
function onFilterToggle(facetKey: string, facetValue: string): void {
const current = state.selectedFilters[facetKey] || [];
const index = current.indexOf(facetValue);
if (index === -1) {
state.selectedFilters[facetKey] = [...current, facetValue];
} else {
state.selectedFilters[facetKey] = current.filter((v) => v !== facetValue);
if (state.selectedFilters[facetKey].length === 0) {
delete state.selectedFilters[facetKey];
}
}
state.page = 0;
executeSearch();
}
// Handle pagination
function onPageChange(newPage: number): void {
state.page = newPage;
executeSearch();
}
// Handle product click from search results
function onProductClick(productId: string, position: number): void {
sendSearchEvent({
type: "search.click",
text: state.query,
misspelled: false,
match: state.results?.recordsFiltered ?? 0,
operator: state.results?.operator ?? "and",
locale: state.locale,
agent: "my-headless-store",
url: window.location.href,
products: [{ productId, position }],
});
}
// Placeholder render function — replace with your framework's rendering
function renderAutocomplete(suggestions: { searches: Array<{ term: string }> }): void {
// Your framework-specific rendering logic here
console.log("Autocomplete suggestions:", suggestions.searches);
}localelocalelocalelocale// Always include locale in search parameters
const params = new URLSearchParams({
query: "shoes",
locale: "en-US", // Required for correct language processing
from: "0",
to: "19",
});fromto// Proper pagination with bounded page sizes
const PAGE_SIZE = 24; // Reasonable default
const MAX_PAGE_SIZE = 50; // API maximum
function getSearchPage(query: string, page: number, locale: string) {
const safePageSize = Math.min(PAGE_SIZE, MAX_PAGE_SIZE);
const from = page * safePageSize;
const to = from + safePageSize - 1;
return productSearch({ query, from, to, locale });
}sortsort// Use API-level sorting — don't re-sort in the frontend
const results = await productSearch({
query: "shirt",
sort: "price:asc", // API handles sorting across entire result set
locale: "en-US",
from: 0,
to: 23,
facets: "category-1/clothing/color/red", // API handles filtering across entire catalog
});