From 10 Minutes to 1 Second: Three‑Stage Elasticsearch Deep‑Pagination Jump Optimization
This article details how a photo‑contest backend migrated from MySQL to Elasticsearch and, through three iterative optimizations—segment pre‑warming, recent‑anchor positioning with Redis ZSet, and a large‑region‑plus‑small‑page cache—reduced arbitrary deep‑page response time on 500 k records from ten minutes to under one second.
Background
The Wukong activity system required reviewers to browse, filter, and evaluate hundreds of thousands of photo submissions. The original MySQL multi‑table joins became a performance bottleneck as data volume and concurrency grew, leading to high latency and database pressure.
To mitigate this, the query logic was migrated to Elasticsearch, which excels at large‑scale, multi‑dimensional search. Elasticsearch, however, only provides sequential pagination via the scroll API or search_after, and does not support random‑page jumps, degrading the user experience for deep pagination.
Basic Solution Choice
Elasticsearch offers three deep‑query methods: from+size, scroll API, and search_after. The official recommendation for deep pagination is search_after because it avoids large offset calculations and does not maintain server‑side state, unlike scroll which is intended for bulk data extraction.
Jump‑Page Solution Evolution
Stage 1 – Basic Scheme
Implemented a cache‑augmented search_after approach:
Asynchronous pre‑warming: When a user first opens the list, a background thread pre‑loads results in steps of 1,000 records.
Multi‑granularity cache: For each step, the final document’s sortValues are stored for page sizes 10, 20, 50, 100.
Redis storage: The position‑to‑ sortValues mapping is kept in a Redis hash.
If a request exceeds Elasticsearch’s default depth limit (10,000), the system computes the start offset, jumps to the nearest cached anchor, and proceeds with sequential search_after until the target page is reached, then returns the page. Cache hits allow the whole operation to finish within one second.
Observed limitations:
First access to a deep page still requires many sequential search_after calls because the cache is not yet built.
Random page access yields low cache‑hit rates.
For very deep jumps (e.g., the last page of 800 k records) the initial latency can be around ten minutes.
Stage 2 – Performance Optimizations
2.1 Introduce Recent‑Anchor Positioning
A Redis ZSET indexes cached anchors by their offset score. When a user requests a page, the system finds the nearest lower anchor via ZREVRANGEBYSCORE, computes the remaining distance, and performs a single additional search_after to land on the target page.
Dynamic anchor pre‑warming: Initial step size of 1,000 records.
Cache structure: Offset stored as the score, sortValues stored as the value in the ZSET.
2.2 Reduce Elasticsearch Query Overhead
During pre‑warming two settings are disabled: "_source": false – skips fetching the full document source. track_total_hits disabled – avoids a full scan to count total hits.
Benchmarks show query latency dropping from ~500 ms (default) to roughly 500 ms per request after these tweaks, with overall pre‑warm time improving dramatically.
2.3 Split Pre‑Warm Intervals
Anchor granularity (5,000 records) did not match the smallest page size (10), leaving a residual ~1 s delay. A two‑tier strategy was introduced:
Maintain large‑region pre‑warming of 5,000‑record blocks.
Asynchronously subdivide each large block into 10‑record micro‑pages and cache their search_after values.
During a jump request, retrieve the fine‑grained anchor directly, eliminating sequential scans.
This “large‑region + small‑page” architecture reduced random‑page latency to under one second in tests with 700 k records.
Current Issues
Cache‑based anchors become stale when data is inserted or deleted, leading to empty pages or duplicate/missing entries. The root cause is the lack of strong consistency between Elasticsearch indexes and Redis‑cached anchors, which are generated at a specific point in time.
Mitigation strategies observed:
Prefer the approach for relatively stable data sets.
On empty‑page detection, clear the stale cache and re‑pre‑warm from the latest Elasticsearch state.
For highly volatile data, consider alternative schemes such as timestamp‑ or version‑based incremental updates.
Reflection
The optimization journey demonstrates a systematic engineering practice: identify bottlenecks, propose incremental solutions, evaluate trade‑offs, and iterate toward the best compromise. The final design combines Elasticsearch’s stateless search_after with Redis‑backed multi‑granularity anchors to achieve sub‑second deep‑page responses on half‑million‑record workloads.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
