remem-ai 0.3.8

Persistent memory for Claude Code — single binary, zero subprocesses
Documentation
use std::collections::HashMap;

use anyhow::Result;
use rusqlite::Connection;

use crate::memory::{self, Memory};

use super::super::common::{paginate_memories, rrf_fuse, sanitize_fts_query};

fn load_ordered_memories(conn: &Connection, ids: &[i64]) -> Result<Vec<Memory>> {
    let loaded = memory::get_memories_by_ids(conn, ids, None)?;
    let id_to_memory: HashMap<i64, Memory> = loaded.into_iter().map(|memory| (memory.id, memory)).collect();
    Ok(ids.iter().filter_map(|id| id_to_memory.get(id).cloned()).collect())
}

pub(super) fn search_with_query(
    conn: &Connection,
    query_text: &str,
    project: Option<&str>,
    memory_type: Option<&str>,
    limit: i64,
    offset: i64,
    include_stale: bool,
    branch: Option<&str>,
) -> Result<Vec<Memory>> {
    let page_target = (limit.max(1) + offset.max(0) + 1).max(2);
    let fetch = page_target * 3;
    let expanded = crate::query_expand::expand_query(query_text);
    let expanded_refs: Vec<&str> = expanded.iter().map(|token| token.as_str()).collect();
    let long_tokens: Vec<&str> = expanded_refs
        .iter()
        .filter(|token| token.chars().count() >= 3)
        .copied()
        .collect();

    let core_tokens = crate::query_expand::core_tokens(query_text);
    let core_refs: Vec<&str> = core_tokens.iter().map(|token| token.as_str()).collect();
    let mut channels: Vec<Vec<i64>> = Vec::new();

    if !long_tokens.is_empty() {
        let safe_query = sanitize_fts_query(&long_tokens.join(" "));
        let fts = memory::search_memories_fts_filtered(
            conn,
            &safe_query,
            project,
            memory_type,
            fetch,
            0,
            include_stale,
            branch,
        )?;
        channels.push(fts.iter().map(|memory| memory.id).collect());
    }

    let entity_ids = crate::entity::search_by_entity_filtered(
        conn,
        query_text,
        project,
        memory_type,
        branch,
        fetch,
        include_stale,
    )?;
    if !entity_ids.is_empty() {
        channels.push(entity_ids);
    }

    if let Some(temporal_constraint) = crate::temporal::extract_temporal(query_text) {
        let temporal_ids = crate::temporal::search_by_time_filtered(
            conn,
            &temporal_constraint,
            project,
            memory_type,
            branch,
            fetch,
            include_stale,
        )?;
        if !temporal_ids.is_empty() {
            channels.push(temporal_ids);
        }
    }

    let like = memory::search_memories_like_filtered(
        conn,
        &core_refs,
        project,
        memory_type,
        fetch,
        0,
        include_stale,
        branch,
    )?;
    if !like.is_empty() {
        channels.push(like.iter().map(|memory| memory.id).collect());
    }

    if channels.is_empty() {
        return Ok(vec![]);
    }

    let final_ids: Vec<i64> = rrf_fuse(&channels, 60.0).iter().map(|(id, _)| *id).collect();
    let ordered = load_ordered_memories(conn, &final_ids)?;
    Ok(paginate_memories(ordered, limit, offset))
}