headless-intelligent-search
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIntelligent Search API Integration
Intelligent Search API 集成
Overview
概述
What this skill covers: The VTEX Intelligent Search API — the only VTEX API that is fully public and designed for direct frontend consumption. Covers all search endpoints, query parameters, response structures, faceted navigation, and the critical requirement to send analytics events.
When to use it: When implementing product search, category browsing, autocomplete, or faceted filtering in a headless VTEX storefront. This is the search solution for any custom headless frontend.
What you'll learn:
- All Intelligent Search API endpoints and their purposes
- How to implement faceted navigation with proper query parameters
- How to paginate results correctly using /
fromparametersto - Why analytics events are mandatory and how to send them via the Intelligent Search Events API - Headless
本技能涵盖内容:VTEX Intelligent Search API —— 唯一完全公开、专为前端直接调用设计的VTEX API。涵盖所有搜索端点、查询参数、响应结构、分面导航,以及发送分析事件的关键要求。
适用场景:在无头VTEX店面中实现商品搜索、分类浏览、自动补全或分面过滤时使用。这是任何自定义无头前端的搜索解决方案。
你将学到:
- 所有Intelligent Search API端点及其用途
- 如何使用正确的查询参数实现分面导航
- 如何使用/
from参数正确分页结果to - 为什么分析事件是必填项,以及如何通过Intelligent Search Events API - Headless发送这些事件
Key Concepts
核心概念
Essential knowledge before implementation:
实现前需掌握的关键知识:
Concept 1: Intelligent Search Is a PUBLIC API
概念1:Intelligent Search是公开API
Unlike most VTEX APIs, Intelligent Search does not require API keys or authentication tokens. It is designed to be called directly from the frontend. The base URL pattern is:
text
https://{accountName}.{environment}.com.br/api/io/_v/api/intelligent-search/{endpoint}This means:
- No BFF proxy needed for search queries (and proxying adds unnecessary latency)
- Results are CDN-cacheable for better performance
- No risk of credential exposure
This is the ONE exception to the "everything through BFF" rule in headless VTEX architecture.
与大多数VTEX API不同,Intelligent Search不需要API密钥或身份验证令牌。它专为前端直接调用设计。基础URL格式如下:
text
https://{accountName}.{environment}.com.br/api/io/_v/api/intelligent-search/{endpoint}这意味着:
- 无需BFF代理处理搜索查询(代理会增加不必要的延迟)
- 结果可通过CDN缓存以提升性能
- 不存在凭证泄露风险
这是无头VTEX架构中“所有请求通过BFF”规则的唯一例外。
Concept 2: Search Endpoints
概念2:搜索端点
Intelligent Search provides these core endpoints:
| 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 |
Intelligent Search提供以下核心端点:
| 端点 | 请求方法 | 用途 |
|---|---|---|
| GET | 通过查询词和/或分面搜索商品 |
| GET | 获取当前查询可用的筛选器 |
| GET | 在用户输入时提供词条和商品建议 |
| GET | 获取最热门的10个搜索词条 |
| GET | 为拼写错误的词条提供修正建议 |
| GET | 获取与当前搜索词条相似的建议词条 |
| GET | 获取为当前查询配置的横幅广告 |
Concept 3: Faceted Navigation
概念3:分面导航
Facets are the combination of filters applied to a search. The path parameter follows the format:
facetstext
/{facetKey1}/{facetValue1}/{facetKey2}/{facetValue2}Filter combination rules:
- Same facet type → OR (union): Selecting "Red" and "Blue" for color returns products matching either color
- Different facet types → AND (intersection): Selecting "Red" color and "Nike" brand returns only red Nike products
Common facet keys include: , , , , , and custom specifications configured as filterable in Intelligent Search settings.
category-1category-2brandpriceproductClusterIds分面是应用于搜索的筛选器组合。路径参数遵循以下格式:
facetstext
/{facetKey1}/{facetValue1}/{facetKey2}/{facetValue2}筛选器组合规则:
- 同一分面类型 → 或(并集):选择颜色的“红色”和“蓝色”会返回匹配任意一种颜色的商品
- 不同分面类型 → 与(交集):选择颜色“红色”和品牌“Nike”只会返回红色的Nike商品
常见的分面键包括:、、、、,以及在Intelligent Search设置中配置为可筛选的自定义规格。
category-1category-2brandpriceproductClusterIdsConcept 4: Analytics Events (Mandatory)
概念4:分析事件(必填项)
Intelligent Search improves results based on shopper behavior. For headless implementations, you must send analytics events using the Intelligent Search Events API - Headless. Without events, search ranking cannot learn and results degrade over time.
The events API base URL is:
text
https://sp.vtex.com/event-api/v1/{accountName}/eventArchitecture/Data Flow:
text
Frontend (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 eventsIntelligent Search会根据购物者行为优化搜索结果。对于无头实现,你必须通过Intelligent Search Events API - Headless发送分析事件。如果没有事件,搜索排名将无法学习优化,结果质量会随时间下降。
事件API的基础URL为:
text
https://sp.vtex.com/event-api/v1/{accountName}/event架构/数据流:
text
Frontend (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 eventsConstraints
约束条件
Rules that MUST be followed to avoid failures, security issues, or platform incompatibilities.
必须遵守的规则,以避免故障、安全问题或平台不兼容。
Constraint: MUST Send Analytics Events
约束:必须发送分析事件
Rule: Every headless search implementation MUST send analytics events to the Intelligent Search Events API - Headless. At minimum, send search impression events when results are displayed and click events when a product is selected from search results.
Why: Intelligent Search uses machine learning to rank results based on user behavior. Without analytics events, the search engine has no behavioral data and cannot personalize or optimize results. Over time, search quality degrades compared to stores that send events. Additionally, VTEX Admin search analytics dashboards will show no data.
Detection: If a search implementation renders results from Intelligent Search but has no calls to or the Intelligent Search Events API → STOP immediately. Analytics events must be implemented alongside search.
sp.vtex.com/event-api✅ CORRECT:
typescript
// 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,
})),
});
}❌ WRONG:
typescript
// 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
}规则:每个无头搜索实现都必须向Intelligent Search Events API - Headless发送分析事件。至少要在结果展示时发送搜索曝光事件,以及用户从搜索结果中选择商品时发送点击事件。
原因:Intelligent Search使用机器学习根据用户行为对结果进行排名。如果没有分析事件,搜索引擎将没有行为数据,无法进行个性化或优化。随着时间推移,搜索质量会比发送事件的店铺差。此外,VTEX后台的搜索分析仪表板将不会显示任何数据。
检测方式:如果搜索实现从Intelligent Search获取结果,但没有调用或Intelligent Search Events API → 立即停止。分析事件必须与搜索功能一同实现。
sp.vtex.com/event-api✅ 正确示例:
typescript
// 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,
})),
});
}❌ 错误示例:
typescript
// 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
}Constraint: MUST Paginate Results Correctly
约束:必须正确分页结果
Rule: Every product search request MUST include and query parameters to control pagination. The maximum page size is 50 items ( must not exceed 49, since indices are inclusive and zero-based).
fromtoto - fromWhy: Without pagination parameters, the API defaults to a small result set. Requesting too many results in a single call (or not paginating at all) causes slow responses, high memory usage on the client, and poor user experience. Additionally, the API enforces a maximum of 50 items per request.
Detection: If a call to does not include and query parameters → STOP immediately. Pagination must always be explicit.
/product_search/fromto✅ CORRECT:
typescript
// 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",
});❌ WRONG:
typescript
// 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();
}规则:每个商品搜索请求必须包含和查询参数以控制分页。最大页面大小为50条(不得超过49,因为索引是包含性的且从0开始)。
fromtoto - from原因:如果没有分页参数,API会默认返回少量结果。单次请求获取过多结果(或完全不分页)会导致响应缓慢、客户端内存占用过高,以及糟糕的用户体验。此外,API强制限制单次请求最多返回50条结果。
检测方式:如果调用不包含和查询参数 → 立即停止。分页必须始终显式设置。
/product_search/fromto✅ 正确示例:
typescript
// 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",
});❌ 错误示例:
typescript
// 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();
}Constraint: Do NOT Unnecessarily Proxy Intelligent Search Through BFF
约束:不要不必要地通过BFF代理Intelligent Search请求
Rule: Intelligent Search API requests SHOULD be made directly from the frontend. Do not route search traffic through the BFF unless you have a specific need (e.g., server-side rendering, adding custom business logic).
Why: Intelligent Search is a public API that does not require authentication. Adding a BFF proxy layer introduces an additional network hop, increases latency on every search operation, adds server cost, and prevents the CDN from caching responses efficiently. Search queries are high-frequency operations — even 50ms of added latency impacts conversion.
Detection: If all Intelligent Search calls go through a BFF endpoint instead of directly to VTEX → note this to the developer. It is not a security issue but a performance concern. If there is no justification (like SSR), recommend direct frontend calls.
✅ CORRECT:
typescript
// 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();
}❌ WRONG:
typescript
// 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
});规则:Intelligent Search API请求应直接从前端发起。除非有特定需求(如服务端渲染、添加自定义业务逻辑),否则不要将搜索流量路由到BFF。
原因:Intelligent Search是无需身份验证的公开API。添加BFF代理层会引入额外的网络跳转,增加每次搜索操作的延迟,提高服务器成本,并影响CDN对响应的缓存效率。搜索查询是高频操作 —— 即使增加50ms的延迟也会影响转化率。
检测方式:如果所有Intelligent Search调用都通过BFF端点而不是直接调用VTEX → 提醒开发者。这不是安全问题,但会影响性能。如果没有合理理由(如SSR),建议直接从前端调用。
✅ 正确示例:
typescript
// 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();
}❌ 错误示例:
typescript
// 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
});Implementation Pattern
实现模式
The canonical, recommended way to implement this feature or pattern.
推荐的标准实现方式或模式。
Step 1: Create a Search API Client
步骤1:创建Search API客户端
Build a typed client for all Intelligent Search endpoints. This runs in the frontend.
typescript
// 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();
}为所有Intelligent Search端点构建一个类型化客户端。该客户端运行在前端。
typescript
// 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();
}Step 2: Implement Faceted Navigation
步骤2:实现分面导航
Fetch available facets for the current query and render filter UI. Update the search when filters change.
typescript
// 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("/");
}获取当前查询的可用分面并渲染筛选器UI。当筛选器变化时更新搜索。
typescript
// 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("/");
}Step 3: Implement Autocomplete
步骤3:实现自动补全
Wire up the autocomplete endpoint to your search input for real-time suggestions.
typescript
// 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);
};
}将自动补全端点与搜索输入框关联,以提供实时建议。
typescript
// 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);
};
}Complete Example
完整示例
A full search implementation with products, facets, autocomplete, and analytics:
typescript
// 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);
}包含商品、分面、自动补全和分析功能的完整搜索实现:
typescript
// 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);
}Anti-Patterns
反模式
Common mistakes developers make and how to fix them.
开发者常犯的错误及修复方法。
Anti-Pattern: Not Sending the locale
Parameter
locale反模式:不发送locale
参数
localeWhat happens: Developers omit the query parameter from search requests.
localeWhy it fails: Without , Intelligent Search may return results in the wrong language or fail to apply locale-specific relevance rules. Multi-language stores will display mixed-language results, and search terms may not be properly tokenized for the target language.
localeFix: Always include the parameter in every Intelligent Search request.
localetypescript
// Always include locale in search parameters
const params = new URLSearchParams({
query: "shoes",
locale: "en-US", // Required for correct language processing
from: "0",
to: "19",
});问题:开发者在搜索请求中省略查询参数。
locale后果:如果没有,Intelligent Search可能返回错误语言的结果,或者无法应用与语言相关的相关性规则。多语言店铺会显示混合语言的结果,搜索词条可能无法针对目标语言正确分词。
locale修复:在每个Intelligent Search请求中始终包含参数。
localetypescript
// Always include locale in search parameters
const params = new URLSearchParams({
query: "shoes",
locale: "en-US", // Required for correct language processing
from: "0",
to: "19",
});Anti-Pattern: Loading All Products at Once
反模式:一次性加载所有商品
What happens: Developers set very large / ranges (e.g., 0 to 999) or implement infinite scroll that loads all results without limit.
fromtoWhy it fails: The Intelligent Search API limits results to 50 items per request. Even if it allowed more, sending large payloads degrades performance for both the API and the client. Users experience long load times and high memory consumption. Additionally, loading products beyond what is visible wastes bandwidth.
Fix: Use proper pagination with reasonable page sizes (12-24 items per page) and lazy-load subsequent pages only when the user scrolls or clicks "next page."
typescript
// 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 });
}问题:开发者设置非常大的/范围(如0到999),或者实现无限滚动时无限制加载所有结果。
fromto后果:Intelligent Search API限制单次请求最多返回50条结果。即使允许更多,发送大负载会降低API和客户端的性能。用户会经历长时间加载和高内存占用。此外,加载超出可见范围的商品会浪费带宽。
修复:使用合理的页面大小(每页12-24条)进行正确分页,仅在用户滚动或点击“下一页”时懒加载后续页面。
typescript
// 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 });
}Anti-Pattern: Rebuilding Search Ranking Logic Client-Side
反模式:在客户端重建搜索排名逻辑
What happens: Developers fetch search results and then re-sort or re-filter them in the frontend instead of using the API's built-in parameter and facet paths.
sortWhy it fails: Intelligent Search's ranking algorithm considers relevance, sales velocity, availability, and shopper behavior. Client-side re-sorting discards this intelligence. Additionally, client-side filtering only works on the current page of results, not the full catalog — a user filtering by "Red" would only see red items from the current 24 results, not from all matching products.
Fix: Use the API's parameter and facet path for all filtering and sorting. Let the search engine do what it was designed to do.
sorttypescript
// 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
});问题:开发者获取搜索结果后,在前端重新排序或过滤,而不是使用API内置的参数和分面路径。
sort后果:Intelligent Search的排名算法会考虑相关性、销售速度、库存情况和购物者行为。客户端重新排序会丢弃这些智能逻辑。此外,客户端过滤仅对当前页面的结果有效,而不是整个商品目录 —— 用户筛选“红色”时,只会看到当前24条结果中的红色商品,而不是所有匹配的商品。
修复:使用API的参数和分面路径进行所有过滤和排序。让搜索引擎完成其设计的工作。
sorttypescript
// 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
});Reference
参考资料
Links to VTEX documentation and related resources.
- Headless catalog and search — Overview of catalog browsing and search in headless stores
- Intelligent Search API reference — Complete API reference for all search endpoints
- Intelligent Search Events API - Headless — Events API for sending analytics from headless implementations
- Intelligent Search overview — General overview of Intelligent Search capabilities
- Search configuration — How to configure searchable specifications, facet ordering, and other search settings
- Autocomplete — How autocomplete suggestions work in Intelligent Search
VTEX文档及相关资源链接。
- Headless catalog and search — 无头店面中目录浏览和搜索的概述
- Intelligent Search API reference — 所有搜索端点的完整API参考
- Intelligent Search Events API - Headless — 用于从无头实现发送分析事件的事件API
- Intelligent Search overview — Intelligent Search功能的总体概述
- Search configuration — 如何配置可搜索规格、分面排序和其他搜索设置
- Autocomplete — Intelligent Search中自动补全建议的工作原理