Improve Rust SDK ergonomics - #2287
Conversation
|
cc @badmonster0 , I think it is in a good shape once merged can close #2278 |
There was a problem hiding this comment.
This is expected to be memoized. Similar to most other examples.
There was a problem hiding this comment.
This is leaking specific data types into the shared framework. We'll have trouble when we want to support more types outside the SDK.
I think we may need a dedicated trait for memo key and memo states, e.g. MemoInput. Probably we can provide a blanket implementation for all type that implements Serialize, and users can also implement MemoInput for specific types.
There was a problem hiding this comment.
Agreed. I鈥檒l introduce a MemoInput trait so each type controls its memo behavior. Normal types can use the standard serialized-value behavior, while file and resource types can provide their own implementation. This removes the hardcoded list of file types from the shared memo code.
| #[derive(Clone, Serialize, Deserialize)] | ||
| cocoindex::context_key!( | ||
| static DB: postgres::Database = "s3_embedding_db", | ||
| state = postgres::Database::state_id |
There was a problem hiding this comment.
I think the way to compute memoization key should be defined on the type instead of on each context_key!() site by default. e.g. in the Python SDK, when declaring a context key, users can provide a boolean to decide whether or not to detect changes on it (doc), but memo key function doesn't have to be specified on each declaration.
We may provide advanced option to allow customizing memo key on sites of declarations (similar to customizing arguments memo key in Python SDK, which accepts lambda: docs), but should be only for advanced cases. The baseline should be deciding memo key by data types.
There was a problem hiding this comment.
That makes sense. I was mixing together two separate decisions:
- whether a context value participates in change detection; and
- how its memo identity is computed when change detection is enabled.
I鈥檒l align the baseline behavior with the Python SDK:
// Uses "DB" as the context key. Change detection is disabled.
cocoindex::context_key!(static DB: postgres::Database);
// Change detection is enabled. The type defines its memo behavior.
cocoindex::context_key!(
static EMBEDDER: SentenceTransformerEmbedder,
detect_change
);
// The logical context-key name can be overridden independently.
cocoindex::context_key!(
static EMBEDDER: SentenceTransformerEmbedder,
key = "embedder",
detect_change
);Change detection will be disabled by default, as it is in Python. When it is enabled, the value鈥檚 type will define its memo identity through a MemoInput implementation. Ordinary serializable types can use a derive/default implementation, while resource types such as files can provide their own identity and state-validation behavior. This removes the need to repeat closures at every context_key! declaration.
key = "..." only overrides the logical context-key name; it does not customize memoization.
For advanced cases, we could separately support a declaration-level projection:
cocoindex::context_key!(
static EMBEDDER: Embedder,
detect_change,
memo_key = |embedder| (
embedder.model_name(),
embedder.revision(),
)
);This would replace the type鈥檚 default memo identity for that declaration, similar to a Python function argument鈥檚 custom memo_key lambda. It would not be required for normal usage.
I鈥檓 also happy to leave this advanced override out of the initial API and add it only when we have a concrete use case. Does this separation match what you had in mind?
There was a problem hiding this comment.
The proposed new syntax looks good. Yes, let's leave the advanced override out in the initial version. Thanks!
| cocoindex::context_key!( | ||
| static DB: postgres::Database = "s3_embedding_db", |
There was a problem hiding this comment.
The syntax of DB: postgres::Database = "s3_embedding_db" is a little bit weird.
Actually, probably by default we can directly use the variable name as the context key, e.g.
cocoindex::context_key!(static DB: postgres::Database);"DB" is used as the context key here, so don't need to be spelled out.
If users need to override the context key, probably can be done through a named argument like this:
cocoindex::context_key!(static DB: postgres::Database, key="s3_embedding_db");There was a problem hiding this comment.
make sense, I made some context_key design comment in above comment, all other context key may use the same or similar patterns
Introduce MemoInput and #[derive(MemoInput)] so memo identity and optional async freshness validation are owned by types. Compose both through nested containers and supported ecosystem leaves, while preserving existing single-file resource memo keys and treating legacy or malformed nested state as cache misses. Redesign context_key! around identifier-default names, an optional key override, and opt-in detect_change matching Python defaults. Connection handles remain untracked unless explicitly enabled, the advanced context-site projection stays out of the initial API, and function-level memo_key overrides remain available. Migrate the Rust examples and documentation to the new contracts and refresh the standalone example lockfiles.
# Conflicts: # rust/sdk/cocoindex/Cargo.toml
|
cc @georgeh0 , PTAL |
Summary
context_key!macro and migrate Rust SDK examples away from hand-writtenLazyLock<ContextKey<_>>definitionsSchemaFields, runtime vector-dimension resolution, duplicate-column validation, and connector-correct vector/decimal/bytes handlingWhy
The Rust SDK required substantially more manual wiring than the Python SDK for batching, context resources, and connector schemas. That boilerplate also let behavior drift between connectors鈥攆or example unresolved dimensions, half-precision vectors, and serialized scalar values could be handled inconsistently.
This change makes those paths declarative and gives validation a shared source of truth. Generated batching wrappers retain per-item cache behavior while coalescing misses, context keys have one concise declaration form, and connector schemas can be derived directly from the row type and resolved from the active embedder.
User impact
Rust applications can now use
#[cocoindex::function(memo, batching)],context_key!, andTableSchema::from_row/connector equivalents instead of maintaining parallel batching, context, and schema definitions. The updated examples demonstrate per-text memoization and runtime embedding dimensions across Postgres, Qdrant, Turbopuffer, and LanceDB.The review fixes also fail early on unresolved or invalid vector dimensions, preserve LanceDB float16 vectors end to end, isolate scheduled batch deadlines, and align Postgres, SQLite, and Doris value encoding with their declared column types.
Validation
uv run maturin developcargo testcargo test -p cocoindex --features postgres,sqlite,doris,qdrant,turbopuffer --test schema_from_rowcargo check --lockedinexamples/rust/text_embeddingcargo check --lockedinexamples/rust/text_embedding_qdrantcargo check --lockedinexamples/rust/text_embedding_turbopuffercocoindex_macrosand the affectedcocoindexlibrary targets