在理想情况下,用户无需浏览第一条搜索结果之外的内容就能找到所需信息。然而在实际应用中,通常需要创建某种分页界面来浏览长列表的搜索结果。

本指南将讨论 Meilisearch 支持的两种分页方式:一种使用 offsetlimit 参数,另一种使用 hitsPerPagepage 参数。

选择合适的分页界面

有多种用户界面模式可以帮助用户浏览搜索结果。在 Meilisearch 中,一个常见且高效的解决方案是使用 offsetlimit 来创建基于”上一页”和”下一页”按钮的界面。

其他解决方案(例如创建页码选择器)允许用户跳转到任意结果页面,这类方案使用 hitsPerPagepage 来获取匹配文档的详尽总数。这些方案效率较低,可能会导致性能下降。

无论选择哪种界面模式,Meilisearch 对任何查询返回的搜索结果数量都存在上限。您可以通过maxTotalHits索引设置来配置此限制,但请注意更高的限制会对搜索性能产生负面影响。

“上一页”和”下一页”按钮

使用”上一页”和”下一页”按钮进行分页意味着用户可以轻松浏览结果,但无法跳转到任意结果页面。这是 Meilisearch 在创建分页界面时推荐的解决方案。

虽然这种方法不如完整的页码选择器精确,但它不需要知道搜索结果的确切数量。由于计算查询匹配文档的详尽数量是一个资源密集型过程,这种界面可能会提供更好的性能。

实现方案

要在网站或应用中实现该接口,我们使用 limitoffset 搜索参数发起查询。响应体将包含 estimatedTotalHits 字段,显示搜索结果的部分计数。这是 Meilisearch 的默认行为:

{
  "hits": [

  ],
  "query": "",
  "processingTimeMs": 15,
  "limit": 10,
  "offset": 0,
  "estimatedTotalHits": 471
}

limitoffset 参数

“上一页”和”下一页”按钮可以通过 limitoffset 搜索参数实现。

limit 设置页面大小。如果将 limit 设为 10,Meilisearch 的响应最多包含 10 条搜索结果。offset 跳过指定数量的搜索结果。如果将 offset 设为 20,Meilisearch 的响应会跳过前 20 条搜索结果。

例如,可以使用 Meilisearch 的 JavaScript SDK 获取电影数据库中的前 10 部电影:

const results = await index.search("tarkovsky", { limit: 10, offset: 0 });

通过组合使用这两个参数,可以创建分页搜索功能。

搜索分页与计算 offset

如果将 limit 设为 20offset 设为 0,你将获得前20条搜索结果。我们可以将其称为第一页。

const results = await index.search("tarkovsky", { limit: 20, offset: 0 });

同理,若设置 limit20offset40,则会跳过前40条结果,获取排名40到59的文档。这可以称为第三页结果。

const results = await index.search("tarkovsky", { limit: 20, offset: 40 });

你可以使用以下公式计算目标页面的 offset 值:offset = limit * (目标页码 - 1)。在前例中,计算过程为:offset = 20 * (3 - 1)。结果为 40offset = 20 * 2 = 40

当查询返回的 hits 数量少于你配置的 limit 值时,即表示已到达最后一页结果。

跟踪当前页码

尽管这种用户界面模式不允许用户跳转到特定页面,但跟踪当前页码仍然很有用。

以下 JavaScript 代码片段将页码存储在 HTML 元素 .pagination 中,并在用户切换到不同搜索结果页面时更新它:

function updatePageNumber(elem) {
  const directionBtn = elem.id
  // 从分页元素中获取存储的页码
  let pageNumber = parseInt(document.querySelector('.pagination').dataset.pageNumber)

  // 更新页码
  if (directionBtn === 'previous_button') {
    pageNumber = pageNumber - 1
  } else if (directionBtn === 'next_button') {
    pageNumber = pageNumber + 1
  }

  // 将新页码存储到分页元素中
  document.querySelector('.pagination').dataset.pageNumber = pageNumber
}

// 在HTML元素中添加数据,表示用户位于第一页
document.querySelector('.pagination').dataset.pageNumber = 0
// 每次用户点击上一页或下一页按钮时,更新页码
document.querySelector('#previous_button').onclick = function () { updatePageNumber(this) }
document.querySelector('#next_button').onclick = function () { updatePageNumber(this) }

禁用首页和末页的导航按钮

当用户无法跳转到”下一页”或”上一页”时,禁用导航按钮通常很有帮助。

offset0 时(表示用户位于第一页结果),应禁用”上一页”按钮。

要判断何时禁用”下一页”按钮,我们建议将查询的 limit 设置为每页希望显示的结果数加一。这个额外的 hit 不应展示给用户,它的作用是表明下一页至少还有一个文档可显示。

以下 JavaScript 代码片段会在用户每次导航到其他搜索结果页面时,检查是否应该禁用按钮:

function updatePageNumber() {
  const pageNumber = parseInt(document.querySelector('.pagination').dataset.pageNumber)

  const offset = pageNumber * 20
  const results = await index.search('x', { limit: 21, offset })

  // 如果 offset 等于 0,说明我们在第一页
  if (offset === 0 ) {
    document.querySelector('#previous_button').disabled = true;
  }

  // 如果 offset 大于 0,说明不在第一页
  if (offset > 0 ) {
    document.querySelector('#previous_button').disabled = false;
  }

  // 如果 Meilisearch 返回 20 条或更少结果
  // 说明当前是最后一页
  if (results.hits.length < 21 ) {
    document.querySelector('#next_button').disabled = true;
  }

  // 如果 Meilisearch 返回正好 21 条结果
  // 且每页只能显示 20 条
  // 说明至少还有一页包含一条结果
  if (results.hits.length === 21 ) {
    document.querySelector('#next_button').disabled = false;
  }
}

document.querySelector('#previous_button').onclick = function () { updatePageNumber(this) }
document.querySelector('#next_button').onclick = function () { updatePageNumber(this) }

数字页码选择器

这种分页方式由一组带编号的页码列表和”下一页”/“上一页”按钮组成。这是一种常见的UI模式,能为用户在浏览搜索结果时提供较高的导航精度。

计算查询结果的总数是一个资源密集型操作。数字页码选择器可能导致性能问题,特别是当你将 maxTotalHits 提高到默认值以上时。

实现方式

默认情况下,Meilisearch查询仅返回 estimatedTotalHits(预估命中数)。这个值会随着用户浏览搜索结果而变化,因此不应用于计算搜索结果页数。

当查询中包含 hitsPerPage(每页结果数)、page(页码)或同时包含这两个搜索参数时,Meilisearch会返回 totalHits(总命中数)和 totalPages(总页数)而非 estimatedTotalHitstotalHits 包含该查询的精确结果总数,totalPages 则包含相同查询的精确分页总数:

{
  "hits": [

  ],
  "query": "",
  "processingTimeMs": 35,
  "hitsPerPage": 20,
  "page": 1,
  "totalPages": 4,
  "totalHits": 100
}

使用 hitsPerPagepage 分页搜索

hitsPerPage 定义了每页显示的搜索结果最大数量。

由于 hitsPerPage 决定了每页的结果数,它会直接影响查询结果的总页数。例如,如果一个查询返回 100 条结果,将 hitsPerPage 设为 25 意味着你将获得 4 页搜索结果。而将 hitsPerPage 设为 50,则意味着你只会得到 2 页搜索结果。

以下示例返回查询的前 25 条搜索结果:

const results = await index.search(
  "tarkovsky",
  {
    hitsPerPage: 25,
  }
);

要浏览搜索结果的不同页面,可以使用 page 搜索参数。如果你将 hitsPerPage 设为 25totalPages4,那么 page 1 包含第 1 到 25 条文档。将 page 设为 2 则会返回第 26 到 50 条文档:

const results = await index.search(
  "tarkovsky",
  {
    hitsPerPage: 25,
    page: 2
  }
);

hitsPerPagepage 的优先级高于 offsetlimit。如果查询中包含 hitsPerPagepage,任何传递给 offsetlimit 的值都会被忽略。

创建带页码的分页列表

响应中包含的 totalPages 字段表示基于查询参数 hitsPerPage 计算出的搜索结果总页数。您可以使用这个值来创建一个带编号的页码列表。

为方便使用,带有 hitsPerPagepage 参数的查询总是会返回当前页码。这意味着您无需手动跟踪当前显示的页面。

以下示例展示了如何动态生成页码按钮列表并高亮当前页:

const pageNavigation = document.querySelector('#page-navigation');
const listContainer = pageNavigation.querySelector('#page-list');
const results = await index.search(
  "tarkovsky",
  {
    hitsPerPage: 25,
    page: 1
  }
);

const totalPages = results.totalPages;
const currentPage = results.page;

for (let i = 0; i < totalPages; i += 1) {
  const listItem = document.createElement('li');
  const pageButton = document.createElement('button');

  pageButton.innerHTML = i;

  if (currentPage === i) {
    listItem.classList.add("current-page");
  }

  listItem.append(pageButton);
  listContainer.append(listItem);
}

添加导航按钮

用户通常会对当前搜索结果页的前后页面更感兴趣。因此,在页面列表中添加”下一页”和”上一页”按钮往往很有帮助。

在这个示例中,我们将这些按钮作为页面导航组件的首尾元素添加:

const pageNavigation = document.querySelector('#page-navigation');

const buttonNext = document.createElement('button');
buttonNext.innerHTML = 'Next';

const buttonPrevious = document.createElement('button');
buttonPrevious.innerHTML = 'Previous';

pageNavigation.prepend(buttonPrevious);
pageNavigation.append(buttonNext);

我们还可以在到达搜索结果的第一页或最后一页时按需禁用这些按钮:

buttonNext.disabled = results.page === results.totalPages;
buttonPrevious.disabled = results.page === 1;