搜索结果分页

    在一个理想的世界里,用户无需查看第一个搜索结果以外的内容就能找到他们想要的内容。然而,在实践中,通常需要创建某种分页界面来浏览较长的结果列表。

    在本指南中,我们将讨论 Meilisearch 支持的两种不同的分页方法:一种使用 offsetlimit,另一种使用 hitsPerPagepage

    选择合适的分页 UI

    有很多 UI 模式可以帮助用户浏览搜索结果。Meilisearch 中一个常见且有效的解决方案是使用 offsetlimit 创建以 “上一页”和“下一页”按钮 为中心的界面。

    其他解决方案,例如 创建页面选择器 以允许用户跳转到任何搜索结果页面,则利用 hitsPerPagepage 获取匹配文档的完整总数。这些方法往往效率较低,并可能导致性能下降。

    无论您选择哪种 UI 模式,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 获取电影数据库中的前十部电影。

    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 });
    

    同样,如果您将 limit 设置为 20offset 设置为 40,则会跳过前 40 个搜索结果,并获取排名从 40 到 59 的文档。我们可以将其称为第三个结果页面。

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

    您可以使用此公式计算页面的偏移值: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) }
    

    禁用第一页和最后一页的导航按钮

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

    只要您的 offset0,就应禁用“上一页”按钮,因为这表示您的用户位于第一个结果页面。

    要了解何时禁用“下一页”按钮,我们建议将查询的 limit 设置为您希望每页显示的结果数加 1。该额外的 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。此值可能会随着用户导航搜索结果而变化,不应用于计算搜索结果页数。

    当您的查询包含hitsPerPagepage或这两个搜索参数时,Meilisearch 会返回 totalHitstotalPages 而不是 estimatedTotalHitstotalHits 包含该查询的完整结果数,totalPages 包含同一查询的完整搜索结果页数。

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

    使用 hitsPerPagepage 进行搜索分页

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

    由于 hitsPerPage 定义了每页的结果数量,因此它会直接影响查询的总页数。例如,如果一个查询返回 100 个结果,将 hitsPerPage 设置为 25 意味着您将有四页搜索结果。而将 hitsPerPage 设置为 50 则意味着您将只有两页搜索结果。

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

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

    要浏览搜索结果页,请使用 page 搜索参数。如果您将 hitsPerPage 设置为 25 且您的 totalPages4,则 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;