Value serialization for memoization

How CocoIndex serializes memoized function returns with serde — the Serialize and Deserialize bounds, MemoInput for memo keys, and SchemaFields for connector rows.

Version
v 1.0.14
Last reviewed
Nov 4, 2026
ExperimentalThe Rust SDK is experimental. It is outside CocoIndex's compatibility promise: any release, including a minor one, may change or remove its API. It is not yet published on crates.io.

Overview

CocoIndex serializes and caches the return values of memoized functions so that unchanged work can be skipped on subsequent runs. The return type of a memoized function is static, so there is nothing to annotate: it must implement serde::Serialize and serde::Deserialize, which is how CocoIndex stores the value and reads it back:

#[cocoindex::function(memo)]
async fn parse_file(_ctx: &Ctx, file: FileEntry) -> Result<Vec<Section>> {
    // ...
}

Derive both on Section: #[derive(Serialize, Deserialize)].

Three traits cover every value CocoIndex serializes or fingerprints; there is no registration step.

Return values

A memoized return value is stored through serde::Serialize and read back through serde::Deserialize. Any type with both implementations works: primitives, String, every serde container, Option, tuples, and structs and enums with #[derive(Serialize, Deserialize)], nested to any depth.

A cache hit deserializes the stored value into the function’s return type. If you change that type’s shape (rename or retype a field) without changing the function, a stored value that no longer deserializes fails the call with an error. Bump the function’s version, or rebuild the cache with UpdateOptions { full_reprocess: true, .. } or cocoindex update --full-reprocess.

Memo keys

Memo keys (function arguments, deps values, change-detected context values) are fingerprinted through the MemoInput trait. Derive it with #[derive(cocoindex::MemoInput)]; every field is handled recursively, so the type needs no Serialize. It composes through Option, sequences, tuples, maps, sets, Box and Arc. uuid::Uuid and serde_json::Value are always available; the chrono and rust_decimal features add those leaf types, and a connector feature enables the leaf features it needs. Wrap another third-party type in a newtype when it appears inside derived data. A type with only Serialize is keyed by its serialized value; a target handle that is Serialize is keyed that way, by its stable key. A memoized function skips any other handle with memo_key(table = skip) (see Function and target handles).

Connector rows

Connector rows are serde::Serialize structs with #[derive(SchemaFields)]. The connector’s TableSchema::from_row::<Row>(primary_key) derives the columns from the struct: an Option<T> field is a nullable column, every other field is NOT NULL.

use cocoindex::SchemaFields;
use cocoindex::connectors::postgres::TableSchema;

#[derive(Serialize, SchemaFields)]
struct Row {
    id: i64,
    title: Option<String>,          // text, nullable
    views: i32,
    #[coco(vector)]
    embedding: Vec<f32>,            // vector(N): N comes from the embedder
    #[coco(vector = 8, half)]
    embedding_half: Vec<f32>,       // halfvec(8)
    #[coco(json)]
    meta: Vec<String>,              // jsonb
}

// A dimension known up front:
let schema = TableSchema::from_row::<Row>(["id"])?.with_vector_dim("embedding", 8)?;

// A dimension reported by the loaded embedder:
let schema = TableSchema::from_row::<Row>(["id"])?
    .with_vector_schema("embedding", ctx.get_key(&EMBEDDER)?)
    .await?;

The field attributes are:

AttributeColumn
#[coco(vector)]A dense f32 vector; the dimension comes from with_vector_schema or with_vector_dim
#[coco(vector = N)]A dense f32 vector of dimension N
#[coco(vector = N, half)]A 16-bit vector of dimension N
#[coco(json)]JSON storage for the field
#[coco(type = "...")]A raw connector column type, used as written
#[coco(rename = "...")]A different column name

with_column_type(field, type) replaces one column’s type for this schema only; use it when one row struct feeds two connectors. To encode one field’s value differently, put #[serde(serialize_with = "...")] on it: the row is encoded by its Serialize implementation.