搜尋結果分頁

    在理想的情況下,使用者不需要查看第一個搜尋結果以外的內容就可以找到他們要找的東西。然而,在實際應用中,通常需要建立某種分頁介面來瀏覽長長的結果列表。

    在本指南中,我們將討論 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 值: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 設定為您希望每頁顯示的結果數量加一。多出來的這一個 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;