分页与排序 #

分页机制 #

基本分页 #

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