kiromi-ai-memory 0.2.2

Local-first multi-tenant memory store engine: Markdown/text content on object storage, metadata in SQLite, plugin-shaped embedder/storage/metadata, hybrid text+vector search.
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Plan 18 phase C — `LexicalIndex` plugin trait.
//!
//! The default implementation [`super::fts5_index::Fts5Index`] talks to the
//! `fts5` virtual tables created idempotently at `Memory::open()` time via
//! [`crate::metadata::MetadataStore::create_indices_if_missing`]. Future swaps
//! (a hosted search service, a different on-disk inverted index) implement
//! this trait and plug into the engine via the builder.

use async_trait::async_trait;

use crate::error::Result;
use crate::index::vector_trait::VectorScope;
use crate::memory::MemoryId;
use crate::partition::PartitionPath;
use crate::summary::SummaryId;

/// Self-declared capabilities for a `LexicalIndex` impl.
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct LexicalIndexCapabilities {
    /// Whether the impl ranks with BM25 (the FTS5 default).
    pub bm25: bool,
    /// Whether the impl supports phrase queries (`"foo bar"`).
    pub phrase_queries: bool,
    /// Whether the impl supports prefix queries (`foo*`).
    pub prefix_queries: bool,
}

impl Default for LexicalIndexCapabilities {
    fn default() -> Self {
        Self {
            bm25: true,
            phrase_queries: true,
            prefix_queries: true,
        }
    }
}

/// Plugin trait for the lexical half of the engine's index surface.
///
/// # Atomicity
///
/// As with [`super::VectorIndex`], the `upsert_*` / `delete_*` methods on
/// this trait are **not, by themselves, atomic with the catalog row**.
/// Direct calls (e.g. from a custom reindex tool) can leave the catalog
/// and the lexical index momentarily out of sync. The engine's authoritative
/// writers — `MetadataStore::append_memory`, `MetadataStore::insert_summary`
/// — fold these inserts into the same SQL transaction as the catalog
/// writes when the default [`super::Fts5Index`] impl is used, preserving
/// the single-transaction property.
#[async_trait]
pub trait LexicalIndex: Send + Sync + std::fmt::Debug + 'static {
    /// Insert or replace the memory's text content.
    ///
    /// **Atomicity:** see the trait-level [Atomicity] note —
    /// invoking this method directly does **not** guarantee atomicity
    /// with the catalog row. Prefer
    /// [`crate::metadata::MetadataStore::append_memory`] for transactional
    /// writes.
    ///
    /// [Atomicity]: LexicalIndex#atomicity
    async fn upsert_memory(
        &self,
        id: &MemoryId,
        partition_path: &PartitionPath,
        content: &str,
    ) -> Result<()>;

    /// Insert or replace the summary's text content.
    async fn upsert_summary(&self, id: &SummaryId, parent_path: &str, content: &str) -> Result<()>;

    /// Delete a memory's text row. Idempotent.
    async fn delete_memory(&self, id: &MemoryId) -> Result<()>;

    /// Delete a summary's text row. Idempotent.
    async fn delete_summary(&self, id: &SummaryId) -> Result<()>;

    /// Top-k matches for `query` under `scope`. Returns `(memory_id, score)`
    /// pairs ordered descending by score (higher = better).
    async fn search_memory(
        &self,
        query: &str,
        k: u32,
        scope: VectorScope,
    ) -> Result<Vec<(MemoryId, f32)>>;

    /// Top-k matches across summaries under `parent_path_prefix`.
    async fn search_summary(
        &self,
        query: &str,
        k: u32,
        parent_path_prefix: &str,
    ) -> Result<Vec<(SummaryId, f32)>>;

    /// Stable identifier for tracing (`"sqlite-fts5"`, …).
    fn id(&self) -> &str;

    /// Self-declared capabilities.
    fn capabilities(&self) -> LexicalIndexCapabilities;
}