Advanced 26 min readModule: Module 13: Succinct & Compressed Data Structures: Roaring Bitmaps
Succinct Data Structures: Roaring Bitmaps & Rank/Select
Represent massive datasets in compressed memory with Succinct Data Structures: Roaring Bitmaps (Array Containers, Bitmap Containers, Run-Length Encoded RLE Containers), Rank and Select operations on bit vectors, and Wavelet Trees.
What You Will Learn in This Lesson
- The concept of Succinct Data Structures: operating directly on compressed data without decompressing
- The 3 internal containers of Roaring Bitmaps: Array (sparse), Bitset (dense), and Run (contiguous ranges)
- How Lucene, Elasticsearch, Spark, and Redis use Roaring Bitmaps for high-speed set intersections (AND/OR)
- Rank and Select primitive queries in constant O(1) time
Introduction & Core Concept
Storing sets containing millions of integers (such as user IDs, document IDs in search engines, or analytics tags) requires hundreds of megabytes of RAM if using standard HashSets. A standard bitset consumes fixed memory regardless of density. Roaring Bitmaps partition 32-bit integers by their top 16 bits and dynamically choose between sparse 16-bit integer arrays, uncompressed bitsets, or Run-Length Encoding (RLE) to achieve up to 100x compression with sub-microsecond set intersection speeds.
WHY DOES THIS MATTER IN THE REAL WORLD?
Search engines (Elasticsearch, Apache Lucene) and distributed analytical databases (ClickHouse, Apache Spark, Druid) use Roaring Bitmaps to intersect billion-record filter queries in milliseconds.
Syntax & Structure
javascript
// Roaring Bitmap Container Selectionif (count < 4096) ArrayContainer(uint16[])else BitsetContainer(uint64[1024])Simulating Roaring Bitmap Dynamic Container Selection and Set Intersection
javascriptjavascript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758// Roaring Bitmap Dynamic Container Architecture Simulationclass SimpleRoaringBitmap {constructor() {// Map high 16-bit chunk -> Containerthis.chunks = new Map();}add(val) {const chunkKey = (val >>> 16) & 0xFFFF;const lowVal = val & 0xFFFF;if (!this.chunks.has(chunkKey)) {// Start with sparse Array Containerthis.chunks.set(chunkKey, { type: 'ARRAY', data: [] });}const container = this.chunks.get(chunkKey);if (container.type === 'ARRAY') {if (!container.data.includes(lowVal)) {container.data.push(lowVal);container.data.sort((a, b) => a - b);}// If cardinality exceeds 4,096 elements, convert to Bitset Container!if (container.data.length > 4096) {const bitset = new Uint32Array(2048); // 65,536 bitscontainer.data.forEach(v => bitset[v >>> 5] |= (1 << (v & 31)));container.type = 'BITSET';container.data = bitset;}} else if (container.type === 'BITSET') {container.data[lowVal >>> 5] |= (1 << (lowVal & 31));}}has(val) {const chunkKey = (val >>> 16) & 0xFFFF;const lowVal = val & 0xFFFF;const container = this.chunks.get(chunkKey);if (!container) return false;if (container.type === 'ARRAY') {return container.data.includes(lowVal);} else if (container.type === 'BITSET') {return (container.data[lowVal >>> 5] & (1 << (lowVal & 31))) !== 0;}return false;}}const rb = new SimpleRoaringBitmap();rb.add(105);rb.add(65536 + 10); // Spans into chunk #1console.log("=== Roaring Bitmap Hybrid Container Engine ===");console.log("Contains 105:", rb.has(105));console.log("Contains 65546:", rb.has(65536 + 10));console.log("Contains 999:", rb.has(999));console.log("✅ Adaptive memory compression active across chunk boundaries!");
Line-by-Line Technical Breakdown
1Rank and Select Queries: Rank(i) returns the number of 1-bits before index i in O(1) time using pre-computed block sum lookup tables. Select(j) returns the index of the j-th 1-bit. These two operations form the foundation of compressed full-text search indexes (FM-Index).
Try It Yourself (Interactive Editor)
Modify the code in real-time and click Run to test live browser output and console logs.
Intelligent Code Runner & Live Sandbox[JAVASCRIPT]
JAVASCRIPT SOURCE EDITOR
Interactive Live CodeCommon Mistakes & How to Avoid Them
#1: Using standard uncompressed `HashSet<Long>` in Java/C# for massive user tracking, exhausting gigabytes of heap RAM.
Standard HashSets box primitives into heap objects with 32 bytes of object header overhead. Roaring Bitmaps compress integers down to fractions of a byte.
Incorrect / Antipattern
Set<Long> userIds = new HashSet<>(); // 32 bytes of overhead per number!Correct / Professional Solution
RoaringBitmap userIds = new RoaringBitmap(); // ~0.5 to 2 bits per numberIndustry Best Practices & Professional Standards
- Use Roaring Bitmaps for high-cardinality search filtering and tag intersection.
- Use SIMD-accelerated bitwise instructions (AVX2/AVX-512) for parallel bitmap AND/OR intersections.
- Store inverted index posting lists as Roaring Bitmaps for instant search filtering.
Lesson Summary & Core Takeaways
- Roaring Bitmaps adaptively switch between Array, Bitset, and RLE containers.
- Enables sub-microsecond set intersection with up to 100x memory compression.
- Widely adopted by Elasticsearch, Lucene, Spark, and high-performance databases.