Skip to content

/api/search pagination.total reports the page window, not the true match count #13

Description

@dubadub

Problem

SearchIndex::search (src/indexer/search.rs) computes the total match count from the already-truncated result set:

let top_docs = searcher
    .search(&*tantivy_query, &TopDocs::with_limit(limit + offset))?;

// Get total count
let total = top_docs.len();

TopDocs::with_limit(limit + offset) caps the collector at the current page window, so top_docs.len() saturates at limit + offset rather than counting all matching documents.

Effect

  • total and total_pages are wrong for any query matching more documents than one page holds.
  • It grows as you paginate deeper, which reads as a moving target: ?page=5&limit=100 reports total: 500 regardless of how many recipes actually match.
  • total_pages (total.div_ceil(limit)) is derived from it, so pagination controls in the web UI are wrong too.

Reproduce:

curl -s 'http://localhost:3000/api/search?q=&limit=100&page=1' | jq .pagination   # total: 100
curl -s 'http://localhost:3000/api/search?q=&limit=100&page=5' | jq .pagination   # total: 500

Suggested fix

Use Tantivy's Count collector for the true total, alongside TopDocs for the page:

use tantivy::collector::{Count, TopDocs};

let (top_docs, total) = searcher.search(
    &*tantivy_query,
    &(TopDocs::with_limit(limit + offset), Count),
)?;

Count scores nothing and just counts matches, so the extra cost is small.

Context

Found during end-to-end verification of recipe locale detection (#11). Pre-existing and unrelated to locale — left out of scope there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions