optimize() compacts and remaps affected indexes in the
same call, before returning. Compaction’s defer_index_remap option opts
out of that — the call returns once compaction itself commits, leaving
the remap for later — which is what lets a custom remapper or a
stable-row-ID table skip the synchronous step. LanceDB Enterprise runs
compaction and indexing as separate, independently scheduled
background jobs — a table can compact while an index build is still
catching up from an earlier compaction — so it needs a way to keep an index
correct without forcing every compaction to wait for a synchronous remap.
The fragment reuse index
The fragment reuse index (FRI) is a Lance format-level mechanism, not an Enterprise add-on: a system index that records every fragment rewrite as a transition — the source fragments’ digests, the destination fragments’ digests, and a row-address mapping between them (Transition and Mapping
in lance-table’s system_index::frag_reuse::ledger). An index that covers
a rewritten fragment does not need to be rebuilt before the rewrite commits;
it can instead translate the row addresses it already stores forward through
the chain of recorded transitions at read time.
Only a compaction run with defer_index_remap ever touches the FRI; a
synchronous-remap rewrite — the Embedded default — never tags a table or
appends anything. Every defer_index_remap commit is classified before
it is admitted; that classifier is Lance’s own commit protocol
(lance-table’s frag_reuse::gate::classify), the same one every write
funnels through — not a lock or a sequencing rule Enterprise imposes from
outside. A table starts untagged: its deferred rewrites are admitted
as ordinary business with nothing appended, unless the rewrite has
stable row IDs to translate or touches a fragment an index covers — either
one makes that commit record a legacy, untagged (“v0”) FRI entry, which
does not tag the table. Tagging is a separate, one-way transition (a
sticky manifest flag that a later trim never clears) that publishes the
FRI’s first real transition; this page does not cover what triggers it.
Once a table is tagged, its commits are classified against which indexes
cover the rewritten fragments the way the rest of this page describes, and
every entry it records from then on is a tagged transition, never v0
again. A table with stable row IDs is permanently confined to the
untagged, v0 form — Lance refuses to tag it at all — so its deferred
compactions keep recording v0 entries indefinitely, for every rewrite that
moves row addresses, not only ones that touch an indexed fragment: the
address translation itself depends on that record, independent of
indexing.
Once a table is tagged, Lance refuses a compaction that does not set
defer_index_remap, including a plain Embedded optimize() call: an
Enterprise-tagged table has to keep deferring the remap, or be compacted
by a writer that can remap the tagged history eagerly.
Enterprise defers and batches the catch-up
Translating addresses through a long chain of transitions at every read adds up, so Enterprise does not let the FRI grow without bound. Its job planner tracks how many transitions have accumulated for a table and schedules a dedicated remap catch-up job — which applies the accumulated transitions to the indexes that need them, physically, in one pass — once either:- the FRI reaches 5 pending transitions (
remap_fri_version_threshold, tunable), or - the oldest pending transition is 1 day old
(
remap_fri_max_age_secs, tunable), whichever comes first.
What to read next
Compaction and cleanup
What compaction merges, and how the embedded library runs it in one call.
Incremental Indexing
The
index_remap job type, and keeping an index current as the table changes.Distributed Compaction
How compaction itself scales across the jobs fleet.
Distributed Indexing
How an index build scales across the jobs fleet.