Package Exports
- @dylanmurzello/vendure-plugin-deep-pagination
- @dylanmurzello/vendure-plugin-deep-pagination/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@dylanmurzello/vendure-plugin-deep-pagination) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Vendure Deep Pagination Plugin
Infinite product pagination using Elasticsearch
search_aftercursors. Bypass the 10k limit.
The Problem
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
The Solution
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
Installation
npm install @dylanmurzello/vendure-plugin-deep-paginationQuick Start
1. Register Plugin
// vendure-config.ts
import { DeepPaginationPlugin } from '@gbros/vendure-plugin-deep-pagination';
export const config: VendureConfig = {
plugins: [
// ... other plugins
DeepPaginationPlugin,
],
};2. Query Products
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
}
}3. Navigate Pages
// First page
const page1 = await client.request(GET_PRODUCTS, {});
// Next page
const page2 = await client.request(GET_PRODUCTS, {
cursor: page1.cursorSearch.nextCursor
});API Reference
Input
| 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) |
Sort Options
{
name?: 'ASC' | 'DESC';
price?: 'ASC' | 'DESC';
}Output
| Field | Type | Description |
|---|---|---|
items |
SearchResult[] |
Products matching query |
totalItems |
number |
Total result count |
hasMore |
boolean |
More pages available |
nextCursor |
string? |
Cursor for next page |
How It Works
Cursor Pagination
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]Deterministic Sorting
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.
Why Keyword Fields?
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)
Limitations
Forward-Only Navigation
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]);
};No Total Page Count
You receive totalItems but not total pages. Display pagination as:
const estimatedPages = Math.ceil(totalItems / take);
// Show: "Page 5 of ~1,320"Performance
| Method | Page 1 | Page 100 | Page 1000 |
|---|---|---|---|
| Offset | 50ms | 200ms | 1000ms |
| Cursor | 50ms | 50ms | 50ms |
Cursor pagination maintains constant performance regardless of page depth.
Requirements
- Vendure >= 3.0.0
- Elasticsearch >= 8.0.0
- Node.js >= 18
Contributing
Contributions welcome! Please open an issue or PR.
Development
git clone https://github.com/dylanmurzello/vendure-plugin-deep-pagination.git
cd vendure-plugin-deep-pagination
npm install
npm run buildLicense
MIT - Dylan Murzello
Acknowledgments
Built for production e-commerce at scale. Open-sourced for the Vendure community.
Inspired by Elasticsearch's search_after documentation.