@dylanmurzello/vendure-plugin-deep-pagination
v1.0.1
Published
Infinite product pagination for Vendure e-commerce using Elasticsearch search_after cursors. Bypass the 10k offset limit. O(1) performance at any page depth.
Downloads
15
Maintainers
Keywords
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.
