Infinite product pagination using Elasticsearch
search_aftercursors. Bypass the 10k limit.
Elasticsearch limits offset-based pagination to 10,000 documents. For e-commerce stores with large catalogs:
- Users cannot browse beyond page 834 (12 products/page)
- Performance degrades linearly with page depth
- SEO suffers from incomplete product indexing
This plugin uses Elasticsearch's search_after API for cursor-based pagination:
- No limits - Navigate through millions of products
- O(1) performance - Constant speed at any page depth
- Drop-in replacement - Extends Vendure's GraphQL API
npm install @dylanmurzello/vendure-plugin-deep-pagination// vendure-config.ts
import { DeepPaginationPlugin } from '@gbros/vendure-plugin-deep-pagination';
export const config: VendureConfig = {
plugins: [
// ... other plugins
DeepPaginationPlugin,
],
};query GetProducts($cursor: String) {
cursorSearch(input: { take: 12, cursor: $cursor }) {
items {
productId
productName
slug
priceWithTax {
... on SinglePrice { value }
... on PriceRange { min max }
}
}
totalItems
hasMore
nextCursor
}
}// First page
const page1 = await client.request(GET_PRODUCTS, {});
// Next page
const page2 = await client.request(GET_PRODUCTS, {
cursor: page1.cursorSearch.nextCursor
});| Field | Type | Description |
|---|---|---|
term |
string? |
Full-text search query |
facetValueIds |
string[]? |
Filter by facet values |
facetValueOperator |
'AND' | 'OR'? |
Facet filter logic (default: OR) |
collectionId |
string? |
Filter by collection ID |
collectionSlug |
string? |
Filter by collection slug |
groupByProduct |
boolean? |
Group variants by product |
take |
number? |
Results per page (default: 100, max: 1000) |
cursor |
string? |
Opaque pagination cursor |
sort |
object? |
Sort options (see below) |
{
name?: 'ASC' | 'DESC';
price?: 'ASC' | 'DESC';
}| Field | Type | Description |
|---|---|---|
items |
SearchResult[] |
Products matching query |
totalItems |
number |
Total result count |
hasMore |
boolean |
More pages available |
nextCursor |
string? |
Cursor for next page |
Traditional offset pagination (skip + take) becomes slow at high offsets because Elasticsearch must scan and discard all previous results.
Cursor pagination uses search_after to resume from the last result's sort values:
Page 1: [A, B, C] -> cursor: "C's sort values"
Page 2: search_after "C's values" -> [D, E, F]
search_after requires stable sort order. We use:
- User-specified field (name, price, etc.)
productId(keyword field)productVariantId(keyword field)
This ensures consistent ordering even when products share the same name/price.
Elasticsearch 9.x disables fielddata by default. We use keyword fields for sorting because:
- Keyword fields use
doc_values(disk-based, efficient) - Text fields require
fielddata(memory-intensive, disabled)
Cursor pagination is forward-only. You can:
- Go to next page (use
nextCursor) - Go to first page (omit
cursor) - Jump to arbitrary pages (not supported)
Solution: Maintain a cursor stack in your frontend:
const [cursors, setCursors] = useState<string[]>([]);
// Forward
const goNext = () => {
setCursors([...cursors, nextCursor]);
fetchPage(nextCursor);
};
// Back
const goPrev = () => {
const newCursors = cursors.slice(0, -1);
setCursors(newCursors);
fetchPage(newCursors[newCursors.length - 1]);
};You receive totalItems but not total pages. Display pagination as:
const estimatedPages = Math.ceil(totalItems / take);
// Show: "Page 5 of ~1,320"| Method | Page 1 | Page 100 | Page 1000 |
|---|---|---|---|
| Offset | 50ms | 200ms | 1000ms |
| Cursor | 50ms | 50ms | 50ms |
Cursor pagination maintains constant performance regardless of page depth.
- Vendure >= 3.0.0
- Elasticsearch >= 8.0.0
- Node.js >= 18
Contributions welcome! Please open an issue or PR.
git clone https://github.com/dylanmurzello/vendure-plugin-deep-pagination.git
cd vendure-plugin-deep-pagination
npm install
npm run buildMIT - Dylan Murzello
Built for production e-commerce at scale. Open-sourced for the Vendure community.
Inspired by Elasticsearch's search_after documentation.