分页与排序 #
分页机制 #
基本分页 #
javascript
const results = await index.search('iphone', {
page: 0, // 页码(从0开始)
hitsPerPage: 20 // 每页结果数
});
console.log(results.page); // 当前页码
console.log(results.nbPages); // 总页数
console.log(results.hitsPerPage); // 每页结果数
console.log(results.nbHits); // 总匹配数
分页结果结构 #
javascript
{
"hits": [...], // 当前页结果
"page": 0, // 当前页码
"nbPages": 15, // 总页数
"hitsPerPage": 20, // 每页结果数
"nbHits": 290, // 总匹配数
"exhaustiveNbHits": true // 是否精确计数
}
偏移量分页 #
javascript
// 使用offset和length替代page
const results = await index.search('iphone', {
offset: 40, // 跳过前40条
length: 20 // 获取20条
});
分页限制 #
| 参数 | 默认值 | 最大值 |
|---|---|---|
| hitsPerPage | 20 | 1000 |
| page | 0 | 无限制 |
| offset | 0 | 无限制 |
| length | 20 | 1000 |
分页实现 #
分页组件 #
javascript
class Pagination {
constructor(options = {}) {
this.currentPage = 0;
this.hitsPerPage = options.hitsPerPage || 20;
this.maxVisiblePages = options.maxVisiblePages || 5;
}
async goToPage(page) {
this.currentPage = page;
return await this.search();
}
async nextPage() {
return await this.goToPage(this.currentPage + 1);
}
async prevPage() {
if (this.currentPage > 0) {
return await this.goToPage(this.currentPage - 1);
}
}
getVisiblePages(totalPages) {
const pages = [];
let start = Math.max(0, this.currentPage - Math.floor(this.maxVisiblePages / 2));
let end = Math.min(totalPages - 1, start + this.maxVisiblePages - 1);
if (end - start < this.maxVisiblePages - 1) {
start = Math.max(0, end - this.maxVisiblePages + 1);
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
}
render(totalPages) {
const visiblePages = this.getVisiblePages(totalPages);
return `
<div class="pagination">
<button ${this.currentPage === 0 ? 'disabled' : ''}
onclick="pagination.prevPage()">上一页</button>
${visiblePages.map(p => `
<button class="${p === this.currentPage ? 'active' : ''}"
onclick="pagination.goToPage(${p})">${p + 1}</button>
`).join('')}
<button ${this.currentPage >= totalPages - 1 ? 'disabled' : ''}
onclick="pagination.nextPage()">下一页</button>
</div>
`;
}
}
无限滚动 #
javascript
class InfiniteScroll {
constructor(options = {}) {
this.page = 0;
this.hitsPerPage = options.hitsPerPage || 20;
this.loading = false;
this.hasMore = true;
this.items = [];
}
async loadMore() {
if (this.loading || !this.hasMore) return;
this.loading = true;
const results = await index.search(this.query, {
page: this.page,
hitsPerPage: this.hitsPerPage
});
this.items = [...this.items, ...results.hits];
this.page++;
this.hasMore = results.page < results.nbPages - 1;
this.loading = false;
return results.hits;
}
reset() {
this.page = 0;
this.items = [];
this.hasMore = true;
}
}
// 使用
const infiniteScroll = new InfiniteScroll();
// 监听滚动事件
window.addEventListener('scroll', async () => {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {
const newItems = await infiniteScroll.loadMore();
renderItems(newItems);
}
});
排序机制 #
默认排序 #
Algolia默认按相关性排序:
text
排名公式: typo → geo → words → filters → proximity → attribute → exact → custom
自定义排名 #
javascript
await index.setSettings({
customRanking: [
'desc(rating)', // 评分降序
'desc(popularity)', // 人气降序
'asc(price)' // 价格升序
]
});
排序方向 #
| 方向 | 说明 | 示例 |
|---|---|---|
| desc | 降序 | desc(rating) |
| asc | 升序 | asc(price) |
排序副本索引 #
为什么需要副本索引? #
当需要按不同字段排序时,需要创建副本索引:
javascript
// 主索引 - 按相关性排序
const mainIndex = client.initIndex('products');
// 价格升序索引
const priceAscIndex = client.initIndex('products_price_asc');
// 价格降序索引
const priceDescIndex = client.initIndex('products_price_desc');
// 销量排序索引
const salesIndex = client.initIndex('products_sales');
创建副本索引 #
javascript
// 创建副本索引
await client.initIndex('products').setSettings({
replicas: [
'products_price_asc',
'products_price_desc',
'products_sales',
'products_rating'
]
});
// 配置副本索引排序
await client.initIndex('products_price_asc').setSettings({
ranking: ['asc(price)', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom']
});
await client.initIndex('products_price_desc').setSettings({
ranking: ['desc(price)', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom']
});
await client.initIndex('products_sales').setSettings({
ranking: ['desc(salesCount)', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom']
});
await client.initIndex('products_rating').setSettings({
ranking: ['desc(rating)', 'typo', 'geo', 'words', 'proximity', 'attribute', 'exact', 'custom']
});
使用副本索引搜索 #
javascript
async function searchWithSort(query, sortBy = 'default') {
const indexMap = {
'default': 'products',
'price_asc': 'products_price_asc',
'price_desc': 'products_price_desc',
'sales': 'products_sales',
'rating': 'products_rating'
};
const indexName = indexMap[sortBy] || indexMap['default'];
const index = client.initIndex(indexName);
return await index.search(query, {
hitsPerPage: 20
});
}
排序UI实现 #
排序选择器 #
javascript
class SortSelector {
constructor(options) {
this.options = options;
this.currentSort = 'default';
}
render() {
return `
<select onchange="sortSelector.change(this.value)">
${Object.entries(this.options).map(([key, label]) => `
<option value="${key}" ${this.currentSort === key ? 'selected' : ''}>
${label}
</option>
`).join('')}
</select>
`;
}
async change(sortKey) {
this.currentSort = sortKey;
return await searchWithSort(this.query, sortKey);
}
}
// 使用
const sortSelector = new SortSelector({
'default': '相关度',
'price_asc': '价格从低到高',
'price_desc': '价格从高到低',
'sales': '销量优先',
'rating': '评分最高'
});
排序组件 #
javascript
class SortWidget {
constructor(container, options) {
this.container = container;
this.options = options;
this.currentSort = 'default';
}
render() {
this.container.innerHTML = `
<div class="sort-widget">
<span>排序方式:</span>
${Object.entries(this.options).map(([key, label]) => `
<button class="sort-btn ${this.currentSort === key ? 'active' : ''}"
data-sort="${key}">
${label}
</button>
`).join('')}
</div>
`;
this.bindEvents();
}
bindEvents() {
this.container.querySelectorAll('.sort-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.currentSort = btn.dataset.sort;
this.render();
this.onSortChange(this.currentSort);
});
});
}
onSortChange(sortKey) {
// 由外部实现
}
}
分页与排序组合 #
搜索状态管理 #
javascript
class SearchState {
constructor() {
this.query = '';
this.page = 0;
this.hitsPerPage = 20;
this.sortBy = 'default';
this.filters = {};
}
setQuery(query) {
this.query = query;
this.page = 0;
}
setPage(page) {
this.page = page;
}
setSort(sortBy) {
this.sortBy = sortBy;
this.page = 0;
}
setFilter(key, value) {
this.filters[key] = value;
this.page = 0;
}
getIndexName() {
if (this.sortBy === 'default') {
return 'products';
}
return `products_${this.sortBy}`;
}
buildSearchParams() {
return {
query: this.query,
page: this.page,
hitsPerPage: this.hitsPerPage
};
}
}
完整搜索函数 #
javascript
async function performSearch(state) {
const index = client.initIndex(state.getIndexName());
const searchParams = {
...state.buildSearchParams(),
facets: ['brand', 'category', 'price_range']
};
const filterString = buildFilterString(state.filters);
if (filterString) {
searchParams.filters = filterString;
}
return await index.search(searchParams);
}
分页优化 #
预加载下一页 #
javascript
async function searchWithPrefetch(state) {
const currentResults = await performSearch(state);
// 预加载下一页
if (state.page < currentResults.nbPages - 1) {
const nextState = { ...state, page: state.page + 1 };
performSearch(nextState).catch(() => {});
}
return currentResults;
}
分页缓存 #
javascript
const pageCache = new Map();
async function cachedSearch(state) {
const cacheKey = JSON.stringify(state);
if (pageCache.has(cacheKey)) {
return pageCache.get(cacheKey);
}
const results = await performSearch(state);
pageCache.set(cacheKey, results);
return results;
}
分页最佳实践 #
1. 合理设置每页数量 #
javascript
// ✅ 推荐
hitsPerPage: 20 // 平衡加载速度和用户体验
// ❌ 避免
hitsPerPage: 1000 // 过大会影响性能
2. 显示总数信息 #
javascript
function renderPaginationInfo(results) {
const start = results.page * results.hitsPerPage + 1;
const end = Math.min((results.page + 1) * results.hitsPerPage, results.nbHits);
return `显示 ${start}-${end} 条,共 ${results.nbHits} 条结果`;
}
3. 限制最大页数 #
javascript
// Algolia限制最多获取1000条结果
// 如果需要更多,考虑使用过滤器缩小范围
const MAX_RESULTS = 1000;
const maxPage = Math.ceil(MAX_RESULTS / hitsPerPage) - 1;
排序最佳实践 #
1. 预定义排序选项 #
javascript
const SORT_OPTIONS = {
default: { label: '相关度', index: 'products' },
price_asc: { label: '价格升序', index: 'products_price_asc' },
price_desc: { label: '价格降序', index: 'products_price_desc' },
rating: { label: '评分最高', index: 'products_rating' },
newest: { label: '最新上架', index: 'products_newest' }
};
2. 保持过滤器状态 #
javascript
// 切换排序时保持过滤条件
async function changeSort(newSort) {
state.setSort(newSort);
const results = await performSearch(state);
renderResults(results);
}
3. 重置分页 #
javascript
// 排序或过滤变化时重置到第一页
function onSortChange(newSort) {
state.setSort(newSort);
state.setPage(0);
performSearch(state);
}
示例:完整搜索页面 #
javascript
class SearchPage {
constructor() {
this.state = new SearchState();
this.init();
}
init() {
this.bindEvents();
this.search();
}
bindEvents() {
// 搜索输入
document.getElementById('searchInput').addEventListener('input',
debounce((e) => {
this.state.setQuery(e.target.value);
this.search();
}, 300)
);
// 排序选择
document.getElementById('sortSelect').addEventListener('change',
(e) => {
this.state.setSort(e.target.value);
this.search();
}
);
// 分页点击
document.getElementById('pagination').addEventListener('click',
(e) => {
if (e.target.dataset.page) {
this.state.setPage(parseInt(e.target.dataset.page));
this.search();
}
}
);
}
async search() {
const results = await performSearch(this.state);
this.render(results);
}
render(results) {
this.renderResults(results.hits);
this.renderPagination(results);
this.renderInfo(results);
}
renderResults(hits) {
// 渲染搜索结果
}
renderPagination(results) {
// 渲染分页组件
}
renderInfo(results) {
// 渲染结果信息
}
}
总结 #
分页与排序要点:
| 要点 | 说明 |
|---|---|
| 分页参数 | page, hitsPerPage, offset, length |
| 分页结果 | hits, nbHits, nbPages |
| 默认排序 | 相关性排序 |
| 自定义排序 | customRanking |
| 副本索引 | replicas实现多维度排序 |
| 最佳实践 | 合理每页数量、保持过滤状态 |
接下来,让我们学习 高亮与代码片段。
最后更新:2026-03-28