Skip to content

Improve Rust SDK ergonomics - #2287

Open
tomz-alt wants to merge 16 commits into
cocoindex-io:mainfrom
tomz-alt:rust-sdk-review-fixes
Open

Improve Rust SDK ergonomics#2287
tomz-alt wants to merge 16 commits into
cocoindex-io:mainfrom
tomz-alt:rust-sdk-review-fixes

Conversation

@tomz-alt

Copy link
Copy Markdown
Contributor

Summary

  • add item-shaped Rust function batching with per-item memoization, body-hash invalidation, configurable batch limits, deadline-neutral physical batches, and shared dependency tracking
  • introduce the context_key! macro and migrate Rust SDK examples away from hand-written LazyLock<ContextKey<_>> definitions
  • derive connector schemas from Rust row types with SchemaFields, runtime vector-dimension resolution, duplicate-column validation, and connector-correct vector/decimal/bytes handling
  • organize public connector and resource modules, add memoized single-text sentence-transformer embedding, and update the showcase, quickstart, contributor playbook, and examples to teach the new APIs

Why

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!, and TableSchema::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 develop
  • cargo test
  • cargo test -p cocoindex --features postgres,sqlite,doris,qdrant,turbopuffer --test schema_from_row
  • cargo check --locked in examples/rust/text_embedding
  • cargo check --locked in examples/rust/text_embedding_qdrant
  • cargo check --locked in examples/rust/text_embedding_turbopuffer
  • strict Clippy for cocoindex_macros and the affected cocoindex library targets

@ZhiHanZ

ZhiHanZ commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

cc @badmonster0 , I think it is in a good shape once merged can close #2278

@badmonster0
badmonster0 requested a review from georgeh0 July 17, 2026 16:38

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected to be memoized. Similar to most other examples.

Comment thread rust/sdk/cocoindex/src/memo.rs Outdated
Comment on lines 249 to 277

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. I was mixing together two separate decisions:

  1. whether a context value participates in change detection; and
  2. 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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed new syntax looks good. Yes, let's leave the advanced override out in the initial version. Thanks!

Comment on lines +32 to +33
cocoindex::context_key!(
static DB: postgres::Database = "s3_embedding_db",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense, I made some context_key design comment in above comment, all other context key may use the same or similar patterns

ZhiHanZ added 2 commits July 27, 2026 12:07
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
@tomz-alt
tomz-alt marked this pull request as ready for review July 27, 2026 21:43
@tomz-alt

Copy link
Copy Markdown
Contributor Author

cc @georgeh0 , PTAL

@tomz-alt
tomz-alt requested a review from georgeh0 July 30, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants