opensearch-roaring-aggs

An OpenSearch plugin that provides opt-in Roaring Bitmap indexing for dramatically faster aggregations on high-cardinality multi-valued keyword fields.
Based on the proposal in apache/lucene#16477.
Table of Contents
Problem
OpenSearch relies on SortedSetDocValues for aggregating multi-valued keyword fields. For each query match, the aggregation engine:
- Performs a random-access lookup into the DocValues column
- Retrieves the segment ordinals for the document
- Increments the corresponding bucket counts
For high-cardinality datasets (100K+ unique values) with high match densities (millions of docs), this creates O(MatchingDocs) non-contiguous memory lookups, causing:
- โ Severe L3 cache misses and memory latency
- โ Poor SIMD vectorization due to variable ordinals per document
- โ GC pressure from object allocation during collection
Solution
This plugin transposes the field representation into per-ordinal Roaring Bitmaps at index time:
Traditional: docโ โ [ordโ, ordโ] docโ โ [ordโ] docโ โ [ordโ, ordโ]
Roaring: ordโ โ {docโ, docโ} ordโ โ {docโ, docโ} ordโ โ {docโ}
At query time, aggregation is performed using bitwise AND + POPCNT operations:
Count(ordinal_k) = POPCNT(QueryBitset AND OrdinalBitmap_k)
This replaces millions of random-access lookups with sequential, cache-friendly, hardware-accelerated bitmap intersections.
Below are empirical, uncached benchmark results measured on 1,000,000 real Wikipedia articles comparing standard OpenSearch DocValues (wiki_default) against Roaring DocValues & Aggregator (wiki_roaring).
๐ฅ๏ธ Hardware & Environment Setup
| Component |
Specification |
| CPU |
Intelยฎ Coreโข i5-6200U CPU @ 2.30GHz (2 Cores / 4 Threads) |
| RAM |
16 GB DDR4 |
| OS / Kernel |
Linux 7.0.0-28-generic (Ubuntu x86_64) |
| OpenSearch Version |
OpenSearch 2.19.0 (Lucene 9.12.1) |
| JVM |
Bundled OpenJDK 21.0.6 (1 GB Heap) |
| Dataset |
1,000,000 English Wikipedia Articles (enwiki-20120502) |
| Query Cache |
Bypassed explicitly on all queries (?request_cache=false) |
๐พ Index Disk Storage Comparison
| Index |
Codec |
Store Size (MB) |
Store Size (Bytes) |
Storage Difference |
wiki_default |
Standard Lucene90 DocValues |
379.29 MB |
397,714,096 bytes |
Baseline |
wiki_roaring |
Opt-in RoaringCodec |
557.00 MB |
584,056,095 bytes |
+46.9% (stores explicit Bitmaps) |
๐ก Storage Trade-off: Roaring Bitmaps pre-index compressed bitset containers (.rvd/.rvm files) for every term ordinal. Trading ~47% additional disk space unlocks up to 439x faster aggregation speed.
โก Uncached Aggregation Latency & Speedup Benchmarks
| # |
Scenario |
Field Type |
Size |
Standard (terms) |
Roaring (roaring_terms) |
Speedup |
| 1 |
Low Cardinality |
Single-valued (label) |
10 |
490.77 ms |
40.56 ms |
12.10x |
| 2 |
Multi-Valued Body |
Multi-valued (words) |
10 |
6,818.71 ms |
15.51 ms |
439.52x |
| 3 |
Multi-Valued Body |
Multi-valued (words) |
100 |
7,647.21 ms |
20.08 ms |
380.76x |
| 4 |
Multi-Valued Body |
Multi-valued (words) |
500 |
7,587.67 ms |
42.11 ms |
180.17x |
| 5 |
Document Titles |
High-cardinality (doctitle) |
10 |
2,661.20 ms |
37.70 ms |
70.60x |
| 6 |
Document Titles |
High-cardinality (doctitle) |
100 |
2,826.59 ms |
17.85 ms |
158.38x |
| 7 |
Filtered Query |
label=LABEL_1 on words |
10 |
12.97 ms |
4.68 ms |
2.77x |
๐ Bulk Indexing Throughput & Latency
Measured on 50,000 Wikipedia documents in 2,000-document bulk batches (isolated standalone runs):
| Metric |
Standard Codec (Lucene90) |
Roaring Codec (RoaringCodec) |
Performance Difference |
| Total Indexing Time |
4.19 s |
5.49 s |
+31% CPU time during segment flush |
| Indexing Throughput |
11,934 docs/sec |
9,109 docs/sec |
-2,825 docs/sec |
| Avg Bulk Latency (2,000 batch) |
167.6 ms |
219.6 ms |
+52 ms per batch |
๐ก Indexing Architectural Trade-off:
- DocValues Replacement:
RoaringCodec does not maintain duplicate DocValues files. For SORTED_SET (keyword) fields, it completely replaces Luceneโs .dvd/.dvm files with .rvd/.rvm Roaring Bitmap files.
- Flush-Time Transposition Overhead: Standard Lucene DocValues simply appends forward document-to-ordinal arrays
(docID โ ordinals[]) to disk during flush. RoaringCodec transposes these tuples into ordinal bitmaps (ordinal โ RoaringBitmap(docIDs)) during segment flush.
- The Payoff: Paying a small ~31% indexing time overhead at segment flush time unlocks a 12x to 439x speedup on aggregations at search time!
๐ Key Takeaways
- Unfiltered Multi-Valued Aggregations (439x Speedup): Standard Lucene DocValues takes 7.3 seconds to iterate doc-by-doc over 1M documents, while
roaring_terms finishes in 13 milliseconds using hardware POPCNT vectorization.
- Agg Size Scaling (
size=10 vs 100 vs 500): roaring_terms stays sub-45ms even when collecting top 500 buckets.
- Query Filtering (
QueryBitset AND OrdinalBitmap): Filtered aggregations run in 4.68 ms (vs 12.97 ms standard).
- Indexing Trade-off (+31% Flush Time): Bitset transposition during segment flush adds a ~31% CPU index time cost to grant sub-15ms aggregation responses.
Installation
From GitHub Releases (Recommended)
bin/opensearch-plugin install https://github.com/rajat315315/opensearch-roaring-aggs/releases/download/v2.19.0.0/opensearch-roaring-aggs-2.19.0.0.zip
From Local File
bin/opensearch-plugin install file:///path/to/opensearch-roaring-aggs-2.19.0.0.zip
From Build Output
# Build the plugin first (see "Building from Source" below)
bin/opensearch-plugin install file:///path/to/opensearch-roaring-aggs/build/distributions/opensearch-roaring-aggs-2.19.0.0.zip
Security Permissions
During installation, you will be prompted to grant security permissions. The plugin requires:
- File I/O permissions: For reading/writing the Roaring Bitmap index files (
.rvd, .rvm)
- Reflection permissions: For RoaringBitmap library off-heap buffer operations
Type y to accept.
Verify Installation
bin/opensearch-plugin list
You should see opensearch-roaring-aggs in the output.
Restart OpenSearch
# Restart the node to load the plugin
sudo systemctl restart opensearch
# or
bin/opensearch
Quick Start
1. Create an Index with the Roaring Codec
PUT /products
{
"settings": {
"index.codec": "RoaringCodec",
"number_of_shards": 1,
"number_of_replicas": 0
},
"mappings": {
"properties": {
"name": { "type": "text" },
"tags": { "type": "keyword" },
"categories": { "type": "keyword" }
}
}
}
2. Index Some Documents
POST /products/_bulk
{"index": {}}
{"name": "Laptop", "tags": ["electronics", "computers", "portable"], "categories": ["tech", "gadgets"]}
{"index": {}}
{"name": "Phone", "tags": ["electronics", "mobile", "portable"], "categories": ["tech", "communication"]}
{"index": {}}
{"name": "Desk", "tags": ["furniture", "office"], "categories": ["home", "work"]}
{"index": {}}
{"name": "Chair", "tags": ["furniture", "office", "ergonomic"], "categories": ["home", "work"]}
{"index": {}}
{"name": "Headphones", "tags": ["electronics", "audio", "portable"], "categories": ["tech", "entertainment"]}
3. Run a Roaring Terms Aggregation
POST /products/_search
{
"size": 0,
"aggs": {
"top_tags": {
"roaring_terms": {
"field": "tags",
"size": 5
}
}
}
}
Expected Response
{
"aggregations": {
"top_tags": {
"buckets": [
{ "key": "electronics", "doc_count": 3 },
{ "key": "portable", "doc_count": 3 },
{ "key": "furniture", "doc_count": 2 },
{ "key": "office", "doc_count": 2 },
{ "key": "audio", "doc_count": 1 }
]
}
}
}
4. Use with Filtered Queries
The real power shows with filtered queries โ block-level skipping bypasses empty 64K doc chunks entirely:
POST /products/_search
{
"size": 0,
"query": {
"term": { "categories": "tech" }
},
"aggs": {
"tech_tags": {
"roaring_terms": {
"field": "tags",
"size": 10
}
}
}
}
Configuration Reference
Index Settings
| Setting |
Default |
Description |
index.codec |
default |
Set to RoaringCodec to enable Roaring Bitmap DocValues for all keyword fields in the index |
Aggregation Parameters
| Parameter |
Required |
Default |
Description |
field |
โ
|
โ |
The keyword field to aggregate on. Must be in an index using RoaringCodec. |
size |
โ |
10 |
Number of top buckets to return, ordered by doc_count descending. |
How It Works
Index Time
- Documents are indexed normally via the standard OpenSearch APIs
- The
RoaringDocValuesConsumer intercepts SortedSetDocValues writes
- Instead of storing
docID โ ordinals[], it transposes into ordinal โ RoaringBitmap(docIDs)
- Each ordinalโs bitmap is run-length optimized and serialized to disk:
.rvm (metadata): field info, ordinal count, offset table, term dictionary
.rvd (data): concatenated serialized Roaring Bitmaps
Query Time
- OpenSearch executes the boolean query and collects matching
docIDs
- The
RoaringTermsAggregator converts matched docIDs into a RoaringBitmap (Q)
- For each ordinal k, it computes
|Q โฉ Bitmap_k| using RoaringBitmap.andCardinality()
- The RoaringBitmap library internally uses:
- Container-level (64K-block) skipping: Empty blocks are skipped in O(1)
- POPCNT acceleration: HotSpot intrinsifies
Long.bitCount() to the x86 POPCNT instruction
- Top-N ordinals are selected via a min-heap and resolved to term values
Architecture Diagram
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ OpenSearch Node โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ REST API โโโโโถโ RoaringTermsAggregation โ โ
โ โ Request โ โ Builder (parse field, size) โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ โ
โ โ RoaringTermsAggregator โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โ โ 1. Collect docIDs โ โ โ
โ โ โ 2. Build query bitmap โ โ โ
โ โ โ 3. AND + POPCNT โ โ โ
โ โ โ 4. Top-N selection โ โ โ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโ โ โ
โ โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ โ
โ โ โ
โ โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโ โ
โ โ RoaringDocValuesProducer โ โ
โ โ (reads .rvd + .rvm files) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Index Time: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ RoaringDocValuesConsumer โ โ
โ โ docโordinals[] โโtransposeโโโถ ordโBitmap(docs) โ โ
โ โ Write .rvd (data) + .rvm (metadata) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
API Reference
roaring_terms Aggregation
A terms aggregation optimized for Roaring Bitmap-indexed keyword fields.
Request:
{
"aggs": {
"<aggregation_name>": {
"roaring_terms": {
"field": "<field_name>",
"size": <number>
}
}
}
}
Response:
{
"aggregations": {
"<aggregation_name>": {
"buckets": [
{
"key": "<term_value>",
"doc_count": <count>
}
]
}
}
}
Compatibility
| Plugin Version |
OpenSearch Version |
Java Version |
Status |
| 2.19.0.0 |
2.19.x |
JDK 17+ |
โ
Supported |
Building from Source
Prerequisites
Build Steps
# Clone the repository
git clone https://github.com/rajat315315/opensearch-roaring-aggs.git
cd opensearch-roaring-aggs
# Build the plugin (includes running tests)
./gradlew build
# Build just the plugin ZIP (skip tests)
./gradlew pluginZip -x test
The plugin ZIP will be located at:
build/distributions/opensearch-roaring-aggs-2.19.0.0.zip
Running Tests
# Run all tests
./gradlew test
# Run a specific test class
./gradlew test --tests "org.opensearch.plugin.roaring.codec.RoaringDocValuesFormatTests"
# Run with verbose output
./gradlew test --info
Test Coverage
| Component |
Test Class |
Coverage |
| BitsetUtil |
BitsetUtilTests |
AND+POPCNT, empty blocks, edge cases |
| DocValues Format |
RoaringDocValuesFormatTests |
Round-trip, multi-valued, high cardinality |
| Aggregation |
RoaringTermsAggregatorTests |
Builder, buckets, merge logic |
Uninstallation
# Remove the plugin
bin/opensearch-plugin remove opensearch-roaring-aggs
# Restart OpenSearch
sudo systemctl restart opensearch
โ ๏ธ Warning: After uninstalling the plugin, indices created with index.codec: RoaringCodec will not be readable. Either reindex the data with the default codec before uninstalling, or keep the plugin installed.
Contributing
See CONTRIBUTING.md for guidelines.
License
This project is licensed under the Apache License 2.0.
Acknowledgments
่ฏ่ฎบ