Repository navigation
[Feature](lance) Add vector and scalar index lifecycle management #66497
Copy link
Copy link
Open
Labels
area/catalogIssues or PRs related to catalog managementIssues or PRs related to catalog managementkind/featureCategorizes issue or PR as related to a new feature.Categorizes issue or PR as related to a new feature.
Description
Parent
Tracked by #66340. #65730 provides read-only index consumption and automatic vector-index selection.
Scope
- Create vector indexes.
- Create scalar indexes.
- Show the number of indexes. Exact logical-index counts remain deferred by the accepted inspection contract; physical entry counts must not be presented as logical-index counts.
- Show logical index names, columns, and types for Directory catalogs through
SHOW INDEX([feature](lance) Support SHOW INDEX for Lance Directory tables #66637; [doc](lance) add doc about SHOW INDEX for lance catalog doris-website#4090). - Show index names, UUIDs, columns, types, and dataset versions. Implementation/tests are merged in [feature](lance) Support SHOW INDEX for Lance Directory tables #66637 / [feature](lance) Add lance_index_entries inspection TVF #66671; user documentation for the physical
lance_index_entriesTVF remains outstanding. REST inspection is outside this delivery scope. - Replace or rebuild an index with the same name.
- Drop indexes by name.
- Define authorization, concurrency, versioning, failure recovery, and metadata refresh semantics.
Completion criteria
- FE statements and metadata APIs are implemented.
- BE or service-side index operations are integrated with Lance.
- Unit and regression tests cover create, inspect, replace, rebuild, drop, and failures.
- User documentation describes syntax, lifecycle semantics, and limitations.
- The behavior is verified for vector and scalar index types.
Delivery status — 2026-09-30
This summarizes the existing staged roadmap; it does not broaden the accepted mutation or inspection contract. Completion on branch-4.1 satisfies the parent feature matrix's branch criterion.
- Merged read-only inspection: [feature](lance) Support SHOW INDEX for Lance Directory tables #66637 (
SHOW INDEX, with [doc](lance) add doc about SHOW INDEX for lance catalog doris-website#4090) and [feature](lance) Add lance_index_entries inspection TVF #66671 (lance_index_entries, with physical UUID/dataset version and FE/BE/regression verification). The physical-TVF website documentation was deferred in [feature](lance) Add lance_index_entries inspection TVF #66671 and is not present in the current catalog guide, so the combined inspection delivery item is not yet fully checked. - Merged mutation foundations: [feature](lance) Add thrift definitions for Lance index entries inspection #67275, branch-4.1: [feature](lance) Add statically validated Lance index DDL surface #67201, branch-4.1: [feature](lance) Durable Lance index job infrastructure with fence, quota and replay #67235, and branch-4.1: [feature](lance) Add Lance index admission, job inspection SQL and catalog DDL guard #67630 provide protocol/static validation, durable job state, admission, job inspection, and guards. They do not complete enabled end-to-end index mutation.
- Still open: branch-4.1: [feature](lance) Add Lance index job dispatcher with thrift dispatch boundary #67978 (dispatcher), branch-4.1: [feature](lance) Add RESOLVE LANCE INDEX JOB (FORCE_RELEASE) and job retention GC #67754 (resolution/retention), branch-4.1: [feature](lance) Add the cgroup-isolated one-shot worker for Lance index mutations #68668 (isolated worker), and branch-4.1: [test](lance) Prove the IVF_PQ tracer and worker-fault convergence through the isolated worker #68669 (tracer/fault tests). The mutation gate remains off. Scalar lifecycle coverage, acceptance evidence, and user documentation still need completion before marking create/replace/rebuild/drop done.
The issue remains open. The original combined completion criteria still cover the unfinished lifecycle, not only the delivered inspection slice.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area/catalogIssues or PRs related to catalog managementIssues or PRs related to catalog managementkind/featureCategorizes issue or PR as related to a new feature.Categorizes issue or PR as related to a new feature.
Activity
Gabriel39 commentedon Aug 5, 2026
@u70b3, would you be available to take ownership of this issue and implement it?
u70b3 commentedon Aug 5, 2026
Thanks @Gabriel39 — I'd be glad to take ownership of this issue. Please assign it to me.
u70b3 commentedon Aug 6, 2026
Here is a revised design proposal for this issue. It builds on #65730, the Lance Java SDK / lance-c versions pinned by Doris, and the existing Doris pattern for mutable external lake-table catalogs (Iceberg/Hive/Paimon): Doris parses a common SQL surface, dispatches through a catalog-specific operation seam, lets the external format remain the source of truth, and refreshes Doris metadata after a successful external commit.
/cc @Gabriel39 @zhangstar333
1. Goals and Phase-1 boundary
Phase 1 provides index lifecycle management for writable filesystem-backed Lance catalogs:
Lance dataset manifests remain the authoritative index metadata. Doris does not persist a second copy of Lance index definitions in its internal table/index metadata.
Phase 1 uses the Lance Java SDK in FE and is intentionally synchronous. A BE-distributed or asynchronous build path is a follow-up and should not change the SQL contract.
2. SQL surface
Reuse the existing Doris index statements and command chain, extended for Lance external tables:
Proposed semantics:
USINGis required for Lance tables; Doris's internal default-to-INVERTED behavior should not be applied implicitly to an external format.IF NOT EXISTSandOR REPLACEare mutually exclusive.CREATE INDEXfails when the logical name already exists;IF NOT EXISTSreturns success without rebuilding;CREATE OR REPLACE INDEXperforms a full rebuild and maps to Lancereplace=true.catalog.IndexType.3. SHOW INDEX and the Lance index model
Lance has two levels of index metadata:
A distributed or incrementally maintained logical index may therefore have multiple UUIDs and dataset versions. The logical count must not be derived from the number of physical manifest entries.
The proposal is to keep Doris's existing MySQL-compatible 13-column
SHOW INDEXschema instead of returning a different column layout only for Lance tables:Key_name: logical index name;Column_name: indexed column;Index_type: logical Lance index type;Properties: aggregate coverage, index-specific details, and a JSON list of physical segments (uuid,dataset_version, and fragment coverage).For example:
{ "indexed_fragments": 8, "unindexed_fragments": 2, "indexed_rows": 100000, "unindexed_rows": 24000, "segments": [ { "uuid": "...", "dataset_version": 12, "fragments": [0, 1, 2, 3] } ] }The index count is the number of distinct user-visible logical indexes, excluding Lance system indexes. It is obtained from logical index descriptions, not from
getIndexes()/ lance-c physical entry counts. No additional count statement is proposed.If reviewers prefer UUIDs and dataset versions as first-class result columns, a dedicated Lance system table or statement is safer than changing the established
SHOW INDEXresult schema.4. Execution model
The Phase-1 flow follows Doris external-catalog DDL precedent:
The pinned Lance Java SDK provides create, drop, logical description, physical metadata, and distributed segment/commit primitives. Therefore Phase 1 does not require a new FE-to-BE thrift/RPC protocol.
There is an important difference from lightweight Iceberg/Paimon metadata DDL: Lance index creation scans column data and performs native CPU, memory, and object-store I/O work. In Phase 1 this work runs in Lance native threads inside FE. The synchronous implementation should therefore be documented with the following limitations:
createIndex;KILL QUERYdoes not guarantee that the native build has stopped;The operator boundary keeps a future implementation free to move segment building to BE workers or an external service while FE retains orchestration and the final commit. Lance exposes the required distributed model, but Lance itself does not provide the scheduler; the current lance-c v0.1.2 surface should not be assumed to contain the same distributed APIs as the Java SDK.
5. Authorization
CREATE INDEX,CREATE OR REPLACE INDEX, andDROP INDEXrequire the table-levelALTERprivilege.SHOW INDEXrequires the table-levelSHOWprivilege.This matches Doris internal index DDL and existing external-table ALTER behavior. The remote filesystem/object-store access still uses the catalog credential; Doris table privilege is the user-facing authorization boundary.
6. Lifecycle semantics
Versioning and visibility
SHOW INDEXreads the pinned snapshot; mutation statements fail instead of silently switching to latest.Concurrency
v9.1.0-beta.3implementation tests same-name concurrent create/replace as a retryable commit conflict. Phase 1 should surface that failure and ask the user to retry instead of promising last-committer-wins.Failure recovery and retries
SHOW INDEXis the reconciliation mechanism.IF NOT EXISTSprovides no-op retry behavior once the logical index is visible.CREATE OR REPLACE INDEXis convergent by name but is not strictly idempotent: retrying may rebuild work and produce a new physical UUID/dataset version.DROP INDEXon a missing name fails unlessIF EXISTSis specified.Drop and metadata refresh
SHOW INDEXreads live index metadata from the target Lance snapshot rather than persisting index information in the Doris catalog cache.7. Initial supported types and catalogs
Proposed initial set:
IVF_PQ.IVF_FLATis included only if the PR verifies that the [feature](lance) support lance catalog in doris #65730 Doris query path actually consumes it end-to-end.BTREE,BITMAP, andINVERTED, each gated by schema/property validation and an end-to-end query test.Catalog scope:
Dataset.createIndex.SHOW INDEXcan be supported through namespace list/stats capabilities if reviewers want it in Phase 1; otherwise all REST lifecycle statements are rejected consistently and documented.8. Implementation and test orientation
Implementation details will be reviewed in the PR. The intended direction is:
AlterTableCommand+ create/drop index ops and the existingShowIndexCommand;LanceIndexOperator-style seam;Tests should cover parser/to-SQL behavior, privileges, supported/unsupported types and properties, duplicate/replace/drop behavior, logical versus physical metadata, append coverage, concurrency conflicts, historical-version mutation rejection, REST behavior, and vector/scalar query use. Regression tests should use an isolated writable Lance dataset rather than mutating a shared read-only fixture.
Questions for reviewers
CREATE OR REPLACE INDEXsyntax acceptable? The alternativePROPERTIES("replace"="true")avoids grammar work but hides lifecycle semantics.SHOW INDEXschema with physical segment metadata inProperties, or should this issue introduce a dedicated Lance index result surface?IVF_PQ + BTREE/BITMAP/INVERTEDthe right initial end-to-end set, withIVF_FLATadded only after query-side verification?SHOW INDEXbe supported through list/stats while mutation remains unsupported, or should all index lifecycle statements be rejected consistently?References
AlterTableCommand, create/drop index ops, andShowIndexCommandGabriel39 commentedon Aug 6, 2026
Thanks for the detailed proposal. The source-of-truth model, logical/physical index distinction, version semantics, and post-commit metadata refresh direction are sound. After comparing the proposal with the current Doris command path, the pinned Lance SDK/Namespace API, and the Lance Spark, StarRocks, ClickHouse, DuckDB, and Trino designs, I think the following points should be resolved before implementation.
Blocking concerns
1. A synchronous native build in FE is not a safe production execution model
The proposal already lists the essential problems: index creation performs data scans and native CPU/memory/object-store I/O in FE, while the pinned API has no reliable cancellation/progress mechanism and FE failover can leave the result ambiguous. This also bypasses Doris workload management and allows a user with ALTER to trigger unbounded native work in the control plane.
The closest Lance integration distributes fragment builds to Spark executors and leaves the driver to coordinate/finalize the commit. ClickHouse similarly separates index metadata operations from the heavy MATERIALIZE INDEX mutation.
My recommendation is an asynchronous job with FE orchestration and BE or external-service execution, including status, cancellation, concurrency limits, and retry/reconciliation. If FE-side execution must exist temporarily, it should be explicitly experimental, disabled by default, and protected by dataset-size/fragment/concurrency limits. It should not be presented as the final production lifecycle implementation.
2. Reusing the current Doris index command chain requires a larger refactor than the proposal describes
The current parser only accepts NGRAM_BF, INVERTED, and ANN; it does not accept IVF_PQ, BTREE, BITMAP, or OR REPLACE:
doris/fe/fe-core/src/main/antlr4/org/apache/doris/nereids/DorisParser.g4
Lines 237 to 240 in e3289c1
More importantly, CreateIndexOp.validate() always translates the definition into the persisted internal catalog.Index, while AlterTableCommand currently rejects CreateIndexOp for external tables:
doris/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/CreateIndexOp.java
Lines 73 to 89 in e3289c1
That conflicts with the stated goal of keeping Lance index DTOs separate from Doris internal index serialization. The design should explicitly introduce a neutral parsed index definition, resolve the target table/catalog first, and then choose internal-index validation/serialization or Lance-specific validation/dispatch. Adding only a LanceIndexOperator after the existing validation path will not be sufficient.
For vector syntax, I would prefer reusing the Doris category-level surface:
This also follows the StarRocks pattern of USING VECTOR plus an index_type property and avoids adding a grammar keyword for every Lance algorithm.
Other required revisions
3. Incremental maintenance is necessary for a usable external index lifecycle
External writers can append fragments immediately after an index is created. Coverage will then decay. CREATE OR REPLACE INDEX performs an expensive full rebuild and is not an adequate maintenance operation.
The pinned Java SDK exposes Dataset.optimizeIndices(), and Lance documents it as the incremental path for indexing newly appended fragments. I suggest adding a separate operation such as:
The tests must also prove that queries include unindexed fragments correctly. Otherwise partial coverage becomes a correctness issue, not merely a performance issue.
4. Do not put unbounded physical segment metadata in SHOW INDEX.Properties
A JSON segments array can become extremely large for a fragmented or distributed index. It can create FE memory, MySQL result packet, and client usability problems.
Lance Spark intentionally returns only logical summaries from SHOW INDEXES and excludes per-segment metadata:
https://lance.org/integrations/spark/operations/ddl/show-indexes/
I recommend keeping the existing 13-column SHOW INDEX surface for logical name, columns, type, and aggregate coverage only. UUIDs, dataset versions, and fragment sets should be exposed through a structured and filterable system table/TVF such as lance_index_segments, not embedded in one string field.
5. REST behavior should be capability-based, not rejected by catalog type
The pinned lance-namespace-core 0.7.7 interface already defines createTableIndex, createTableScalarIndex, listTableIndices, describeTableIndexStats, and dropTableIndex:
https://github.com/lance-format/lance-namespace/blob/v0.7.7/java/lance-namespace-core/src/main/java/org/lance/namespace/LanceNamespace.java#L394-L447
For a Directory Catalog, direct Dataset operations are appropriate. For a REST Catalog, index operations should go through the Namespace service when the server supports them; Doris should return a clear unsupported error when it does not. Directly mutating the dataset with vended credentials would bypass service-side authorization, managed versioning, and transaction policy. At minimum, REST SHOW INDEX should use list/stats operations in Phase 1.
6. Narrow and rename the initial type set
I suggest Phase 1 support:
Lance INVERTED is an FTS index with different tokenizer/query semantics from a Doris internal inverted index. Exposing the same keyword before Doris can query it is misleading. A future SQL surface should call it FTS.
The complete type/property matrix should be part of the design, not deferred entirely to the PR. It should include column types, vector element type/dimension/nullability, metric compatibility, parameter ranges, and build/query ABI compatibility.
7. Define SQL identifier semantics for index names
Allowing both idx and IDX while matching names exactly makes DROP INDEX idx ambiguous under Doris/MySQL identifier behavior. We should preserve display case but define quoted/unquoted resolution and reject new case-only duplicates, or provide an unambiguous quoted-name rule for pre-existing external collisions.
8. Refresh/recovery must handle post-commit failures
“Failed mutations do not refresh metadata” is insufficient when the Lance commit succeeds but FE fails before the edit log or client response. A later IF NOT EXISTS retry may be a no-op and still leave Doris caches stale.
The implementation should distinguish pre-commit failure from unknown/post-commit outcome, reopen the latest snapshot to verify the postcondition, and invalidate/refresh Doris metadata even for a reconciled no-op. SHOW INDEX helps the user observe state but does not repair the query path cached metadata.
Also, local/file catalogs need an explicit shared-filesystem or single-node restriction; otherwise a master failover can move execution to an FE that cannot see the same path.
Answers to the review questions
The proposal demonstrates a good understanding of the Lance index model, but the execution placement and the split between Doris internal index objects and external Lance operations need to be redesigned before implementation starts.
u70b3 commentedon Aug 6, 2026
Revised Design Proposal v2 — Asynchronous Lance Index Lifecycle
This revision supersedes the previous proposal and incorporates the review feedback from @Gabriel39.
Major changes from v1
1. Goals and Phase-1 boundary
Phase 1 provides a production-oriented lifecycle for user-visible Lance indexes:
Lance dataset manifests or the Namespace service remain the authoritative index metadata. Doris does not persist a second copy of Lance index definitions in its internal table/index metadata. Doris persists only its own asynchronous job state and the information needed to reconcile an external operation.
Historical-version tables remain read-only for index mutation. Inspection uses the selected snapshot; mutation targets only the latest writable table state.
An FE-side synchronous native build is not part of the production implementation. If retained temporarily for development, it must be experimental, disabled by default, and protected by strict dataset-size, fragment-count, and concurrency limits. It does not satisfy Phase-1 completion.
2. SQL surface
Vector creation
Scalar creation
Full replacement and incremental maintenance
CREATE OR REPLACE INDEX performs a full rebuild and atomically replaces the logical index. BUILD INDEX indexes only fragments not covered by the current logical index and maps to the Lance incremental optimization path.
Inspection, status, cancellation, and drop
The existing Doris SHOW BUILD INDEX and CANCEL BUILD INDEX surfaces are extended to Lance external-index jobs instead of introducing a second job-control vocabulary.
Statement semantics
3. Neutral command model and catalog dispatch
The current internal path must not translate every parsed definition into persisted catalog.Index before the target catalog is known.
The revised analysis flow is:
ParsedIndexSpec contains only SQL-level information:
For an internal OLAP table, the existing provider validates and translates the specification into Doris catalog.Index and the legacy schema-change path.
For a Lance external table, LanceIndexProvider produces a Lance-specific immutable definition and operation request. Lance types and physical metadata are never added to Doris internal IndexType serialization.
The provider boundary is also used by SHOW, BUILD, DROP, reconciliation, and capability checks so lifecycle behavior does not drift across separate command implementations.
4. Asynchronous execution model
Durable job
Create, replace, and incremental build create a persistent LanceIndexJob before heavy work starts. The minimum states are:
The persisted job records:
FE failover reconstructs unfinished jobs from the edit log and resumes polling, reconciliation, commit, or refresh. It does not blindly restart native training.
Responsibility split
FE performs:
For Directory Catalog, BE workers perform:
For REST Namespace Catalog, the Namespace service owns execution and commit according to its advertised capabilities. Doris tracks the returned operation or transaction identifier and reconciles the result through the service; it does not substitute a direct Dataset mutation.
FE metadata locks are not held during native training. Doris serializes conflicting jobs for the same dataset and logical index name, while Lance transaction rules remain authoritative for conflicts with external writers.
Resource and cancellation behavior
5. Provider and catalog behavior
Directory Catalog
For Directory Catalog, this proposal chooses Doris BE execution rather than introducing an external Java/Rust build service. Because BE is C++, this choice requires lance-c to expose the distributed Rust Lance APIs that already exist in core.
Directory-backed datasets use a distributed BE build pipeline. Phase 1 extends lance-c with the native capabilities needed by that pipeline:
FE selects disjoint fragment groups, dispatches BE tasks, validates the returned coverage, and performs the single atomic logical-index commit through the pinned Lance Java SDK. BE workers never publish the logical index independently.
CREATE and CREATE OR REPLACE build all fragments visible in the starting snapshot. BUILD INDEX computes the uncovered fragments from the latest logical description and sends only those fragments through the same segment pipeline. Concurrent appends after planning remain unindexed and can be covered by a later BUILD INDEX.
For object-store-backed datasets, workers use the catalog credentials and the dataset URI resolved from the latest table metadata.
A local/file dataset is mutable only when the deployment guarantees that the path is visible with identical contents to the selected executor and to every FE that may reconcile or finalize the operation. Otherwise mutation is rejected. Local filesystem support is intended primarily for single-node development and tests; it must not silently depend on the current master FE's local disk.
REST Namespace Catalog
REST behavior is capability-based:
6. Initial type and property matrix
Only types verified by both creation and the Doris query path are enabled.
IVF_PQ
BTREE
BITMAP
Deferred types
7. Logical and physical metadata surfaces
Lance has a logical index identified by name, columns, type, definition, and aggregate coverage. It may contain multiple physical segments with independent UUIDs, dataset versions, and fragment sets.
SHOW INDEX
The existing 13-column Doris schema is retained. It returns one row per logical index/column:
The Properties field never contains a segment array or fragment-ID list.
Logical index count is the number of user-visible logical descriptions, excluding system indexes. It is not derived from physical manifest entries. No separate count statement is added.
Physical metadata
A filterable lance_index_segments TVF exposes one row per physical segment:
Phase 1 does not expose the complete segment-to-fragment mapping. Aggregate logical coverage is available from SHOW INDEX, and per-segment fragment counts are available from the TVF. Explicit fragment-ID membership is deferred until there is a demonstrated diagnostic use case, avoiding another potentially unbounded result surface.
The TVF reads live metadata from the selected Lance snapshot. It does not persist a copy in Doris catalog metadata.
8. Authorization and identifier semantics
Doris preserves the display case returned by Lance but resolves user-supplied index names case-insensitively, matching existing Doris/MySQL identifier behavior.
9. Versioning, concurrency, and visibility
10. Failure recovery and metadata refresh
The implementation distinguishes three outcomes.
Confirmed pre-commit failure
The external manifest or Namespace transaction was not committed. The job becomes FAILED. Doris does not run post-DDL refresh. Unreferenced artifacts are reclaimed later by Lance cleanup according to retention policy.
Confirmed commit
The requested postcondition is visible in the latest snapshot:
Doris invalidates and refreshes table metadata and emits the normal external-DDL refresh edit log. A refresh failure does not turn the job into an ordinary build failure; the job becomes COMMITTED_REFRESH_PENDING and retries refresh.
Unknown or post-commit failure
After FE failover, timeout, lost worker response, or an ambiguous commit result, Doris reopens the latest snapshot or queries the Namespace transaction and verifies the operation-specific postcondition.
A reconciled IF NOT EXISTS or IF EXISTS no-op still invalidates/refreshes Doris metadata when it observes external state that may be newer than the local cache.
SHOW INDEX is the user-visible reconciliation tool, but it is not a substitute for repairing Doris metadata caches.
11. Test plan
Tests cover:
Regression tests use isolated writable datasets and generate expected result files through the normal regression-test scripts.
12. Confirmed design decisions and implementation decomposition
The review questions from v1 are resolved as follows:
Implementation is split into the following dependency-ordered work items:
These items may be delivered as separate dependent PRs, but the production Directory create/build path remains disabled until the native build, cancellation, durable job, commit-fencing, reconciliation, and end-to-end query requirements are all present.
Remaining implementation-planning question
The technical direction above is fixed, but the delivery boundary still needs reviewer agreement:
Is direct BE execution through an extended lance-c the preferred integration model, or should Doris use an external Java/Rust index-build service?
References:
Gabriel39 commentedon Aug 6, 2026
Thanks for the detailed revision. V2 addresses most of the architectural concerns from the first review: synchronous FE construction is no longer the production path, the neutral command model is clearer, BUILD INDEX is separated from full replacement, logical and physical metadata are separated, and the recovery states are substantially better defined.
I think the Directory Catalog direction is now viable, but I do not think the design is implementation-ready yet. The following issues still need to be resolved.
1. The REST design currently promises more than the pinned Namespace API can express
Doris pins Lance Namespace 0.7.7. Its
CreateTableIndexRequestsupports the column, index type, name, distance type, and FTS-related fields, but it does not carrynum_partitions,num_sub_vectors,zone_size, replace semantics, or an uncovered-fragment/incremental-build request:https://github.com/lancedb/lance-catalog/blob/v0.7.7/java/lance-namespace-apache-client/docs/CreateTableIndexRequest.md
There is also no separate Namespace BUILD INDEX operation. In addition,
ListTableIndicesexposes only name, UUID, columns, and status, while index stats expose only type, distance, indexed/unindexed row counts, and physical index count. That is not sufficient to verify that an existing REST index has the complete normalized definition required by the proposedIF NOT EXISTSreconciliation.Please either:
“Advertised capabilities” should also be made concrete. In the pinned Java interface, optional operations are primarily represented by typed
UnsupportedOperationException; there is no general capability-advertisement API.2. Reusing SHOW/CANCEL BUILD INDEX requires a real external-job integration design
The current
CancelBuildIndexCommandexplicitly rejects external catalogs and eventually looks up an internalOlapTableandSchemaChangeHandler.IndexChangeJob:https://github.com/apache/doris/blob/master/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CancelBuildIndexCommand.java
Similarly,
SHOW BUILD INDEXreads the internal/jobs/<dbId>/build_indexproc data and has internal-index-specific columns such asPartitionNameandAlterInvertedIndexes:https://github.com/apache/doris/blob/master/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowBuildIndexCommand.java
Extending the SQL vocabulary is reasonable, but the design should specify the common job registry/proc source, catalog qualification, filtering, privilege checks, output schema, and state mapping for internal and Lance jobs. Otherwise the SQL examples in this proposal cannot work through the existing implementation.
3. REST reconciliation needs job-specific provenance, not only a matching postcondition
The remote
transaction_idis optional. If the service commits the operation but FE fails before persisting the returned transaction ID, a later “same name and matching definition” observation does not prove that this Doris job performed the commit; an external writer may have produced the same postcondition.This is particularly important because Lance allows concurrent CreateIndex transactions. For two operations on the same logical name, the later operation can replace the earlier one rather than returning the retryable conflict assumed by this proposal:
https://lance.org/format/table/transaction/
Please persist a client operation ID before dispatch and propagate it through Namespace transaction properties or another queryable server-side idempotency/provenance mechanism. For Directory builds, expected segment UUIDs can provide this proof. Without job-specific evidence, the REST job should remain
OUTCOME_UNKNOWNinstead of being marked successful merely because the logical definition matches.4. DROP also needs a durable operation record
The proposal creates durable jobs for create, replace, and build, but allows DROP to complete synchronously while still promising failover-safe post-commit reconciliation and refresh.
If DROP commits externally and FE fails before refresh/edit-log completion, there is no durable state from which to resume. DROP should therefore use the same durable Lance job/operation record, even if it normally moves through the states very quickly.
5. Please make the distributed segment contract explicit
The pinned Java SDK already exposes
mergeExistingIndexSegmentsandcommitExistingIndexSegments, so BE construction followed by one FE commit is technically plausible:https://github.com/lance-format/lance/blob/v9.1.0-beta.3/java/src/main/java/org/lance/Dataset.java
However, the design should require:
See: https://lance.org/guide/distributed_indexing/
Credentials should not be persisted in the job edit log. Workers should obtain or refresh catalog credentials when a task or retry is dispatched.
6. Metadata details
I recommend reporting
IVF_PQ, rather thanANN, inSHOW INDEX.Index_type. ANN is the SQL category, whileIVF_PQis the actual index type; reporting only ANN becomes ambiguous when additional ANN implementations are added.REST metadata should also be explicitly capability-dependent: the pinned stats model does not expose fragment counts or the complete creation properties, so those SHOW fields cannot always be populated uniformly.
Integration-model recommendation
For the remaining planning question, I prefer direct BE execution through an extended lance-c for Directory Catalog. An external Java/Rust build service would introduce a new deployment, authentication, versioning, availability, and operational dependency for a core Apache Doris feature. Keeping execution in BE is a better fit as long as the production path remains gated until the required lance-c build, cancellation, progress, segment-metadata, and compatibility APIs are available.
Section 5 already says that the proposal chooses BE execution, while the final section presents this as unresolved. Please make the conclusion consistent.
Once the REST capability boundary, existing job-command integration, job-specific reconciliation, and durable DROP semantics are clarified, I think the design will be in much better shape for approval and decomposition into implementation issues.
u70b3 commentedon Aug 6, 2026
Thank you @Gabriel39 for the patient and precise guidance across both review rounds — each round has made this design substantially stronger. Below is the v3 full revision, addressing every round-2 point directly and incorporating several self-identified corrections.
Revised Design Proposal v3 — Asynchronous Lance Index Lifecycle
This revision incorporates the second-round review from @Gabriel39: REST is now an explicit capability matrix, SHOW/CANCEL BUILD INDEX get a concrete external-job integration design, reconciliation requires job-specific provenance, DROP uses the same durable job record, and the distributed segment contract is specified. It also contains several self-identified corrections to v2.
/cc @Gabriel39 @zhangstar333
Major changes from v2
operation_idbefore dispatch. Reconciliation requires job-specific evidence — for Directory, the expected segment UUID set plus theoperation_idrecorded in the Lance transaction properties at commit time; for REST, the server-returnedtransaction_id(see the honest boundary in §10). A matching postcondition alone is never accepted as success — including for DROP.Dataset.createIndex/commitExistingIndexSegments/dropIndexAPIs do not carry transaction properties in the pinned SDK; commits that must recordoperation_idgo throughTransaction.Builder.transactionProperties+operation.CreateIndex+CommitBuilder(verified end-to-end through JNI in the pinned sources).Corrections to v2 (self-identified)
docs/src/format/table/transaction.md:172-173). The pinned implementation reaches that semantics via conflict + rebase:check_create_index_txnreturns a retryable conflict for a same-name regular index on the first attempt (rust/lance/src/io/commit/conflict_resolver.rs:577-617), and the JavaCommitBuilder.maxRetriesdefaults to 0, so without retries the caller observes the conflict error. v2 saw only the implementation layer; the design must assume the doc layer for external writers (retried/ rebased commits can replace Doris-created indexes) — hence provenance in §10.rust/lance/src/index/vector/utils.rs:193,204-211). v2's matrix (UINT8 with L2/COSINE/DOT) was inconsistent. UINT8 is deferred until a binary-vector/HAMMING decision is made. INT8 is deferred: it is advertised by the pinned lance-c creation surface and accepted by the Rust core (L2/COSINE/DOT allowed), but it is an undocumented branch there and lacks end-to-end verification. FLOAT64 remains deferred — not advertised by the pinned lance-c creation surface (lance.h:926restricts to float32/float16/uint8/int8), although the Rust core accepts it.transaction_idis a generic long-running-operation handle, not the index progress channel.be/src/format_v2/table/lance_reader.cpp:521on branch-4.1). Lance INVERTED is deferred because Doris has no SQL-level FTS semantics for it yet — not because the query path is absent.{name, uuid, columns, type, dataset_version}per entry (lance-csrc/index.rs), and the Namespace stats model exposes only row counts (spec.yaml:3206-3234). This becomes lance-c extension item ⑥; the TVF ships without those columns until then. Reviewer approval for this deferral is requested explicitly.rust/lance/src/index/create.rs:202,rust/lance/src/index/api.rs:211; no normalization in builder/Python/Java layers). The §8 design — Doris-layer case-insensitive resolution over Lance's exact-match storage — stands precisely becauseidx/IDXcoexistence is a real external state.1. Goals and Phase-1 boundary
Phase 1 provides a production-oriented lifecycle for user-visible Lance indexes:
Lance dataset manifests or the Namespace service remain the authoritative index metadata. Doris persists only its own job state and reconciliation evidence. Historical-version tables remain read-only for index mutation; inspection uses the selected snapshot, mutation targets only the latest writable state.
An FE-side synchronous build is not part of the production implementation. A development fallback (experimental, disabled by default, limited by dataset-size/fragment/concurrency settings) MAY use the pinned Java SDK's existing uncommitted primitives (
IndexOptions.withFragmentIds/withIndexUUID, javadoc-verified uncommitted + fragment-scoped). The production objection is compute placement in the control plane, not missing APIs.2. SQL surface
Unchanged from v2 except the property-key question (Q2):
CREATE INDEX ... USING ANN PROPERTIES ("index_type"="IVF_PQ", "metric_type"="l2", "num_partitions"="256", "num_sub_vectors"="16")(key naming is Q2 below; example uses the proposed key);USING BTREE/USING BITMAP;CREATE OR REPLACE INDEX= full rebuild + atomic name replacement;BUILD INDEX= incremental coverage of uncovered fragments;SHOW INDEX FROM t;SHOW BUILD INDEX FROM db WHERE ...;CANCEL BUILD INDEX ON ctl.db.tbl (job_id);DROP INDEX [IF EXISTS].Statement semantics (delta from v2):
3. Neutral command model and catalog dispatch
Unchanged from v2:
ParsedIndexSpec→ resolve catalog/database/table →InternalIndexProviderorLanceIndexProvider→ provider validation → internal schema change or Lance external-index job. Lance definitions never enter Doris internalcatalog.Indexserialization.4. Durable job framework and SHOW/CANCEL BUILD INDEX integration
Job record (persisted via edit log)
operation_id(client-generated UUID), persisted before any dispatch;States
Unchanged from v2:
PENDING → PREPARING → BUILDING → COMMITTING → REFRESHING → FINISHED, plusFAILED / CANCELLING / CANCELLED / COMMITTED_REFRESH_PENDING / OUTCOME_UNKNOWN. DROP traverses the same machine (typically PENDING → COMMITTING → REFRESHING → FINISHED in one cycle). FE failover reconstructs unfinished jobs from the edit log and resumes polling, reconciliation, commit, or refresh; it does not blindly restart native training.Resource admission
Unchanged from v2, restated here because it gates the production path: jobs are admitted through explicit per-catalog and cluster-wide concurrency limits; BE execution is associated with workload-management accounting where the executor supports it.
Integration with the existing job commands
The grammar already accepts the required shapes on both master and branch-4.1:
SHOW BUILD INDEX ((FROM|IN) db)? wildWhere? sortClause? limitClause?andCANCEL BUILD INDEX ON <multipart table> (job_ids)(DorisParser.g4: master fe-sql-parser :392-393/:644-646; branch-4.1 fe-core :390-391/:640-642). The current implementations are internal-only on both branches:CancelBuildIndexCommand.validatecallsUtil.prohibitExternalCatalog(:80) and resolves anOlapTable+SchemaChangeHandler.IndexChangeJob;ShowBuildIndexCommandbuilds the internal proc path/jobs/<dbId>/build_index(:188-191) with internal-specific columns (PartitionName,AlterInvertedIndexes, TITLE_NAMES :68-74). The design:IndexChangeJobregistry. Lance jobs register here; the internal path is untouched.SHOW BUILD INDEXunions internal and external jobs. A newJobTypecolumn (INTERNAL/EXTERNAL_LANCE) distinguishes rows. Internal-only columns stay empty for external rows (Lance jobs are table-level, not partition-level). External rows populate the generic columns: JobId, DbName (catalog-qualified), TableName, IndexName, State, Progress, CreateTime, FinishTime, Msg.catalog.db.tbl(grammar +LogicalPlanBuilder.visitCancelBuildIndexare catalog-aware). SHOW's FROM clause currently takes a singleidentifieronly; it is extended to accept a catalog-qualifiedcatalog.db(preferred, consistent with the table qualification) — exact grammar detail in the PR.SHOWon the resolved external table; CANCEL requires table-levelALTER.PENDING … OUTCOME_UNKNOWN); WHERE filtering onStateuses these same job-state values; documentation maps them to the internal lifecycle.5. Execution model
Directory Catalog: BE pipeline + segment contract
Production execution requires extending lance-c (prerequisite, §12). FE orchestrates; BE workers build; FE performs the single atomic logical-index commit through the pinned Java SDK.
Commit path (verified against pinned sources): provenance-carrying commits go through the general transaction path —
Transaction.Builder.transactionProperties(Map)(Transaction.java:176-179) carrying the job'soperation_id, withoperation.CreateIndex, executed viaCommitBuilder(JNI wiring: lance-jni/src/transaction.rs:830-846). The one-shotDataset.createIndex(IndexOptions),commitExistingIndexSegments, anddropIndexpaths do not carry transaction properties (they build the commit internally withNoneproperties; create/commit-existing: rust/lance/src/index.rs:1538-1547, drop: rust/lance/src/index.rs:1177-1194) and are therefore not used for provenance commits.Segment contract:
Index.Builder(Index.java:187-258),commitExistingIndexSegments(Dataset.java:1176),IndexOptions.withFragmentIds/withIndexUUID(IndexOptions.java:135-157), and the commit path's own validation (non-empty set, unique UUIDs, non-overlapping fragment coverage; rust/lance/src/index.rs:100-131). The full loop is additionally verified end to end by a PoC on lance-core 9.1.0-beta.3: fragment-scoped uncommitted builds over disjoint fragment subsets → single coordinator commit → dataset version increment by exactly one → k-NN query consumes the resulting multi-segment logical index. An end-to-end Doris test will prove the same for a BE-built (lance-c) segment committed via the Java SDK.uuid, field IDs,name,datasetVersion,indexVersion,indexType, fragment coverage, and theindexDetailsprotobuf bytes (createdAt/baseIdoptional;createdAtis filled by the commit side). This is the exact field set the commit path requires — validated statically (JNIindex_metadata_to_segment, lance-jni/src/blocking_dataset.rs:1218-1243, rejects segments missing fragment coverage or index details;segment.fieldsis validated against the indexed column's field id, rust/lance/src/index.rs:1469-1475) and empirically (PoC: a Builder-reconstructed Index missing any of these fails before commit). Bare UUIDs are not sufficient.commitExistingIndexSegmentsrejects duplicate segment UUIDs (PoC-verified); the "shared UUID" wording inwithIndexUUID's javadoc belongs to the separate merge flow. FE pre-assigns per-worker UUIDs (viawithIndexUUIDwhere the executor supports it) so the expected UUID set is known before dispatch; this set backs the provenance rule in §10.VectorTrainer.trainIvfCentroids, distributed to builders throughIvfBuildParams.setCentroids; PoC-verified across workers); on the BE path this depends on lance-c extension ① accepting caller-supplied centroids/codebooks (§12). This keeps segments mutually compatible for a future physical merge and yields uniform recall across segments. Independent per-segment models (valid for fan-out query) are rejected for Phase 1 because they preclude later merge. Physical segment merge itself is not performed in Phase 1.FE experimental fallback: unchanged gating (experimental, disabled by default, size/fragment/concurrency limits). Implementation uses the Java SDK's existing uncommitted primitives; no lance-c dependency.
REST Namespace Catalog: capability matrix
Capability detection is behavioral: the pinned Java interface represents unsupported operations as default methods throwing a typed
UnsupportedOperationExceptionbound to spec error code 0 (LanceNamespace.java:402-448,errors/ErrorCode.java:22); there is no capability-advertisement API. Unsupported combinations fail during analysis where statically known, otherwise with a clear capability error. Doris never falls back to direct Dataset mutation with vended credentials. All REST mutations use the durable job record.SHOW INDEXListTableIndices+DescribeTableIndexStats. Fields limited to the stats model: name, UUID, columns, status; index type, distance, indexed/unindexed row counts, physical index count (spec.yaml:3149-3234). Fragment counts and creation properties are unavailable → corresponding SHOW fields are empty.CREATE INDEXCreateTableIndexRequestcarries: column, index_type, name, distance_type (FTS fields unused; spec.yaml:3049-3108). IVF build properties (num_partitions,num_sub_vectors) and BTREEzone_sizeare rejected for REST tables.CREATE OR REPLACE INDEXTableIndexAlreadyExists(error code 7); a non-atomic drop+create pair is not offered under OR REPLACE.BUILD INDEXDROP INDEXDropTableIndex(LanceNamespace.java:445); still uses the durable job record.lance_index_segmentsTVFMonitoring and provenance: progress is polled via
ListTableIndices/DescribeTableIndexStats(the spec-sanctioned channel). If the server returns atransaction_id, it is persisted as the REST job's only server-side evidence (§10). The pinned request model cannot carry a client operation ID or transaction properties; adding a namespace-level idempotency/provenance key is listed as a spec-evolution follow-up.(Alternative considered: make Namespace spec/API changes — replace semantics, build parameters, incremental build — an explicit prerequisite and offer uniform REST behavior. The matrix above is the Phase-1 proposal; the spec-evolution path can be a follow-up with the Lance community.)
6. Initial type and property matrix
index_type=IVF_PQ; exactly one fixed-size-list column; element types FLOAT16, FLOAT32 (others deferred, see corrections); field and elements non-null; metrics L2/COSINE/DOT;num_partitions,num_sub_vectorsoptional positive integers (num_sub_vectorsmust divide the dimension);num_bitsfixed to 8.zone_sizepositive integer (Lance default 4096,DEFAULT_BTREE_BATCH_SIZE, rust/lance-index/src/scalar/btree.rs:91).7. Logical and physical metadata surfaces
Index_typereports the physical Lance type (IVF_PQ,BTREE,BITMAP) rather than the SQL category.Propertiescarries bounded logical details only (physical type, metric, parameters, aggregate indexed/unindexed rows and fragments from the logical description) — never segment arrays. The logical index count is the number of user-visible logical index descriptions (i.e., distinct grouped names), excluding Lance system indexes, and is not derived from physical manifest entries; no separate count statement is added.lance_index_segmentsTVF: catalog, database, table, logical index name, segment UUID, physical index type, dataset version, creation timestamp when available. Per-segment row/fragment counts are deferred (see corrections): the lance-c list JSON carries per-entry{name, uuid, columns, type, dataset_version}and no fragment coverage or row counts; this becomes lance-c extension item ⑥.8. Authorization and identifier semantics
Unchanged from v2: mutation + CANCEL require table-level ALTER; SHOW surfaces require table-level SHOW. Doris resolves user-supplied index names case-insensitively (quoted and unquoted identifiers alike) and preserves display case; new case-only duplicates are rejected; pre-existing external collisions surface an explicit ambiguity error on mutation. Note the layered reality (corrected research): Lance itself is case-sensitive and exact-matching (create.rs:202, api.rs:211), so Doris's case-insensitive resolution is a Doris-layer policy over exact-match storage — which is exactly why external case-only collisions are a real state that must be handled explicitly.
9. Versioning, concurrency, and visibility
10. Failure recovery, provenance, and metadata refresh
The three outcome classes from v2 are retained, with a strengthened and honestly-bounded success criterion.
Confirmed pre-commit failure
Unchanged: job FAILED; no post-DDL refresh; unreferenced artifacts reclaimed by Lance cleanup per retention.
Confirmed commit — requires job-specific evidence
operation_idin the Lance transaction properties via the general Transaction path (§5; runtime-verified readable back from the new version's transaction).DropIndexoperation class, and the one-shotDataset.dropIndexcommits internally withNoneproperties (rust/lance/src/index.rs:1177-1194), so it is not used for provenance commits. A provenance-carrying drop goes through the same general Transaction path, expressed asoperation.CreateIndexwith an empty new-index list andremovedIndicesset to the index metadata loaded for the name before dispatch (withRemovedIndices, operation/CreateIndex.java:92-94) — mirroring the Rust one-shot's internal operation — withtransactionPropertiescarrying the job'soperation_id. That recordedoperation_idis the job-specific evidence; the exact removed segment UUID set is further corroboration. The postcondition "name is absent" is corroboration only — an external writer can independently drop the same name (false positive) or recreate it after our drop (false negative). Without transaction-properties evidence, a Directory drop follows the unknown-outcome rule below.transaction_id(persisted when returned), matched via the generic transaction API (DescribeTransaction, LanceNamespace.java:754) where the server supports it. A matching logical definition observed through list/stats is corroboration only.On a confirmed commit, Doris invalidates/refreshes metadata and emits the external-DDL refresh edit log; a refresh failure transitions the job to COMMITTED_REFRESH_PENDING with retry (unchanged).
Unknown or post-commit failure
Reconcile by reopening the latest snapshot / querying the Namespace state, as in v2 — but if no job-specific evidence is available (expected: FE failover before persisting the returned transaction_id; a drop without transaction-properties evidence; any REST operation on a server that returns no transaction_id), the job remains OUTCOME_UNKNOWN with a diagnostic. It is never marked successful from a matching postcondition alone — this is required precisely because same-name CreateIndex is last-committer-wins for external writers.
A reconciled IF NOT EXISTS / IF EXISTS no-op still invalidates/refreshes Doris metadata when newer external state is observed (unchanged).
11. Test plan
v2 coverage, plus:
operation_idin transaction properties (read back from the new version's transaction); commit-with-lost-response reconciled via UUID set; drop commits are expressed asoperation.CreateIndexwithremovedIndicesover the general Transaction path (verified through JNI) and reconciled only via transaction-properties evidence; matching-postcondition-without-evidence stays OUTCOME_UNKNOWN (including REST with no transaction_id returned);12. Confirmed design decisions and implementation decomposition
Transaction.Builder.transactionProperties+operation.CreateIndex+CommitBuilder); one-shot convenience APIs are not used where provenance is required.operation_idpersisted pre-dispatch; success requires job-specific evidence; otherwise OUTCOME_UNKNOWN. REST boundary per §10 is stated explicitly.index_type=IVF_PQ; OR REPLACE = full rebuild; BUILD = incremental; SHOW Index_type = physical type.Work items (dependency-ordered; deliverable as separate PRs):
Questions for reviewers
fe/fe-sql-parseron master vsfe/fe-coreon branch-4.1; both verified). Should [Feature](lance) Add vector and scalar index lifecycle management #66497 implementation target branch-4.1 only, master-first with a 4.1 backport, or dual-track?USING ANN), I propose adopting the internal ANN property keymetric_type(verified required byAnnIndexPropertiesChecker). One caveat: internal ANN currently accepts onlyl2_distanceandinner_product— COSINE would be a newly introduced value for this key, and Lance value names differ (l2/cosine/dot). Options: (a)metric_typewith Lance value names (l2/cosine/dot), documented as Lance-table-specific values; (b)metric_typewith Doris value names mapped to Lance (l2_distance→L2,inner_product→DOT, plus newcosine→COSINE); (c) fully Lance-native keymetric. My preference is (a) for key consistency with internal ANN while keeping Lance-native values. Build parameter names stay Lance-native (num_partitions,num_sub_vectors,num_bits) in all options.References
docs/src/format/table/transaction.md:162-194); implementation conflict path:rust/lance/src/io/commit/conflict_resolver.rs:577-617(pinned v9.1.0-beta.3)Dataset.java(:590/:607/:1106/:1176),IndexOptions.java(:135-157),Index.java(:187-258),Transaction.java(:176-179),operation/CreateIndex.java(:92-94),CommitBuilder.java(:75,:273); JNI:lance-jni/src/transaction.rs:830-846,lance-jni/src/blocking_dataset.rs:1218-1243include/lance/lance.h); upstream lance-c v0.1.3 segment-selection APIsCancelBuildIndexCommand.java(:80),ShowBuildIndexCommand.java(:68-74,:188-191),ShowIndexCommand.java(:52-66),DorisParser.g4(master fe-sql-parser :392-393/:644-646; 4.1 fe-core :390-391/:640-642),AnnIndexPropertiesChecker.java(:42-46)Gabriel39 commentedon Aug 7, 2026
Thanks for the v3 revision. After checking the confirmed 4.2 release boundary and re-reading the baseline implementation in #65730, I need to correct part of my previous review and narrow the proposal.
There are two confirmed product constraints:
These constraints materially change the appropriate Phase-1 design.
Query syntax: keep the existing vector_search() contract
I rechecked #65730. The baseline deliberately exposes Lance vector search only through the
vector_search()TVF:#65730
The implementation and regression tests use:
There is no implementation or stated plan in the baseline to translate internal-table ANN query forms such as
l2_distance_approximate(...)orinner_product_approximate(...)into a Lance search.Therefore:
vector_search().USING ANNas a category in CREATE INDEX is only DDL/parser reuse; it does not imply query-syntax reuse."metric" = "l2|cosine|dot"(option c), rather than introducingmetric_typewith internal-table values.Query behavior is relevant to this issue only as end-to-end verification: an index created by the lifecycle API must be consumed correctly by the existing
vector_search(..., "use_index"="true")path, with a compatible metric and correct results. This issue should not redesign the query syntax or optimizer rules.Scope the implementation to the existing lance-c 0.1.2 APIs
The pinned lance-c 0.1.2 already exposes the one-shot lifecycle APIs required by the original issue:
lance_dataset_create_vector_indexlance_dataset_create_scalar_indexlance_dataset_drop_indexlance_dataset_index_countlance_dataset_index_list_jsonhttps://github.com/lance-format/lance-c/blob/v0.1.2/include/lance/lance.h#L490-L541
The 4.2 design should use those existing APIs, with the heavy direct-Dataset operation executed on a selected BE for Directory Catalog, and use the existing Namespace operations for the supported REST cases. The limitations of the one-shot API—no native progress reporting, cooperative cancellation, distributed fragment build, or job-specific transaction provenance—should be documented honestly rather than solved by expanding lance-c in this release.
The following v3 items depend on new lance-c capabilities and should be removed from the 4.2 implementation scope:
In particular, the original #66497 scope requires replacing or rebuilding an index with the same name. The existing
replace=truepath provides a full rebuild. IncrementalBUILD INDEXwas added during review, but it is not required by the original issue and should be deferred because the intended distributed/incremental implementation requires APIs outside the 4.2 boundary.Revised Phase-1 lifecycle
For 4.2, I recommend limiting the lifecycle to:
CREATE INDEXfor the verified vector and scalar types;CREATE OR REPLACE INDEXmapped to the existing full-buildreplace=truebehavior;SHOW INDEX, using the existing list/count metadata and grouping physical entries by logical name where necessary;DROP INDEX;Do not promise a reliable native CANCEL operation if the underlying call cannot be interrupted. Likewise, SHOW should not promise coverage fields that require a new lance-c API. UUID, columns, type, and dataset version are already available and satisfy the original metadata scope.
The query-side acceptance test should be limited to proving that a created IVF_PQ index is used by the existing
vector_search()path and that scalar indexes are consumed by the existing Lance predicate path. FTS SQL design, internal ANN query syntax, distributed recall/model strategy, and future physical merge behavior are outside this issue.Answers to the v3 questions
metrickey and values (l2,cosine,dot), matchingvector_search(). Do not require internal ANN query/property syntax compatibility.With these scope corrections, v3 should be simplified substantially. The goal of this issue is index lifecycle management using the capabilities already present in the 4.1 baseline and pinned SDKs, not a distributed index-build framework or a unified internal/external vector-query syntax.
u70b3 commentedon Aug 7, 2026
Quick update on the prerequisite track (v3 §12 decision 2 / Q3): the upstream engagement has started — I've filed lance-format/lance-c#55, an RFC covering all six lance-c extensions, split into three independently-mergeable waves:
pb::IndexMetadatabytes as the BE→FE transfer format).One finding worth surfacing here, since it de-risks the schedule: the Rust core (9.1.0-beta.3) already implements nearly everything behind the C boundary.
CreateIndexBuilderalready carriesfragments/index_uuid/train/progress/transaction_propertiesandexecute_uncommitted() -> IndexMetadata(rust/lance/src/index/create.rs:50-141), with scalar fragment-scoped builds supported as well (:270); the injection pointsIvfBuildParams.centroidsandPQBuildParams.codebookboth exist. The only genuine core-side gaps are the cancellation hook and per-segment listing stats — the rest is FFI exposure.This does not presume the Q3 answer: if round-3 prefers a different tracking form, the upstream issue adapts. Round-3 feedback welcome whenever convenient — no urgency imposed by this step.
Gabriel39 commentedon Aug 7, 2026
Thanks for starting the upstream discussion. The Rust-core findings are useful for future distributed-index work, and lance-format/lance-c#55 can remain as that future track.
However, this does not change the confirmed scope for Doris 4.2, and Q3 is no longer open: lance-c extensions are not a prerequisite for #66497 in 4.2. Features that depend on those extensions must be deferred. Therefore, Wave 1 should not be described as blocking the 4.2 implementation or as de-risking its current schedule; it is outside the release's critical path.
For #66497, the next design revision should be simplified around the existing branch-4.1 baseline and the pinned lance-c 0.1.2 one-shot APIs:
lance_dataset_create_vector_indexandlance_dataset_create_scalar_index;replace=truepath;lance_dataset_index_countandlance_dataset_index_list_jsonfor the currently available metadata;lance_dataset_drop_index;The 4.2 design should explicitly exclude fragment-scoped distributed builds, shared-model distribution, BE-to-FE uncommitted segment transfer, coordinator segment commit, incremental
BUILD INDEX, physical merge, native progress, cooperative cancellation, and per-segment coverage/statistics extensions. It should also avoid promising reliableCANCEL BUILD INDEXsemantics when the underlying one-shot native call cannot be interrupted.The REST path is separate: lance-c additions do not expand REST Catalog capabilities. REST index lifecycle operations must continue to use the pinned Namespace request/response models and the server's implemented capabilities. In particular, newer lance-c APIs would not provide REST
OR REPLACE, incremental build, cancellation, or additional request parameters.One future-facing note for lance-c#55:
pb::IndexMetadatabytes are not directly accepted by the pinned Java SDK.Index.Buildertakes the metadata as individual fields and exposes no publicparseFrom(byte[])entry point, so the cross-SDK design still needs an explicit Java decoder/adapter and a version-compatibility contract. This does not need to be solved for #66497 because that distributed path is out of scope for 4.2.Please make the next revision a 4.2 design based only on the existing APIs, covering BE selection/execution, supported index types and properties,
replace=truesemantics, privileges, concurrency, versioning, failure behavior, metadata refresh, local-path visibility, and the restricted Namespace 0.7.7 REST capability matrix. Query-side work should remain limited to validating index consumption through the existingvector_search()and scalar predicate paths.u70b3 commentedon Aug 7, 2026
Revised Design Proposal v4 — Lance Index Lifecycle, scoped to the 4.2 boundary
/cc @Gabriel39 @zhangstar333
Major changes from v3
lance_dataset_create_vector_index/create_scalar_index/drop_index/index_count/index_list_json, lance.h@v0.1.2 L490-541); REST keeps the v3 capability matrix on the pinned Namespace 0.7.7 model. No new native capability is required anywhere.BUILD INDEX(incremental) removed from 4.2 — it was added during review and is not required by the issue; its intended implementation needs APIs outside the 4.2 boundary. The statement is rejected for Lance tables with an explicit error (§2).OUTCOME_UNKNOWN+ diagnostics. This is the proposed 4.2 trade-off, with the upgrade path tracked in §9.vector_search(..., "use_index"="true")path (branch-4.1VectorSearchTableValuedFunction.java:81,158-159) with compatible metric and correct results, and that scalar indexes are consumed by the existing Lance predicate path ([feature](lance) support lance catalog in doris #65730). No query-syntax or optimizer work.metricwith valuesl2|cosine|dot, matching the existing query surface (option c).USING ANNremains DDL category reuse only.lance_index_segmentsTVF deferred: its distinct value was per-segment coverage, which is exactly the deferred capability; the remaining columns duplicate SHOW INDEX's grouped output. It returns with the future-work track (§9).1. Goals and Phase-1 boundary (4.2)
Provide the issue's lifecycle over the pinned SDKs, nothing more:
CREATE INDEXfor IVF_PQ vector indexes and BTREE/BITMAP scalar indexes (Directory full; REST per §4 matrix);CREATE OR REPLACE INDEX= full rebuild via the existingreplace=truebehavior;SHOW INDEX— logical definitions, names, UUIDs, columns, types, dataset versions (grouped by logical name where a table carries multiple physical entries per name; REST fields limited to the §4 stats model);DROP INDEXby name (Directory and REST);Explicit non-goals for 4.2 (each mapped in §9): fragment-scoped distributed builds, shared-model distribution, uncommitted segment transfer, coordinator commit of worker segments, physical segment merge, native progress/cancellation, per-segment coverage/statistics, incremental BUILD, FTS SQL semantics, internal-table ANN query syntax compatibility.
Historical-version tables remain read-only for index mutation; inspection uses the selected snapshot, mutation targets only the latest writable state.
2. SQL surface
CREATE INDEX idx ON t (embedding) USING ANN PROPERTIES ("index_type"="IVF_PQ", "metric"="l2", "num_partitions"="256", "num_sub_vectors"="16")."metric"acceptsl2|cosine|dot(Lance-native values, matchingvector_search()). Unknown keys/values are rejected at analysis.USING BTREE/USING BITMAP(optional"zone_size"for BTREE per the pinned Lance default, unchanged from v3).num_sub_vectorsmust divide the dimension;num_bitsfixed to 8; BTREE/BITMAP column-type lists per v3 §6.CREATE OR REPLACE INDEX= full rebuild + atomic same-name replacement viareplace=true.SHOW INDEX FROM t;DROP INDEX [IF EXISTS].SHOW BUILD INDEX FROM db WHERE ...;CANCEL BUILD INDEX ON ctl.db.tbl (job_id)— retained (builds are async jobs), semantics per §5.BUILD INDEXis rejected for Lance tables in 4.2 with an explicit unsupported-operation error naming the deferral — not silently accepted, not partially implemented.Statement semantics: create/replace/drop each return after a durable job is accepted.
IF NOT EXISTS/IF EXISTSreconciliation compares name + column + physical type (Directory) — definition fields not readable back through the pinned list JSON (metric, build parameters) do not participate, so a same-name/column/type index with different parameters is treated as existing; this limitation is documented; REST compares only stats-observable fields per §4. OR REPLACE remains convergent-but-not-idempotent. Single column per index.USING ANNis DDL/parser category reuse only — it implies nothing about query syntax. Carried over unchanged from prior revisions: conflict-retry decisions are never made by parsing native exception strings;IF NOT EXISTSandOR REPLACEare mutually exclusive;USINGis required for Lance tables (no implicit default index type).3. Neutral command model and dispatch
Unchanged from v3:
ParsedIndexSpec→ resolve catalog/database/table →InternalIndexProviderorLanceIndexProvider→ provider validation → internal schema change or Lance external-index job. Lance definitions never enter Doris internalcatalog.Indexserialization. Implementation targets branch-4.1 only.4. Execution model and REST capability matrix
Directory Catalog: one-shot build on a selected BE
FE admits the job (§5), selects a BE (any healthy BE for object-storage datasets; the local/file restriction below constrains the choice for local datasets), and dispatches one task. The BE opens the dataset through lance-c v0.1.2 and executes the matching one-shot call, which trains, builds, and commits internally as a single dataset version. FE then refreshes metadata (§6). There is no FE-side commit in 4.2.
Documented limitations of this execution form (honest, not solved in 4.2): no native progress reporting; no cooperative interruption; whole-dataset build in one BE process (memory/compute on a single node); commit carries no job-specific provenance (§6). These are exactly the capabilities tracked upstream in §9.
Local/file restriction (unchanged from v3): a local/file dataset is mutable only when the path is visible with identical contents to the selected executor and to every FE that may reconcile or finalize the operation; otherwise mutation is rejected.
REST Namespace Catalog: capability matrix (rows unchanged from v3; the TVF row is subsumed by the global TVF deferral, change 7)
Behavioral detection via typed
UnsupportedOperationException(spec error code 0); unsupported combinations fail at analysis where statically known, otherwise with a clear capability error; Doris never falls back to direct Dataset mutation with vended credentials.SHOW INDEXListTableIndices+DescribeTableIndexStats; fields limited to the stats model (name, UUID, columns, status; type, distance, row counts where offered).CREATE INDEXCreateTableIndexRequestcarries (column, index_type, name, distance_type; FTS fields unused). IVF build properties and BTREEzone_sizeare rejected for REST tables.CREATE OR REPLACE INDEXBUILD INDEXDROP INDEXDropTableIndex; durable job record applies.Monitoring via the spec-sanctioned
ListTableIndices/DescribeTableIndexStatspolling;transaction_idpersisted when returned (§6).5. Durable jobs, SHOW/CANCEL integration, admission
Job record (edit-log persisted; simplified from v3): job id, operation kind (CREATE / REPLACE / DROP), catalog/db/table identity, dataset URI or Namespace table id, logical index name and normalized definition, starting dataset version, state, timestamps, sanitized failure information, Namespace transaction id when returned. No credentials — obtained/refreshed at dispatch.
States (simplified — the one-shot call fuses build and commit):
PENDING → RUNNING → REFRESHING → FINISHED, plusFAILED / CANCELLING / CANCELLED / COMMITTED_REFRESH_PENDING / OUTCOME_UNKNOWN.CANCELLING → CANCELLEDcompletes once the fence is persisted. FE failover reconstructs unfinished jobs from the edit log and resumes polling or refresh; it does not blindly re-dispatch a build (§6).SHOW/CANCEL BUILD INDEX integration as in v3 §4 (registry, proc union with
JobType, catalog qualification, privileges: SHOW requires table-level SHOW, CANCEL requires table-level ALTER), minus any field that would require a new lance-c API; theProgresscolumn for one-shot jobs is a coarse mapping from job state — no native build progress exists in 4.2.Cancellation semantics (honest): CANCEL persists intent and fences Doris-side tracking, polling, and refresh. It cannot interrupt the native one-shot call. A fenced build may still complete and commit externally; the job's terminal state remains
CANCELLED, and Doris performs no further observation of the fenced job — the externally committed index is picked up by the next reconciliation or metadata refresh (a subsequent DDL statement, manualREFRESH, or cache invalidation on access), and theDROP INDEXthe operator issues performs that reconciliation itself. This is documented as best-effort cancellation, not abort.Resource admission: per-catalog and cluster-wide concurrency limits on running build jobs, unchanged in intent from v3.
6. Failure recovery, provenance, and metadata refresh (rewritten for 4.2)
The v3 outcome classes are retained; the evidence standard is restated for what the pinned one-shot APIs can actually provide.
FAILED; no refresh; unreferenced artifacts reclaimed by Lance cleanup per retention.COMMITTED_REFRESH_PENDINGwith retry. In the failover flow (completion observation lost before the terminal state was persisted), REST confirmation additionally requires the persistedtransaction_id; a list/stats postcondition match alone remains corroboration only, per the rule below.operation_idand Doris pre-assigns no segment UUID, a matching postcondition (index with expected name/column/type at a newer dataset version) is corroboration only — an external writer can independently create or replace the same name (Lance same-name CreateIndex is last-committer-wins). Without job-specific evidence the job remainsOUTCOME_UNKNOWNwith a diagnostic naming the ambiguity; it is never marked successful from a matching postcondition alone. For REST, the server-returnedtransaction_id(when present) remains the only job-specific evidence, matched viaDescribeTransactionwhere supported.IF NOT EXISTS/IF EXISTSno-op or anOUTCOME_UNKNOWNdiagnosis — still invalidates/refreshes Doris metadata, so a diagnosis never leaves a stale cache behind.OUTCOME_UNKNOWN: such a job never retries automatically. The operator inspects actual state viaSHOW INDEX(or the dataset directly), then either issuesDROP INDEXif the mutation landed or re-issues the statement if it did not —CREATE OR REPLACE INDEXis the recommended re-issue form for create/replace ambiguities, converging manually to a known state.This is a deliberate, stated weakening relative to v3 (which carried deterministic UUID-set + transaction-properties evidence). It follows from the confirmed 4.2 scope: the only remedies live in the deferred capabilities, and §9 names the upgrade path.
7. Metadata surfaces and authorization
SHOW INDEX: unchanged 13-column schema (branch-4.1ShowIndexCommand.java:52-66); one row per logical index/column, grouping same-named physical entries;Index_typereports the physical Lance type (IVF_PQ/BTREE/BITMAP);Propertiescarries bounded logical details only — physical type; segment count; metric and build parameters where known from the creating job record (on Directory these are not readable back through the pinned list JSON, so for externally created indexes they stay empty; on REST,distance_typeis populated from the stats model where offered, and build parameters are always empty — they are inexpressible there) — never segment arrays, no coverage fields. The logical index count is the number of distinct grouped names, excluding system indexes. Field provenance is stated per catalog kind: Directory — UUID, columns, type, and dataset version come from the existingindex_list_jsonand satisfy the issue's metadata scope; REST — the pinned stats model exposes name, UUID, columns, status, and (where offered) type/distance/row counts only — no dataset version, so the corresponding SHOW fields stay empty (spec.yaml:3162-3184, :3206-3234).use_index=false.8. Test plan (4.2)
vector_search(..., "use_index"="true")with a compatible metric and returns correct results; scalar indexes are consumed by the existing Lance predicate path; equivalence with index use disabled;COMMITTED_REFRESH_PENDING), admission limits enforced;OUTCOME_UNKNOWN(Directory and REST-without-transaction_id);9. Moved to future work — tracked, not dropped
All removals land in one tracked place: lance-format/lance-c#55 (upstream RFC, filed 2026-08-07) plus a Doris-side follow-up issue to be opened when the 4.2 branch cuts. Every deferred item below is a Directory-path capability — per round-3 follow-up, lance-c additions do not expand REST Catalog capabilities: REST
OR REPLACE, incremental build, cancellation, and additional request parameters require Lance Namespace spec evolution, tracked separately as a spec-level follow-up with the Lance community; nothing in lance-c#55 expands the §4 REST matrix. The mapping:pb::IndexMetadatabytes; cross-SDK consumption requires an explicit Java decoder/adapter from bytes toIndex.Builderper-field setters, plus a version-compatibility contract — the pinned SDK has noparseFrom(byte[]); per reviewer note, issuecomment-5212293290)BUILD INDEXCorrection to my earlier cross-link comment (issuecomment-5212075800), per round-3 Q3: nothing in lance-c#55 gates this issue — the RFC continues as the post-4.2 distributed track, and its wave split was designed for exactly this decoupling. When the deferred track resumes, v3's provenance design (pre-assigned UUID sets + transaction-properties commits) is reinstated on top of E1/E2, closing the §6 gap.
10. Work items (4.2; branch-4.1; deliverable as separate PRs)
metrickey; rejection of REST-inexpressible properties) + explicitBUILD INDEXrejection for Lance;OUTCOME_UNKNOWNboundaries, single-node whole-dataset build).Round-3 answers adopted
metricwithl2|cosine|dot, matchingvector_search(); no internal ANN query/property syntax compatibility introduced. ✔One clarification on scope wording: v3 had gated the production path on extended lance-c because its target was the distributed build form; with 4.2's production form redefined as the one-shot selected-BE execution, no gating remains. The §6 provenance weakening is the only deliberate technical downgrade, and it is stated rather than hidden.
85 remaining items
branch-4.1: [feature](lance) Add Lance index admission, job inspectio…
[fix](lance) Settle job reports atomically and refresh fairly
[fix](lance) Settle job reports atomically and refresh fairly
Gabriel39 commentedon Oct 8, 2026
After reconsidering the implementation against the execution model of Iceberg
rewrite_data_files, please simplify the design and implementation for this issue. This changes the previously accepted v5.1 direction: a durable asynchronous Lance index job framework should no longer be a requirement for the initial implementation.1. Use synchronous SQL with bounded worker execution
The SQL statement should validate the request, dispatch resource-isolated work to BE, wait for completion, refresh metadata after a confirmed commit, and return the result. Internal parallel execution does not require an asynchronous SQL/job interface.
Keep heavy Lance work outside FE and the BE main process, with hard worker resource limits, bounded concurrency/queueing, deadlines, and reliable child cleanup. These protections are independent of durable job management and should remain.
2. Revert the job infrastructure already introduced
Please prepare an explicit cleanup/revert on
branch-4.1, rather than leaving the old framework disabled or maintaining two execution paths:SHOW LANCE INDEX JOB(S), and catalog guards tied to unresolved jobs. Preserve and adapt the useful privilege, schema, parameter, snapshot, and IF-condition validation for synchronous execution.Keep the read-only
SHOW INDEXand physical-index inspection functionality, and the reusable target-aware DDL validation. Please update the issue design and PR roadmap to reflect this scope change.3. State the simpler failure contract explicitly
The initial version does not promise persistent job history, FE failover recovery, resumable builds, or durable same-name serialization across failures.
A timeout, disconnect, worker loss, or lost response after dispatch must not be presented as proof that nothing committed. Return an explicit indeterminate-outcome error when applicable, do not automatically retry a potentially committed mutation, and let the user inspect authoritative index metadata. Observing a same-name index does not prove which request created it.
Cancellation cannot undo an existing commit. Worker capacity must remain accounted for until termination is confirmed. A confirmed commit followed by metadata-refresh failure must be reported distinctly from a failed build.
4. Keep a path to distributed construction
Synchronous SQL does not preclude distributed index construction. The desired extension is:
pin snapshot -> partition fragments -> parallel BE builds of uncommitted segments -> validate/collect outputs -> one coordinated final commit -> refresh -> returnPlease keep build and commit responsibilities separable. Verify the APIs available in the pinned Lance bindings and add the necessary adapter support before implementing this distributed path; do not run several complete, independently committing
create_indexcalls against the same logical index name.The initial implementation may use one worker. Multi-BE construction can follow without introducing durable jobs; persistent recovery should be a separate, justified follow-up requirement rather than a prerequisite for index lifecycle support.
u70b3 commentedon Oct 8, 2026
Agreed — accepted, I'll rework this to the synchronous model.
The synchronous shape was my starting point when drafting the original design, simply because it is easier to implement and verify. The durable async framework came in only to keep post-dispatch ambiguity safe across failover, and its complexity — fences, quotas, replay, force-release — has outgrown that value. The synchronous path is also a much smaller diff to review and leaves fewer corners for hard-to-debug failure modes.
Concretely:
[refactor](lance) Remove the durable Lance index job framework (apach…
[refactor](lance) Remove the durable Lance index job framework (apach…
[refactor](lance) Remove the durable Lance index job framework (apach…
[refactor](lance) Remove the durable Lance index job framework (apach…
[refactor](lance) Remove the durable Lance index job framework (apach…
branch-4.1: [refactor](lance) Remove the durable Lance index job fram…
branch-4.1: [refactor](lance) Remove the durable Lance index job fram…
u70b3 commentedon Oct 14, 2026
This supersedes the v5.1 proposal following the synchronous-direction review. The baseline facts — the commit-resolved pinned revisions, panic behavior at the FFI boundary, the binding entry points, and the Rust-to-C error mapping — were re-verified against the pinned sources, and every execution parameter the direction comment left open was resolved to the reading most consistent with it. Section 11 records that evidence and the single remaining decision (client-disconnect semantics), and asks for confirmation of the 4.2 boundary.
Design Proposal v6 — Synchronous Lance Index Lifecycle for Doris 4.2
0. Authority and revision summary
This standalone document defines the capability and safety contract for Lance index lifecycle management in Doris 4.2 under a synchronous execution model. If accepted, it supersedes the v5.1 minimal one-shot proposal.
Context for this revision
On 2026-10-08, after reconsidering the implementation against the execution model of Iceberg
rewrite_data_files, the reviewer changed the previously accepted v5.1 direction: a durable asynchronous Lance index job framework is no longer a requirement for the initial implementation. The SQL statement itself validates the request, dispatches resource-isolated work to a BE, waits for completion, refreshes metadata after a confirmed commit, and returns the result. Internal parallel execution does not require an asynchronous SQL/job interface.The job infrastructure already introduced on
branch-4.1— the durable job state machine, manager, persistent same-name fences, unresolved-job quotas, and job replay of #67235, plus the job-specific parts of #67630 (durable job creation and JobId results,SHOW LANCE INDEX JOB(S), catalog guards tied to unresolved jobs) — is reverted explicitly rather than left disabled or maintained as a second execution path. The reusable privilege, schema, parameter, snapshot, andIF-condition validation, the read-onlySHOW INDEX, and the physical-index inspection are preserved and adapted for synchronous execution. The worker resource protections — heavy Lance work outside FE and the BE main process, hard worker resource limits, bounded concurrency/queueing, deadlines, and reliable child cleanup — are independent of durable job management and remain requirements.Relative to v5.1, this revision retains: the SQL surface and type/property matrix, the authoritative one-snapshot metadata contract, admission-time authoritative preflight and
IFbehavior, dataset-version and schema-contract revalidation before the mutating native invocation, hard worker process isolation, typed provider-result classification, and secret handling. It removes: durable job records, the compactPENDING -> RUNNING -> ...lifecycle, same-name fences, unresolved-job quotas,FORCE_RELEASE, job inspection SQL, and replay/failover recovery. TheUNKNOWNconcept survives only as a client-visible indeterminate-outcome error class, never as a persisted state.Baseline
The baseline is resolved at commit level, because package versions and release tags alone do not determine API or error semantics: Doris
branch-4.1atc8c0ddca4d; FE Java bindinglance-core12.0.0; BE C bindinglance-cat commit98468344(its package version string still reads0.1.9), whose Cargo manifest pins the Rust core at68c12dfd. The BE binding is identified by the exact source URL recorded inthirdparty/vars.shat the stated Doris commit — not by its version string or tag. Any future statement about "what the pinned bindings do" resolves the vars.sh-to-commit-to-manifest chain before asserting a fact.No 4.2 guarantee depends on upgrading or extending those APIs; Section 8 records which extension capabilities the pinned bindings do and do not expose.
Normative terms use MUST, MUST NOT, SHOULD, and MAY in their ordinary requirements sense. Sections explicitly marked future work are non-normative for 4.2.
1. Scope and release boundary
1.1 Core safety contract
Doris 4.2 executes one Lance mutation inside a single synchronous SQL statement without retrying after dispatch may have occurred, fabricating an outcome, or implying that nothing committed when the outcome is uncertain. Mutation outcome and metadata refresh are independent dimensions. A mutation statement returns exactly one of four outcome classes:
A client that cannot receive a result packet — a timeout with a lost response, a disconnect, or FE loss — treats the outcome as indeterminate. This is a documentation and driver rule (Sections 5.5 and 6.2), because no SQL error can be delivered on a closed connection.
1.2 Required in 4.2
The 4.2 deliverable includes:
CREATE INDEX,CREATE OR REPLACE INDEX(full same-name rebuild), andDROP INDEXby logical name, for the initially verified vector and scalar index types, executed synchronously within the statement;SHOW INDEXfor logical index metadata and thelance_index_entriesTVF for physical index UUID and dataset version (both unchanged from the merged read-only work);IF NOT EXISTSandIF EXISTSbehavior based on authoritative Lance metadata;SHOWandALTERauthorization;IF-condition checks before any dispatch, with the final authoritative preflight inside the master's name entry (Section 4.3);Lance manifests remain the authoritative index metadata. Doris persists no mutation control state: no job record, no fence, no quota book, no replay.
1.3 Explicitly deferred from 4.2
These exclusions are normative and cannot gate 4.2:
UNKNOWNoperational protocol —SHOW LANCE INDEX JOB(S),RESOLVE ... FORCE_RELEASE, audited release notes, and late-commit warnings attached to records;UNKNOWNwatchers or reconciliation daemons, idempotency protocols, automatic retry of dispatched mutations, incrementalBUILD INDEX, coverage repair, build progress, and continuously appended-table freshness SLAs;countRows()surfaces;2. User-visible contract
2.1 SQL surface
USING ANNis a neutral DDL category. It does not add or imply Doris internal ANN query syntax for Lance. Lance vector queries continue to usevector_search().CREATE OR REPLACEmaps toreplace=trueand fully rebuilds the logical name. It converges by name but is not idempotent: another invocation may produce a different UUID and dataset version. It is mutually exclusive withIF NOT EXISTS;USING, target column, and required properties are validated before dispatch.Only top-level CREATE, REPLACE, and DROP mutate Lance indexes in 4.2;
ALTER TABLE ... ADD/DROP INDEXandBUILD INDEXremain unsupported. Internal tables are unchanged.2.2 Synchronous execution semantics
All Lance index mutations are forwarded to and executed on the active master FE. An admitted mutation executes within the statement and returns when one of the Section 1.1 outcome classes is known:
The statement returns no job id and exposes no job surface. A client that loses the connection, times out, or kills the statement follows Sections 5.5 and 6.2 and MUST NOT blindly resubmit.
2.3 Submission and
IFbehaviorAdmission reads authoritative metadata before any dispatch:
SHOW INDEXexposes a stable value; the exposed mappings aremetricagainstmetric_type,num_sub_vectorsagainstcompression.num_sub_vectors, and the effectivenum_bits=8againstcompression.num_bits.num_partitionsis not reliably exposed and is therefore not compared, and no substitute value (for exampletarget_partition_size) may stand in for it. No additional canonicalization or metadata read is a prerequisite;Two ordering rules close the preflight races. First, the final authoritative preflight runs after the active-name entry is acquired on the master, immediately before dispatch admission: a preflight taken earlier — before forwarding, before queueing, or before the entry — is not sufficient to dispatch, so a request that queued behind another same-name statement cannot proceed on a stale snapshot. Second, an authoritative no-op completes its required bounded cache invalidation before returning (Section 6.4), so a stale cache cannot contradict the returned state; the invalidation closure and its residual failure class are defined there.
Authoritative preflight is not an external lock. A post-preflight race against an external writer is classified from the result rules in Section 6.5; Doris never turns a generic post-invocation error into an IF no-op by inspecting message text.
2.4 Supported type and property matrix
IVF_PQFLOAT16orFLOAT32elementsindex_type=IVF_PQ;metricisl2,cosine, ordot; positive requirednum_partitionsandnum_sub_vectors; subvectors divide the vector dimension; configured static bounds apply;num_bits=8is fixedBTREEparams_json=NULLBITMAPparams_json=NULLFor CREATE and REPLACE, Hamming distance, nullable/unsupported nested fields,
FLOAT64,UINT8, orINT8vector elements, composite indexes, arbitrary scalar JSON, and unknown properties or values fail before dispatch — these admission limits never restrict what can be dropped by name. FE enforces static syntax, type, and bounds; the worker repeats the independently checkable facts against the admitted snapshot before the mutating native invocation.Vector element nullability is not a schema-level admission check: the manifest has no slot for element nullability, and the pinned Java SDK reconstructs every fixed-size-list element as nullable regardless of what was written, so element nullability is unobservable at the schema level. The two validation layers are kept separate: facts independently checkable before the call — the element type and the non-null top-level field — are checked before the mutating native invocation; validation of actual element values happens inside the provider's build, within the worker's budget, and its failures classify under the post-invocation rules of Section 6.5, never as provable pre-invocation failures.
3. Authoritative metadata contract
3.1 One pinned Directory snapshot
Every SHOW, physical inspection, IF check, and post-commit refresh uses one opened Directory
Datasetsnapshot. One snapshot does not mean one native call: the read chain issues several calls against the same opened dataset and keeps one overall deadline for the chain.__lance_frag_reuse,__lance_mem_wal) are filtered and the raw list is deduplicated by exact name;The chain bounds raw entries and logical names separately, both finite. Name occupancy for mutation is determined from the physical list together with the available logical descriptions. A legacy index whose segments all lack readable details may be omitted from logical descriptions while its name still appears in the physical list: plain CREATE rejects such a name as occupied, CREATE IF NOT EXISTS does not claim a match without a reliable definition, and DROP IF EXISTS does not claim absence from an incomplete logical list alone. Missing legacy details are not malformed metadata; malformed or oversized data still fails closed with a typed bounded error, and the two cases are never conflated. Whether SHOW continues to omit such legacy logical descriptions is a fixed, documented behavior, not an accident of the read path.
Normal inspection and FE admission MUST NOT call
Dataset.countRows().Dataset.getIndexStatistics()is also forbidden because the pinned version may migrate metadata and write a manifest. Provider-local build validation stays in the isolated worker and creates no Doris row-count surface.FE metadata reads have bounded results and deadlines through the existing bounded inspection executor (
LanceIndexInspectionExecutor, 4 concurrent / 16 queued on the baseline), which remains the read-side owner: admission reads go through it, and a statement exiting does not reclaim capacity of a read still inside JNI. Timeout does not imply JNI cancellation; ownership remains until the native call returns. A separate read helper process is not a 4.2 requirement.3.2 Logical
SHOW INDEXSHOW INDEXkeeps the existing MySQL-compatible 13-column schema and returns one row per logical index/column. For Lance:Key_nameis the exact display name;Column_namecomes from the same-snapshot field-ID resolution;Index_typeis the logical Lance algorithm such asIVF_PQ,BTREE, orBITMAP, not the SQL categoryANN;Propertiesis deterministic, valid, allowlisted JSON under a reviewed finite UTF-8 bound.There is no second logical TVF or server-side exact-count API; callers may count distinct
Key_namevalues in the bounded SHOW result.3.3 Minimal physical inspection
lance_index_entries("table"="ctl.db.tbl")returns one row per physical entry:CatalogName, DatabaseName, TableName, IndexName, IndexUuid, DatasetVersion.The TVF exists only because UUID and dataset version do not fit the established SHOW schema. It exposes no fragments, coverage, provider/consistency state, opaque details, or transaction properties.
The TVF requires one table argument, table
SHOW, and an optional ordinary predicate over the bounded result. It is all-or-error and never silently truncated.3.4 Pinned type vocabulary, system entries, and bounds
The pinned Java SDK has different logical and physical vocabularies:
IVF_PQ, while the physical listing may expose the umbrella manifest typeVECTOR;BTREEversusBTreeandLABEL_LISTversusLabelList.Authoritative IF and internal joins use one normalization rule:
VECTORonly for supported vector algorithms;SCALARonly for supported scalar algorithms;Logical SHOW displays the concrete per-name description value. The minimal physical TVF does not expose the umbrella type.
Duplicate logical names or UUID ownership, unknown field IDs, malformed or oversized data, and required logical/physical mismatches fail closed with a typed bounded error.
Directory inspection caps logical names, physical entries, external strings, fields per index, and aggregate field-name bytes. Exact values follow Doris configuration conventions. Because these post-JNI checks do not bound native allocation, focused FE stress/failure tests cover accepted and over-limit data.
4. Target identity, admission, and Doris-side guards
4.1 Stable target and logical name
The target key used for validation, DDL guards, and in-memory concurrency is:
The catalog identity is persisted configuration. The request's display name and normalized name bytes live only in bounded invocation memory for the duration of the statement and its cleanup — this contract adds no mutation persistence. Normalization v1 is the UTF-8 result of Java
toLowerCase(Locale.ROOT). Doris preserves display case, rejects new case-only duplicates, resolves a unique match to its stored name, and fails mutation on ambiguous external case-only collisions.There is no durable fence keyed on this tuple. Credential-bearing URLs are never identity; URI aliases are external writers. Replacing the dataset at the same URI concurrently with a mutation is unsupported and lands in the indeterminate class of Section 6.
4.2 Dataset-version and schema-contract revalidation
CREATE and REPLACE admission records the exact selected/latest dataset version and the following ordered contract:
normalized_typeincludes relevant parameters such as decimal precision/scale and timestamp unit/time-zone semantics. Unindexed fields are excluded.Before the mutating native invocation, the worker reopens the admitted dataset version, independently recomputes contract v1, and compares the ordered representation. An unavailable version, mismatch, or unsupported contract produces a complete pre-invocation confirmed failure with typed
STALE_ADMISSIONorUNSUPPORTED_SCHEMA_CONTRACT; the worker never switches to latest. This revalidation confirms the admission snapshot itself; it does not observe whether storage-latest has since advanced, and any post-admission race is arbitrated by the provider's commit machinery and classified under Sections 4.3 and 6.5. No additional worker-start latest-equality gate is added: this design does not require storage-latest to still equal the admitted version when the worker starts, and such a gate would turn concurrent appends that the provider can safely arbitrate into early rejections — Section 4.3 promises no such failure — while still not guaranteeing equality at final commit. The worker may still read storage-latest for result diagnostics; such reads never gate the invocation.Java admission and Rust execution share golden contract fixtures.
A DROP target is revalidated against the pinned admission snapshot: the worker reopens the admitted dataset version and resolves the normalized name uniquely to a stored name in that snapshot's bounded physical list, then issues one drop by that name. This confirms the admission snapshot only — it does not prove that storage-latest has not advanced since admission, and a concurrent external change is arbitrated by the provider's commit machinery and classified under Sections 4.3 and 6.5. No field-schema contract is built for DROP — CREATE's single-column and algorithm restrictions never limit what can be dropped, and composite, nested, or legacy indexes whose names appear in the physical list are droppable by logical name. A same-URI dataset replacement that reproduces the same version cannot be detected by this revalidation; no indeterminate-detection promise is made for it.
4.3 Same-name concurrency and admission serialization
With fences and quotas removed, same-name concurrency is handled by master-local admission plus provider-side arbitration, and nothing else:
4.4 Credentials
No component persists credentials: dispatch, authoritative reads, and refresh resolve current catalog credentials when executed.
A credential-only catalog ALTER is not target-changing and MUST remain allowed; provider, stable-locator, and selected-version changes remain guarded (Section 4.5). Known-expired credentials fail before the mutating invocation as a confirmed failure. No credential-TTL or safety-margin protocol is required. Section 7 defines secret handling.
4.5 Minimal DDL guard boundary
While a mutation executes, an in-memory active-target token on the master guards target-changing DDL: a Doris DDL that changes the persisted catalog identity, Directory provider, normalized locator, or selected-version semantics fails fast with a busy error. Credential-only ALTER, ordinary cache refresh, unrelated properties, and external appends remain allowed. No broad catalog lock is held across execution.
Three distinct owners are involved and never conflated: the per-name admission entry (Section 4.3), the short FE metadata/DDL locks held only during bounded validation and admission (Section 5.1), and the active-target token (this section). FE restart loses the token; that window is accepted and is not serialized across failures.
Target-aware routing precedes internal
catalog.Indextranslation. Internal validation/serialization is unchanged, and Lance algorithms do not extend the persisted internalIndexTypeenum.5. Synchronous execution and resource boundary
5.1 Execution flow
The execution pipeline is the one stated in Section 2.2; this section fixes its locking and resource boundaries. FE metadata/DDL locks cover only bounded validation and admission — not dispatch, native execution, waiting, metadata reads, or refresh.
5.2 Hard process isolation
The pinned C binding catches Rust panics at the FFI boundary and reports them as a typed panic error, and its build configuration rejects unwinding-incompatible abort settings at compile time. Nevertheless OOM, stack overflow, and double panics can still terminate a process, and the native runtime creates threads outside Doris
MemTrackercontrol. Production mutation therefore never runs inside FE, the BE main process, or an ordinary RPC handler.Before Lance/Rust initializes materially, the selected BE launches a same-build worker in a verified OS-enforced boundary providing:
A deployment unable to verify the boundary rejects before the mutating invocation; there is no in-process or soft-RSS fallback. Launcher, containment, environment, diagnostic, and handshake details are not public contracts.
The BE supervisor and its queues are bounded. Capacity reservation and BE selection are strictly non-invoking: they do not send the execute request, launch a worker, initialize Lance/Rust, or make native calls. Once the single execute request is sent, a complete trusted identity-matched busy or other pre-invocation rejection is a terminal confirmed failure and is never automatically retried within the same statement.
Local/
file://mutation has a separate disabled-by-default operator assertion, requires exactly one FE and BE, normalized absolute paths, pre/post version and identity checks, and rejection on topology change before dispatch. These checks cannot prove a shared mount namespace; object storage is the production mode.5.3 Invocation identity, result envelope, and termination evidence
LanceErrorCoderead before the consuming error message, a bounded sanitized message, and matching domain-termination proof when available;lance_free_string;5.4 Capacity ownership and queueing
5.5 Time budget, cancellation, and disconnect
6. Failure contract
6.1 Promise and non-goals
The initial version does not promise persistent job history, FE failover recovery, resumable builds, or durable same-name serialization across failures. All in-flight control state is in memory. An FE restart or failover during a statement is observed by the client as a disconnect and follows Sections 5.5 and 6.2.
6.2 Indeterminate outcomes
A timeout, disconnect, worker loss, or lost response after dispatch, when no complete trusted outcome has been received, MUST NOT be presented as proof that nothing committed. Such a statement returns an explicit indeterminate-outcome error that:
SHOW INDEX FROM ...andlance_index_entries(...);6.3 Cancellation
Cancellation (query kill, statement timeout) triggers best-effort worker termination and returns control to the client; it cannot undo an existing commit. If a complete trusted success was already received before cancellation, the statement reports the committed, refresh incomplete class rather than indeterminate (Sections 1.1 and 5.5). Worker capacity and the active-name entry remain held until termination is confirmed (Sections 5.3, 5.4).
6.4 Refresh obligations independent of outcome
Mutation outcome and metadata refresh are independent. One bounded refresh action runs inside the statement where the table below requires it; there is no refresh job and no background retry framework. Refresh failure keeps the mutation classification and attaches a separate refresh diagnostic.
IFno-op — authoritative state already satisfies the conditionA confirmed commit followed by a refresh failure MUST be reported as a distinct typed error — committed, refresh incomplete — never as a build failure; the direction comment requires exactly this distinction. The commit is a fact, Lance metadata is authoritative, and the user may retry inspection or refresh through ordinary means. The build outcome is never rewritten by refresh failure.
The no-op branch closes the same way. Its required actions are a bounded local cache invalidation on the executing (master) FE and initiation of the ordinary external-table refresh propagation — the existing edit-log-and-replay mechanism every other external refresh already uses — both retried inside the statement and practically always completable. Reads served by the executing FE then re-resolve from authoritative metadata and never hit cache entries predating the invalidation; caches on other FEs converge through the existing refresh-propagation semantics, and this contract claims no new cross-FE freshness guarantee beyond that existing mechanism. An optional authoritative metadata reload that fails afterwards attaches an independent diagnostic and does not change the classification. In the residual pathological case where the invalidation or the propagation initiation cannot complete, the statement returns a typed refresh error in the confirmed-failure family: no mutation was executed, it is not a build failure, and it is neither committed nor indeterminate.
A native success proves that this invocation committed. It does not guarantee that another external writer has not since replaced or removed the index; "the index exists" is a statement about this commit, not a promise about the present external state.
6.5 Provider-result classification
LANCE_OKfrom the one native mutating invocationLANCE_ERR_COMMIT_CONFLICT(exact code)NOT_FOUND,INVALID_ARGUMENT,NOT_SUPPORTED,INDEX,IO, aggregateINTERNAL, and typed panicThree properties of the pinned ABI shape this table and are normative context, not implementation detail:
IF EXISTSno-op before the mutating invocation; there is no post-invocation DROP no-op optimization in 4.2;Duplicate CREATE may return coarse
INDEXorINVALID_ARGUMENT, not a stable already-exists/no-commit result. CREATE IF NOT EXISTS therefore resolves before dispatch; a later matching index is only corroboration and the outcome stays indeterminate.6.6 Error surface
Errors are typed, bounded, and sanitized. The four outcome classes of Section 1.1 are distinguishable by the client through distinct error codes/errClasses following Doris conventions; exact codes are implementation details. No secrets, raw provider payloads, unbounded text, or fabricated progress appear in any error.
7. Authorization, confidentiality, and cleanup
ALTER.SHOW.Failed native work may leave unreferenced index files. Doris never guesses and deletes physical paths. Lance cleanup/VACUUM and historical-version retention own physical reclamation.
8. Distributed construction extension (future work, non-normative)
Synchronous SQL does not preclude distributed index construction. The desired extension is:
lance_index_segment_builder_execute_uncommittedreturns protobuf-encoded segment metadata without touching the manifest, andlance_dataset_commit_index_segmentscommits pre-built segments as one logical index in a single manifest commit; the FE binding exposescommitExistingIndexSegments. Entry-point existence is not sufficient evidence: this path still requires Doris-side orchestration, transport of the uncommitted segment artifacts, verification of the segment contract, object-storage visibility, model compatibility, and exactly-one coordinated commit before implementation;replace=falsecontract. The initial version therefore keeps the single one-shot mutating call, and any two-phase capability is designed and verified separately — CREATE/IF semantics are not changed for extensibility;create_indexcalls against the same logical index name;9. Delivery and release evidence
9.1 Reviewable delivery slices
branch-4.1: remove the durable job framework of branch-4.1: [feature](lance) Durable Lance index job infrastructure with fence, quota and replay #67235 and the job-specific parts of branch-4.1: [feature](lance) Add Lance index admission, job inspection SQL and catalog DDL guard #67630 (job creation, JobId results, job inspection SQL, unresolved-job guards), preserving and adapting the privilege/schema/parameter/snapshot/IF validation,SHOW INDEX, and the physical TVF. Compatibility verification targets: the removed operation code keeps its tombstone and is never reused; the journal reader still decodes an old record; edit-log replay discards it; old image modules are skipped; and the JobId removal is checked against ordinary MySQL, forwarded, and Flight SQL result schemas. Existing metadata must not become unreadable;Mutation remains disabled until the evidence for its slice passes.
9.2 Gates
IFno-op invalidation and refresh initiation including their failure and timeout paths —CREATE IF NOT EXISTSandDROP IF EXISTSno-op cases assert zero execute sends, zero workers, the correct SQL class, name/target owner release, that a repaired refresh is retried only through a fresh authoritative preflight, and that a follower-initiated statement followed by a read on another FE observes the existing refresh-propagation semantics; case-only collisions; privileges.9.3 Safety configuration invariants
The 4.2 contract does not fix setting names. Mutation defaults disabled and cannot be enabled until the implementation has a verified hard worker memory/PID/runtime boundary, finite concurrency and queue bounds, a configured total time budget covering reads, queueing, execution, and refresh, and bounded metadata/input processing. Exact settings and reviewed defaults follow Doris conventions.
10. Accepted operational limitations
11. Review request
Verified baseline evidence. The four facts below were corrected against the pinned sources on 2026-10-09 and shape the contract above; please flag any disagreement:
branch-4.1c8c0ddca4d; FElance-core12.0.0; BElance-ccommit98468344(version string0.1.9; vars.sh-pinned; Rust core68c12dfd) — package versions and release tags alone do not determine ABI behavior;commitExistingIndexSegments, and the pinned C binding exposes both the uncommitted build half (lance_index_segment_builder_execute_uncommitted) and a C-side segment-commit entry (lance_dataset_commit_index_segments);The single open decision — client-disconnect semantics. A plain disconnect after dispatch lets the dispatched work finish within its budget: no commit is guaranteed, no result packet can be delivered, and the client treats the outcome as indeterminate (Sections 1.1 and 5.5). Explicit KILL or statement timeout, by contrast, requests best-effort termination; a complete success already received before cancellation is still reported as committed, refresh incomplete rather than indeterminate (Section 6.3). This is the one place the design departs from Doris convention, which cancels queries when the client disconnects. The choice is a product trade-off, stated honestly: terminating on disconnect as well is coherent, would free the worker's CPU, memory, and slot sooner, and a disconnected client observes indeterminate under either policy — it cannot receive a result packet either way. Letting dispatched work finish is recommended because the work was explicitly requested, its completion becomes authoritatively inspectable, and its continued resource use is already bounded by the statement budget. Confirmation or rejection of this single choice is requested.
Boundary confirmation. Please confirm whether this document accurately reflects the final Doris 4.2 implementation boundary. A simple Yes would be helpful; if not, please identify any specific conflict with Required in 4.2 (Section 1.2) or Explicitly Deferred (Section 1.3).
Please review this comment-thread document as the complete proposal; no requirement should be inferred from v5/v5.1 or from the review history they capture.
u70b3 commentedon Oct 14, 2026
PR roadmap (new live tracking comment — I will keep this one edited up to date as PRs move)
Replaces the earlier roadmap comment (pre-2026-10-08, now marked deprecated). The synchronous execution contract is the published v6 proposal (design comment, 2026-10-14); the sync-series PRs implement it. The split was redrawn after the synchronous-direction change (direction comment, 2026-10-08): mutations execute synchronously inside the SQL statement; the durable job framework was reverted rather than kept as a second path.
SHOW INDEXlance_index_entriesTVF (physical UUID / dataset-version inspection)SHOW LANCE INDEX JOB(S)/guards (reverted)SHOW INDEX, TVF, DDL guard, gate (default off)All PRs target
branch-4.1. Mutation remains disabled (enable_lance_index_mutationdefault off) until the execution evidence gates pass.[doc](lance) add doc about lance_index_entries TVF (#4311)