aimem-core 0.5.0

Core library for AiMem — give your AI a memory. Hybrid search + knowledge graph over Turso.
docs.rs failed to build aimem-core-0.5.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: aimem-core-0.4.0

aimem-core

Language / 语言 / 言語: English | 简体中文 | 日本語

aimem-core is AiMem's Rust core library.

It provides:

  • Turso-backed storage
  • multiprocess WAL coordination for file-backed stores (.db-tshm sidecar)
  • project mining and conversation import
  • hybrid keyword + vector retrieval
  • memory layers / wake-up context generation
  • knowledge graph support
  • embedding profile guards
  • multimodal content primitives
  • Unicode/CJK/Kana-aware keyword fallback for Chinese and Japanese retrieval
  • reproducible text and deterministic multimodal truth benchmarks in the repository

CLI lives in aimem-cli. MCP server lives in aimem-mcp.

0.5.0 upgrade: breaking changes

  • New embedding model: local MiniLM 384d / fastembed is replaced by EmbeddingGemma 2 ONNX Q8 768d. Old vectors and remote Gemini vectors cannot share the new model's store. Stop writers, back up the old DB and its WAL sidecars, then re-embed original text into a separate database. Do not relabel stored profiles; there is no automatic vector conversion.
  • Rust 1.95+ is now required. First local use downloads about 314 MB of weights plus the tokenizer; subsequent runs can use the cache offline.
  • Turso 0.8.3: downstream applications using Turso directly should align versions to avoid incompatible types or duplicate native symbols.
  • Rust API: replace matches on EmbedError::Fastembed with EmbedError::Local. Local provider/model/dimension constants have changed; use embed_query for manual retrieval queries.

Full release notes and migration instructions.

Current capabilities

Current aimem-core includes:

  • async embedding flow
  • LocalEmbedder
  • runs onnx-community/embeddinggemma-2-ONNX through ONNX Runtime using Q8 text weights and normalized 768d vectors
  • downloads about 314 MB of weights plus the tokenizer to the Hugging Face cache on first use (HF_HOME / HF_HUB_CACHE supported); works offline once cached
  • embeds text only, preserving text extraction from multimodal inputs; truncates at 8,192 tokens
  • embed / embed_one add the document prefix; embed_query adds the retrieval query prefix, which Searcher uses automatically
  • opt-in Gemini2Embedder
  • multimodal ContentPart
  • embedding profile metadata:
    • provider
    • model
    • dimension
  • profile guards against mixed stores
  • Drawer helper API
  • MemoryStack::file_text(...)
  • MemoryStack::file_drawer_with_id(...)
  • MemoryStack::file_drawers_with_ids(...)
  • Turso FTS + RRF-based hybrid search
  • CJK/Japanese keyword fallback for no-embedding search
  • reproducible EN/ZH/JA benchmark fixtures and result snapshots
  • tighter extractor heuristics with multilingual regression tests

Install

[dependencies]
tokio = { version = "1", features = ["full"] }
cargo add aimem-core

Recommended API surface

Prefer either:

use aimem_core::prelude::*;

or explicit root imports like:

use aimem_core::{AimemDb, Drawer, LocalEmbedder, MemoryStack, Miner, Searcher};

Core concepts

  • Store — one AiMem DB
  • Wing — project or domain
  • Room — topic inside a wing
  • Drawer — one stored chunk
  • ContentPart — multimodal part inside a drawer
  • L0/L1/L2/L3 — identity, essential story, recall, deep search

Embedding modes

Local

  • LocalEmbedder
  • runs onnx-community/embeddinggemma-2-ONNX through ONNX Runtime using Q8 text weights and normalized 768d vectors
  • downloads about 314 MB of weights plus the tokenizer to the Hugging Face cache on first use (HF_HOME / HF_HUB_CACHE supported); works offline once cached
  • embeds text only, preserving text extraction from multimodal inputs; truncates at 8,192 tokens
  • embed / embed_one add the document prefix; embed_query adds the retrieval query prefix, which Searcher uses automatically
  • default recommendation
  • embedding stays local

Remote

  • Gemini2Embedder
  • explicit opt-in
  • only explicit text / data URI / raw bytes are sent

URI-only multimedia parts are not automatically read from disk and uploaded.

Store compatibility guard

aimem-core records embedding profile metadata and validates semantic writes and queries against:

  • provider
  • model
  • dimension

Legacy MiniLM 384d vectors cannot be mixed with the new local 768d vectors. Local EmbeddingGemma 2 and remote Gemini also cannot share a store even though both produce 768d vectors. To migrate, rebuild a separate database from the original text; existing databases are never converted or deleted automatically.

Drawer helpers

use aimem_core::prelude::*;

let drawer = Drawer::new("id", "wing", "room", "content", "agent")
    .with_source_file("README.md")
    .with_chunk_index(3);

Stable-ID filing through MemoryStack:

use std::sync::Arc;
use aimem_core::prelude::*;

# #[tokio::main]
# async fn main() -> anyhow::Result<()> {
let cfg = Config::load()?;
let db = AimemDb::open(&cfg.db_path).await?;
let embedder = Arc::new(LocalEmbedder::new()?);
let stack = MemoryStack::new(db, embedder, &cfg);

let inserted = stack
    .file_drawer_with_id(
        "attachment.chunk.001",
        "attachments",
        "user-123",
        "Attachment chunk body".to_string(),
        vec![ContentPart::text("Attachment chunk body")],
        Some("report.pdf"),
        1,
        "example",
    )
    .await?;

println!("inserted={inserted}");
# Ok(())
# }

When you already have a related set of stable-ID drawers (for example one attachment summary plus chunk drawers), batch them in one call:

use std::sync::Arc;
use aimem_core::prelude::*;

# #[tokio::main]
# async fn main() -> anyhow::Result<()> {
let cfg = Config::load()?;
let db = AimemDb::open(&cfg.db_path).await?;
let embedder = Arc::new(LocalEmbedder::new()?);
let stack = MemoryStack::new(db, embedder, &cfg);

let inserted = stack
    .file_drawers_with_ids(&[
        DrawerFilingRequest::new(
            "attachment.summary.001",
            "attachments",
            "user-123",
            "Attachment summary".to_string(),
            "example",
        )
        .with_parts(vec![ContentPart::text("Attachment summary")])
        .with_source_file("report.pdf"),
        DrawerFilingRequest::new(
            "attachment.chunk.001",
            "attachments",
            "user-123",
            "Attachment chunk 1".to_string(),
            "example",
        )
        .with_parts(vec![ContentPart::text("Attachment chunk 1")])
        .with_source_file("report.pdf")
        .with_chunk_index(1),
    ])
    .await?;

println!("inserted={inserted:?}");
# Ok(())
# }

AiMem checks existing stable IDs before embedding, so retrying the same batch does not spend another remote embedding call on drawers that are already stored.

For multimodal drawers:

use aimem_core::prelude::*;

let drawer = Drawer::multimodal(
    "id",
    "wing",
    "room",
    "Look at this image",
    vec![ContentPart::text("Look at this image")],
    "agent",
);

Hybrid search example

use std::sync::Arc;
use aimem_core::prelude::*;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let db = AimemDb::memory().await?;
    let embedder = Arc::new(LocalEmbedder::new()?);

    let content = "We moved the memory backend to Turso for local hybrid search.";
    let embedding = embedder.embed_one(content).await?;

    let drawer = Drawer::new("drawer_demo_001", "demo_app", "backend", content, "example");
    db.insert_drawer_with_profile(
        &drawer,
        Some(&embedding),
        embedder.provider_name(),
        embedder.model_name(),
    ).await?;

    let searcher = Searcher::new(db, embedder);
    let hits = searcher.hybrid_search("local memory database", Some("demo_app"), None, 5).await?;
    assert!(!hits.is_empty());
    Ok(())
}

Filing text directly

use std::sync::Arc;
use aimem_core::prelude::*;

# #[tokio::main]
# async fn main() -> anyhow::Result<()> {
let cfg = Config::load()?;
let db = AimemDb::open(&cfg.db_path).await?;
let embedder = Arc::new(LocalEmbedder::new()?);
let stack = MemoryStack::new(db, embedder, &cfg);

let id = stack.file_text("demo_app", "notes", "remember this", "example").await?;
println!("{id}");
# Ok(())
# }

Notes

  • AimemDb::open() bootstraps schema automatically.
  • File-backed AimemDb::open() enables Turso multiprocess WAL coordination and may create .db-wal / .db-tshm sidecars beside the DB file.
  • Miner::new(db, None) and ConvoMiner::new(db, None) work fine for text-only filing.
  • drawers without embeddings still appear in keyword / fallback search.
  • AimemDb::embedding_profile() reports the current store profile.