Building indexes online¶
When a new index is added to a store that already has data, the Record Layer may mark the index write-only instead of building it in the opening transaction. Schema evolution covers when that happens. Use OnlineIndexer to fill the index in many small transactions while the store stays available for ordinary reads and writes.
Because the index is write-only during the build, live record saves keep it up to date. The online build only has to catch up; it does not race a moving target.
Basic usage¶
Build across multiple transactions, then mark the index readable:
try (OnlineIndexer indexBuilder = OnlineIndexer.forRecordStoreAndIndex(recordStore, "newIndex")) {
indexBuilder.buildIndex();
}
If the store is small enough to finish in the current transaction:
try (OnlineIndexer indexBuilder = OnlineIndexer.forRecordStoreAndIndex(recordStore, "newIndex")) {
indexBuilder.rebuildIndex(recordStore);
}
rebuildIndex is the same idea as the automatic build that runs when a store has fewer than about 200 records. For anything larger, use buildIndex().
By default buildIndex() continues a write-only index that is already partly built. That is the recommended behavior unless you have reason to think the existing index data is corrupt.
Limiting impact on live traffic¶
Index builds run at FDBTransactionPriority.BATCH unless you raise the priority. The builder also caps how much work each transaction does. The knobs that usually matter:
Setter |
Default |
What it does |
|---|---|---|
|
100 records |
Maximum records processed in one transaction. Lower it if transactions time out. |
|
same as |
Start below the max so the first transactions are cheaper. |
|
900,000 |
Commit when the transaction write size exceeds this. |
|
4,000 |
Commit and start a new transaction before FoundationDB’s 5 second limit. |
|
10,000 |
Throttle the scan so the build does not crowd out user traffic. |
|
100 |
Extra retries for a single range, including |
|
|
Log scanned ranges. |
Progress is tracked by default. Scanned-record counts are persisted and can be read through IndexBuildState.
setConfigLoader can replace these values between transactions if you want the build to react to load.
Parallel builds¶
One indexer is enough for a modest store. For a large store, run several processes with the same arguments and turn on mutual indexing:
OnlineIndexer.IndexingPolicy policy = OnlineIndexer.IndexingPolicy.newBuilder()
.setMutualIndexing()
.build();
try (OnlineIndexer indexBuilder = OnlineIndexer.newBuilder()
.setRecordStore(recordStore)
.setIndex("newIndex")
.setIndexingPolicy(policy)
.setRecordsPerSecond(2000)
.build()) {
indexBuilder.buildIndex();
}
Mutual indexing splits the primary-key space into fragments — by approximate FoundationDB shard boundaries, unless you pass your own — and lets each process claim work without duplicating it. A RangeSet on the index records which ranges are done, which also keeps non-idempotent indexes such as COUNT consistent.
To choose the fragments yourself, pass primary-key boundaries. The list can start and end with null so the full key space is covered:
OnlineIndexer.IndexingPolicy.newBuilder()
.setMutualIndexing()
.setMutualIndexingBoundaries(primaryKeyBoundaries)
.build();
Mutual indexing is experimental. Every participant must use the same policy.
Building from another index¶
If an existing readable index already covers the records you need, scan that instead of the primary store:
OnlineIndexer.IndexingPolicy.newBuilder()
.setSourceIndex("existingIndex")
.build();
The source index must be readable, idempotent, and cover every record the new index needs. If it cannot be used, the build falls back to a record scan unless you call forbidRecordScan().
Partly built indexes¶
IndexingPolicy chooses what to do when the index is disabled, write-only, already readable, or was started with a different method (CONTINUE, REBUILD, or ERROR). The default is to continue a write-only build.
See the OnlineIndexer javadoc for the full builder API.