搜索结果分页
在一个理想的世界中,用户无需查看第一个搜索结果之外的内容即可找到他们想要的内容。然而,在实践中,通常需要创建某种分页界面来浏览长长的结果列表。
在本指南中,我们将讨论 Meilisearch 支持的两种不同的分页方法:一种使用 offset
和 limit
,另一种使用 hitsPerPage
和 page
。
选择正确的分页 UI
有许多 UI 模式可以帮助您的用户浏览搜索结果。在 Meilisearch 中,一种常见且高效的解决方案是使用 offset
和 limit
来创建以“上一页”和“下一页”按钮为中心的界面。
其他解决方案,例如创建页面选择器,允许用户跳转到任何搜索结果页面,利用 hitsPerPage
和 page
来获得匹配文档的详尽总数。这些方法效率较低,并可能导致性能下降。
无论您选择哪种 UI 模式,Meilisearch 对于任何给定查询都将返回有限的最大搜索结果数。您可以使用maxTotalHits
索引设置来配置此项,但请注意,更高的限制会对搜索性能产生负面影响。
“上一页”和“下一页”按钮
使用“上一页”和“下一页”按钮进行分页意味着用户可以轻松浏览结果,但无法跳转到任意结果页面。这是 Meilisearch 在创建分页界面时推荐的解决方案。
虽然这种方法提供的精度不如功能齐全的页面选择器,但它不需要知道搜索结果的确切数量。由于计算与查询匹配的文档的详尽数量是一个资源密集型过程,因此像这样的界面可能会提供更好的性能。
实施
为了在网站或应用程序中实现此界面,我们使用 limit
和 offset
搜索参数发出查询。响应正文将包含一个 estimatedTotalHits
字段,其中包含搜索结果的部分计数。这是 Meilisearch 的默认行为
{
"hits": [
…
],
"query": "",
"processingTimeMs": 15,
"limit": 10,
"offset": 0,
"estimatedTotalHits": 471
}
limit
和 offset
“上一页”和“下一页”按钮可以使用limit
和 offset
搜索参数来实现。
limit
设置页面的大小。如果您将 limit
设置为 10
,Meilisearch 的响应将最多包含 10 个搜索结果。offset
跳过一定数量的搜索结果。如果您将 offset
设置为 20
,Meilisearch 的响应将跳过前 20 个搜索结果。
例如,您可以使用 Meilisearch 的 JavaScript SDK 获取电影数据库中的前十部电影
const results = await index.search("tarkovsky", { limit: 10, offset: 0 });
您可以将这两个参数一起使用来创建搜索页面。
搜索页面和计算 offset
如果您将 limit
设置为 20
,将 offset
设置为 0
,您将获得前 20 个搜索结果。我们可以称之为第一页。
const results = await index.search("tarkovsky", { limit: 20, offset: 0 });
同样,如果您将 limit
设置为 20
,将 offset
设置为 40
,您将跳过前 40 个搜索结果,并获得排名从 40 到 59 的文档。我们可以称之为第三个结果页面。
const results = await index.search("tarkovsky", { limit: 20, offset: 40 });
您可以使用此公式来计算页面的 offset 值:offset = limit * (目标页码 - 1)
。在前面的示例中,计算结果如下所示:offset = 20 * (3 - 1)
。这给了我们 40
作为结果:offset = 20 * 2 = 40
。
一旦查询返回的 hits
少于您配置的 limit
,您就已到达最后一个结果页面。
跟踪当前页码
即使此 UI 模式不允许用户跳转到特定页面,但跟踪当前页码仍然很有用。
以下 JavaScript 代码片段将页码存储在 HTML 元素 .pagination
中,并在用户每次移动到不同的搜索结果页面时更新它
function updatePageNumber(elem) {
const directionBtn = elem.id
// Get the page number stored in the pagination element
let pageNumber = parseInt(document.querySelector('.pagination').dataset.pageNumber)
// Update page number
if (directionBtn === 'previous_button') {
pageNumber = pageNumber - 1
} else if (directionBtn === 'next_button') {
pageNumber = pageNumber + 1
}
// Store new page number in the pagination element
document.querySelector('.pagination').dataset.pageNumber = pageNumber
}
// Add data to our HTML element stating the user is on the first page
document.querySelector('.pagination').dataset.pageNumber = 0
// Each time a user clicks on the previous or next buttons, update the page number
document.querySelector('#previous_button').onclick = function () { updatePageNumber(this) }
document.querySelector('#next_button').onclick = function () { updatePageNumber(this) }
禁用第一页和最后一页的导航按钮
当用户无法移动到“下一页”或“上一页”时,禁用导航按钮通常很有帮助。
当您的 offset
为 0
时,应禁用“上一页”按钮,因为这表示您的用户位于第一个结果页面。
要了解何时禁用“下一页”按钮,我们建议将查询的 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 })
// If offset equals 0, we're on the first results page
if (offset === 0 ) {
document.querySelector('#previous_button').disabled = true;
}
// If offset is bigger than 0, we're not on the first results page
if (offset > 0 ) {
document.querySelector('#previous_button').disabled = false;
}
// If Meilisearch returns 20 items or fewer,
// we are on the last page
if (results.hits.length < 21 ) {
document.querySelector('#next_button').disabled = true;
}
// If Meilisearch returns exactly 21 results
// and our page can only show 20 items at a time,
// we have at least one more page with one result in it
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
,而不是 estimatedTotalHits
。totalHits
包含该查询的完整结果数,而 totalPages
包含同一查询的搜索结果页面的完整页数。
{
"hits": [
…
],
"query": "",
"processingTimeMs": 35,
"hitsPerPage": 20,
"page": 1,
"totalPages": 4,
"totalHits": 100
}
使用 hitsPerPage
和 page
搜索页面
hitsPerPage
定义一个页面上显示的最大搜索结果数。
由于 hitsPerPage
定义了页面上的结果数,因此它直接影响查询的总页数。例如,如果一个查询返回 100 个结果,将 hitsPerPage
设置为 25
意味着您将有四个搜索结果页面。相反,将 hitsPerPage
设置为 50
意味着您将只有两个搜索结果页面。
以下示例返回查询的前 25 个搜索结果
const results = await index.search(
"tarkovsky",
{
hitsPerPage: 25,
}
);
要浏览搜索结果页面,请使用 page
搜索参数。如果您将 hitsPerPage
设置为 25
并且您的 totalPages
为 4
,则 page
1
包含文档 1 到 25。将 page
设置为 2
则返回文档 26 到 50。
const results = await index.search(
"tarkovsky",
{
hitsPerPage: 25,
page: 2
}
);
注意
hitsPerPage
和 page
优先于 offset
和 limit
。如果查询包含 hitsPerPage
或 page
中的任何一个,则传递给 offset
和 limit
的任何值都将被忽略。
创建编号页码列表
响应中包含的 totalPages
字段包含基于您的查询的 hitsPerPage
的搜索结果页面的完整计数。使用此字段创建编号页码列表。
为了易于使用,带有 hitsPerPage
和 page
的查询始终返回当前页码。这意味着您无需手动跟踪您当前显示的页面。
在以下示例中,我们动态创建一个页面按钮列表并突出显示当前页面
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;