Repository navigation
Description
Summary
Feast has no Lance support today — no issue or PR references it. This proposes LanceSource, but the more useful framing is that Lance is a second implementation of the abstraction already agreed in #6499, and it is the motivating use case that makes #5782, #5652 and #5330 concrete rather than theoretical.
Filing it as one issue because those three are only separable on paper: an embedding source is useless without a version pin, a declared vector schema, and a choice of execution engine.
Why this is not #1494 / #1533
Format-specific sources have a poor record here — #1494 (Hudi) was closed wontfix, #1533 (Delta) has been open since 2021. So this is deliberately not "add another format".
In #6499, @ntkathole proposed moving catalog connection config onto the DataSource so the offline store stays generic and other engines (Trino, DuckDB) can serve the same tables; @falloficaruss agreed. That abstraction is currently specified against exactly one format and one catalog API. Lance stresses it in two ways Iceberg does not:
- A non-Iceberg format inside a REST catalog. Apache Polaris serves Lance through its generic-tables API, on a different path from its Iceberg API. If the source only models Iceberg tables, catalog-backed formats need a parallel code path — cheaper to settle before
IcebergRestCatalogSourcelands than after. - A non-JVM read path. Lance has a native Python/Rust reader, so this source can be served with no Spark at all. That is a real test of whether the
DataSourceis engine-agnostic, rather than Spark-agnostic in name only.
Proposal
LanceSource
LanceSource(
catalog="my_catalog", # catalog-backed
namespace="my_namespace",
table="item_embeddings",
# or path-based:
# uri="s3://bucket/item_embeddings.lance",
version=3, # or tag="candidate" — #5782
timestamp_field="generated_at",
)A naming question worth settling early: if #6499 lands as IcebergRestCatalogSource, then a sibling called LanceSource mixes two axes — one named for a catalog, one for a format. Either the catalog connection is a shared, reusable piece that both formats reference, or the names should agree on an axis. I don't have a strong preference, but it is easier to decide now than to rename later.
Vector schema is declared on the FeatureView, using fields Field already has:
Field(name="embedding", dtype=Array(Float32),
vector_index=True, vector_length=160, vector_search_metric="COSINE")Two engines, one source — #5330
| Path | Engine | Use |
|---|---|---|
| Native | pylance + DataFusion/DuckDB |
local dev, CI, notebooks, eval-set construction |
| Spark | lance-spark |
existing batch/GPU clusters |
The read-path half is already solved: HybridOfflineStore (#5541, shipped in 0.51.0) routes by data source type, so both stores can coexist in one project.
The remaining gap is #5330 — batch_engine / batch_configs on FeatureView are still placeholders, so a project cannot choose the engine per FeatureView. An embedding pipeline needs exactly that: Spark for generation, native for interactive retrieval, one project. #6359 looks like the structural prerequisite, since the offline-store retrieval path currently bypasses the compute engine.
Precedent for one source across engines: FileSource is served by both the Dask and DuckDB offline stores.
Pin semantics — answering @jfw-ppi on #5782
@jfw-ppi asked whether the response schema would stay FeatureView-defined rather than varying by store or snapshot. Proposed:
- The
FeatureViewschema is the contract. A pinned version selects data, never shape. - At resolve time, validate the pinned version's actual schema against the declared schema.
- Incompatibility fails explicitly. A vector-length change is a hard error, not a silently different response.
Related, and an argument for validating rather than trusting declarations: in Field.__eq__ the vector_index and vector_search_metric comparisons are currently commented out, so only vector_length participates in equality:
or self.vector_length != other.vector_length
# or self.vector_index != other.vector_index
# or self.vector_search_metric != other.vector_search_metricA metric change from COSINE to L2 is therefore invisible to feast apply / plan. Happy to split that out into its own bug if preferred.
Vector config placement — #5652
This assumes #5652's direction: vector config belongs to the feature definition, not global online_store config. The Field attributes already exist, but online stores still read dimension and metric from global config, so two feature views with different dimensions cannot coexist in one project.
Scope
Not proposing Lance as an online store. Lance is a format plus indexes, not a low-latency KV service, and its write path is columnar while online_write_batch is row-oriented proto. Offline / retrieval axis only.
Suggested sequencing
LanceSource+ native (pylance) offline store, path-based only — landable without waiting on feat: Extend Feast's DataSource to natively support Iceberg REST Catalog-backed tables #6499- Catalog-backed addressing, coordinated with feat: Extend Feast's DataSource to natively support Iceberg REST Catalog-backed tables #6499
version/tagpinning + schema validation (Supporting snapshot based time travel for Feast #5782)- Spark offline store support for
LanceSource - Per-FeatureView engine selection (Support compute engine configs into FeatureView definition #5330, after Wire ComputeEngine.get_historical_features() into the standard retrieval path to replace per-store BFV transformation duplication #6359)
Activity
feat: Add LanceFormat table format with version/tag pinning
feat: Add LanceFormat table format with version/tag pinning
haoxu0 commentedon Oct 4, 2026
Status, plus a correction to the original text.
Correction. The body says it is "cheaper to settle before
IcebergRestCatalogSourcelands than after". That was wrong when I wrote it — the work from #6499 had already merged.sdk/python/feast/infra/data_sources/contrib/iceberg_catalog/exists on master withIcebergSource,UnityCatalogSource,IcebergRestClient, and both DuckDB and Spark tests. Apologies for the misleading framing.Reading the merged code changes the argument rather than weakening it.
IcebergSourcecarries catalog configuration flattened onto the source —catalog_type,catalog_name,endpoint,catalog_properties: Dict[str, str],token_env_var,credential_vending— and constructs its client insideget_catalog_client()with an if/elif oncatalog_type. A second catalog-backed format either duplicates all of that or motivates extracting it. There is also already aUnityCatalogSourcealongsideIcebergSource, so the naming is drifting between two axes: one source named for a catalog, one for a format.What landed. #6925 (
1196e2233) addedLanceFormatto the existingTableFormatabstraction from #5650 rather than adding a new source, which turned out to be the better fit:catalog/namespaceaddressing plus an optional pin viaversionortagSparkSourceunchanged, because it already drives its reader fromtable_format.format_type.valueandtable_format.propertiesversion >= 1enforced so the Python and proto semantics agree, since the proto treats0as unsetSo the
LanceSourcesketch in the body is superseded —LanceFormatis the shape that merged.Still open. The format descriptor exists; there is no Lance reader yet. The remaining scope from this issue:
pylanceread path, so a Lance source can be served without Spark. This is the real test of whether the DataSource/offline-store split is engine-agnostic rather than Spark-agnostic in name only.catalog_type/catalog_propertiesquestion above settled. Worth noting Apache Polaris serves Lance through its generic-tables API, on a different path from its Iceberg API, so the two cannot share a source even though they can share a catalog.FeatureViewshould fail explicitly. That is the open question @jfw-ppi raised on Supporting snapshot based time travel for Feast #5782.Keeping this open for 1-3. Happy to split them out if that is easier to track.
Related: #5782, #5652, #5330, #6359, #6499
feat: Add a non-JVM read path for Lance data sources
feat: Add a non-JVM read path for Lance data sources (#6943)