分面搜索 #
什么是分面搜索? #
分面搜索(Faceted Search)允许用户通过多个维度(分面)来筛选搜索结果,常见于电商网站的筛选功能。
text
┌─────────────────────────────────────────────────────────────┐
│ 搜索结果: iPhone │
├─────────────────────────────────────────────────────────────┤
│ 筛选条件: │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 品牌 │ │ 价格 │ │ 颜色 │ │
│ │ ☑ Apple(50) │ │ □ 0-500(20) │ │ ☑ 黑色(30) │ │
│ │ □ Samsung │ │ ☑ 500-1000 │ │ □ 白色(15) │ │
│ │ □ Huawei │ │ □ 1000+ │ │ □ 蓝色(5) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 搜索结果列表... │
└─────────────────────────────────────────────────────────────┘
配置分面 #
设置分面属性 #
javascript
await index.setSettings({
attributesForFaceting: [
'brand', // 品牌分面
'category', // 分类分面
'color', // 颜色分面
'price', // 价格分面
'inStock', // 库存分面
'searchable(tags)' // 可搜索的标签分面
]
});
分面类型 #
| 类型 | 配置 | 用途 |
|---|---|---|
| 普通分面 | 'brand' |
筛选和统计 |
| 仅筛选 | 'filterOnly(brand)' |
仅用于筛选,不统计 |
| 可搜索 | 'searchable(brand)' |
支持搜索分面值 |
获取分面数据 #
基本分面查询 #
javascript
const results = await index.search('iphone', {
facets: ['brand', 'category', 'color']
});
console.log(results.facets);
分面结果结构 #
javascript
{
"facets": {
"brand": {
"Apple": 150,
"Samsung": 80,
"Huawei": 30
},
"category": {
"Smartphones": 200,
"Tablets": 50,
"Accessories": 10
},
"color": {
"Black": 100,
"White": 80,
"Blue": 50
}
},
"facets_stats": {
"price": {
"min": 99,
"max": 1499,
"avg": 599.5,
"sum": 599500
}
}
}
获取所有分面值 #
javascript
const results = await index.search('', {
facets: ['brand'],
maxValuesPerFacet: 100 // 每个分面最多返回100个值
});
分面过滤 #
单选分面 #
javascript
// 选择单个品牌
const results = await index.search('iphone', {
facets: ['brand', 'category'],
facetFilters: ['brand:Apple']
});
多选分面(AND关系) #
javascript
// 同时选择品牌和分类
const results = await index.search('iphone', {
facets: ['brand', 'category'],
facetFilters: [
'brand:Apple',
'category:Smartphones'
]
});
同一分面多选(OR关系) #
javascript
// 选择多个品牌
const results = await index.search('iphone', {
facets: ['brand', 'category'],
facetFilters: [
['brand:Apple', 'brand:Samsung'] // 数组内是OR关系
]
});
组合使用 #
javascript
// 复杂组合
const results = await index.search('phone', {
facets: ['brand', 'category', 'color', 'price_range'],
facetFilters: [
['brand:Apple', 'brand:Samsung'], // Apple或Samsung
'category:Smartphones', // 且是手机
'inStock:true' // 且有库存
]
});
数值分面 #
价格区间 #
javascript
// 数据准备
{
"price": 999,
"price_range": "500-1000" // 预计算价格区间
}
// 搜索
const results = await index.search('', {
facets: ['price_range'],
facetFilters: ['price_range:500-1000']
});
数值统计 #
javascript
const results = await index.search('', {
facets: ['price']
});
// 获取价格统计
const priceStats = results.facets_stats.price;
console.log(`最低价: $${priceStats.min}`);
console.log(`最高价: $${priceStats.max}`);
console.log(`平均价: $${priceStats.avg}`);
数值过滤 #
javascript
// 使用filters进行数值过滤
const results = await index.search('', {
facets: ['brand', 'category'],
filters: 'price >= 500 AND price <= 1000'
});
分面搜索 #
搜索分面值 #
javascript
// 搜索品牌分面中包含"Ap"的值
const result = await index.searchForFacetValues('brand', 'Ap');
console.log(result.facetHits);
// [
// { value: "Apple", count: 150, highlighted: "<em>Ap</em>ple" },
// { value: "Apex", count: 20, highlighted: "<em>Ap</em>ex" }
// ]
带过滤的分面搜索 #
javascript
// 在特定条件下搜索分面值
const result = await index.searchForFacetValues('brand', 's', {
filters: 'category:Smartphones',
maxFacetHits: 10
});
分面UI实现 #
分面组件 #
javascript
class FacetWidget {
constructor(attribute, container) {
this.attribute = attribute;
this.container = container;
this.selected = [];
}
render(facetData) {
const html = Object.entries(facetData)
.map(([value, count]) => `
<label class="facet-item">
<input type="checkbox"
value="${value}"
${this.selected.includes(value) ? 'checked' : ''}>
<span>${value}</span>
<span class="count">(${count})</span>
</label>
`)
.join('');
this.container.innerHTML = html;
this.bindEvents();
}
bindEvents() {
this.container.addEventListener('change', (e) => {
const value = e.target.value;
if (e.target.checked) {
this.selected.push(value);
} else {
this.selected = this.selected.filter(v => v !== value);
}
this.onChange(this.selected);
});
}
onChange(selected) {
// 由外部实现
}
getFilters() {
if (this.selected.length === 0) return null;
return this.selected.map(v => `${this.attribute}:${v}`);
}
}
分面管理器 #
javascript
class FacetManager {
constructor(index) {
this.index = index;
this.facets = {};
this.selectedFacets = {};
}
registerFacet(attribute, options = {}) {
this.facets[attribute] = {
attribute,
type: options.type || 'disjunctive', // conjunctive 或 disjunctive
selected: []
};
}
selectFacet(attribute, value) {
const facet = this.facets[attribute];
if (!facet) return;
if (facet.type === 'conjunctive') {
facet.selected = [value];
} else {
if (!facet.selected.includes(value)) {
facet.selected.push(value);
}
}
}
deselectFacet(attribute, value) {
const facet = this.facets[attribute];
if (!facet) return;
facet.selected = facet.selected.filter(v => v !== value);
}
buildFacetFilters() {
const filters = [];
for (const [attribute, facet] of Object.entries(this.facets)) {
if (facet.selected.length === 0) continue;
if (facet.type === 'conjunctive') {
filters.push(`${attribute}:${facet.selected[0]}`);
} else {
filters.push(facet.selected.map(v => `${attribute}:${v}`));
}
}
return filters;
}
async search(query) {
const facetFilters = this.buildFacetFilters();
const facetAttributes = Object.keys(this.facets);
return await this.index.search(query, {
facets: facetAttributes,
facetFilters: facetFilters.length > 0 ? facetFilters : undefined
});
}
}
// 使用
const manager = new FacetManager(index);
manager.registerFacet('brand', { type: 'disjunctive' });
manager.registerFacet('category', { type: 'conjunctive' });
manager.registerFacet('price_range', { type: 'disjunctive' });
manager.selectFacet('brand', 'Apple');
const results = await manager.search('phone');
分面类型 #
合取分面(Conjunctive) #
AND关系,选择多个值时结果必须同时满足:
javascript
// 选择 "Smartphones" 和 "Tablets"
// 结果必须同时属于两个分类(通常无结果)
// 一般用于单选场景
facetFilters: ['category:Smartphones']
析取分面(Disjunctive) #
OR关系,选择多个值时结果满足任一即可:
javascript
// 选择 "Apple" 和 "Samsung"
// 结果可以是Apple或Samsung
facetFilters: [['brand:Apple', 'brand:Samsung']]
分面计数 #
独立计数 #
当选择一个分面时,其他分面的计数应该反映未选择状态:
javascript
async function getDisjunctiveFacets(query, selectedFacets) {
const baseParams = {
query,
facets: ['brand', 'category', 'color']
};
// 主查询
const mainResults = await index.search({
...baseParams,
facetFilters: buildFacetFilters(selectedFacets)
});
// 为每个未选择的分面单独查询
const disjunctiveQueries = Object.keys(baseParams.facets)
.filter(facet => !selectedFacets[facet])
.map(facet => ({
...baseParams,
facetFilters: buildFacetFilters(selectedFacets, facet)
}));
const disjunctiveResults = await Promise.all(
disjunctiveQueries.map(params => index.search(params))
);
// 合并结果
return mergeFacetResults(mainResults, disjunctiveResults);
}
高级分面 #
层级分面 #
javascript
// 数据结构
{
"category": "Electronics",
"category.l1": "Electronics",
"category.l2": "Electronics > Phones",
"category.l3": "Electronics > Phones > Smartphones"
}
// 搜索
const results = await index.search('', {
facets: ['category.l1', 'category.l2', 'category.l3'],
facetFilters: ['category.l2:Electronics > Phones']
});
动态分面 #
javascript
// 根据搜索结果动态显示分面
async function getDynamicFacets(query) {
const results = await index.search(query, {
facets: ['*'] // 获取所有分面
});
// 只显示有数据的分面
const activeFacets = Object.entries(results.facets)
.filter(([_, values]) => Object.keys(values).length > 1)
.map(([name]) => name);
return activeFacets;
}
分面最佳实践 #
1. 预计算区间 #
javascript
// ❌ 不推荐:运行时计算
filters: 'price >= 500 AND price < 1000'
// ✅ 推荐:预计算区间
{
"price": 750,
"price_range": "500-1000"
}
facetFilters: ['price_range:500-1000']
2. 限制分面数量 #
javascript
await index.setSettings({
maxValuesPerFacet: 50 // 每个分面最多50个值
});
3. 使用filterOnly #
javascript
// 不需要统计的分面使用filterOnly
attributesForFaceting: [
'brand',
'category',
'filterOnly(internal_id)' // 仅用于筛选
]
4. 分面排序 #
javascript
// 按计数排序(默认)
// 或按字母排序
await index.setSettings({
sortFacetValuesBy: 'alpha' // 或 'count'
});
分面示例 #
电商筛选 #
javascript
async function ecommerceSearch(query, filters = {}) {
const { brands = [], categories = [], priceRanges = [], page = 0 } = filters;
const facetFilters = [];
if (brands.length > 0) {
facetFilters.push(brands.map(b => `brand:${b}`));
}
if (categories.length > 0) {
facetFilters.push(categories.map(c => `category:${c}`));
}
if (priceRanges.length > 0) {
facetFilters.push(priceRanges.map(p => `price_range:${p}`));
}
return await index.search(query, {
facets: ['brand', 'category', 'price_range', 'color', 'size'],
facetFilters: facetFilters.length > 0 ? facetFilters : undefined,
page,
hitsPerPage: 20,
maxValuesPerFacet: 100
});
}
总结 #
分面搜索要点:
| 要点 | 说明 |
|---|---|
| 配置 | attributesForFaceting |
| 获取 | facets参数 |
| 过滤 | facetFilters参数 |
| 类型 | 合取(AND)、析取(OR) |
| 数值 | facets_stats统计 |
| 搜索 | searchForFacetValues |
接下来,让我们学习 过滤与筛选。
最后更新:2026-03-28