Skip to main content
Glama

memory_search

Search across memories and journal entries using semantic meaning similarity, with automatic fallback to keyword search when needed.

Instructions

Semantic search across all memories and journal entries. Returns results ranked by meaning-similarity. Falls back to keyword search if Ollama is not available.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesWhat to search for (natural language)
categoryNoFilter by category
projectNoFilter by project name
topKNoNumber of results (default: 5)

Implementation Reference

  • The handleMemorySearch function is the main handler for the 'memory_search' tool. It calls searchMemories and formats results with relevance scores, dates, and fallback notes.
    import { searchMemories, type SearchResult } from "../memory/search.js";
    import { isUsingFallback } from "../memory/embeddings.js";
    
    export async function handleMemorySearch(
      query: string,
      options?: { category?: string; project?: string; topK?: number },
    ): Promise<string> {
      const results = await searchMemories(query, {
        topK: options?.topK ?? 5,
        category: options?.category,
        project: options?.project,
      });
    
      if (results.length === 0) {
        return `No memories found matching "${query}".`;
      }
    
      const lines = results.map((r, i) => formatResult(r, i + 1));
      const fallbackNote = isUsingFallback() ? "\n\n*Using keyword search — install Ollama for semantic search.*" : "";
      return `Found ${results.length} result(s) for "${query}":\n\n${lines.join("\n\n")}${fallbackNote}`;
    }
    
    function formatResult(r: SearchResult, rank: number): string {
      const date = new Date(r.created_at).toISOString().slice(0, 10);
      const score = (r.score * 100).toFixed(0);
      const project = r.project ? ` [${r.project}]` : "";
      const source = r.source === "journal" ? " (journal)" : ` (${r.category})`;
    
      return `**${rank}. ${score}% match**${project}${source} — ${date}\n${r.content}`;
    }
  • Registration of the 'memory_search' tool with Zod schema definitions for query (required string), category (enum filter), project (optional string), and topK (optional number).
    server.tool(
      "memory_search",
      "Semantic search across all memories and journal entries. Returns results ranked by meaning-similarity. Falls back to keyword search if Ollama is not available.",
      {
        query: z.string().describe("What to search for (natural language)"),
        category: z
          .enum(["decision", "preference", "fact", "episode", "lesson", "architecture", "framework", "general"])
          .optional()
          .describe("Filter by category"),
        project: z.string().optional().describe("Filter by project name"),
        topK: z.number().optional().describe("Number of results (default: 5)"),
      },
      async ({ query, category, project, topK }) => {
        try {
          const result = await handleMemorySearch(query, { category, project, topK });
          return { content: [{ type: "text", text: result }] };
        } catch (err) {
          return {
            content: [{ type: "text", text: `Error searching memories: ${err}` }],
            isError: true,
          };
        }
      },
    );
  • The searchMemories function implements the actual search logic with semantic embedding-based matching (via Ollama) and fallback to keyword search. It queries both memories and journal_entries tables.
    export async function searchMemories(
      query: string,
      options: {
        topK?: number;
        category?: string;
        project?: string;
        includeArchived?: boolean;
        minScore?: number;
      } = {},
    ): Promise<SearchResult[]> {
      const {
        topK = 5,
        category,
        project,
        includeArchived = false,
        minScore = 0.3,
      } = options;
    
      // Fall back to keyword search when Ollama is unavailable
      if (isUsingFallback()) {
        return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 });
      }
    
      const db = getDb();
      const queryEmbedding = await embed(query);
    
      if (!queryEmbedding) {
        return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 });
      }
    
      let memorySql = `SELECT id, content, category, project, created_at, embedding FROM memories WHERE embedding IS NOT NULL`;
      const params: unknown[] = [];
    
      if (!includeArchived) {
        memorySql += ` AND archived = 0`;
      }
      if (category) {
        memorySql += ` AND category = ?`;
        params.push(category);
      }
      if (project) {
        memorySql += ` AND project = ?`;
        params.push(project);
      }
    
      const memories = db.prepare(memorySql).all(...params) as Array<{
        id: string;
        content: string;
        category: string;
        project: string | null;
        created_at: number;
        embedding: Buffer;
      }>;
    
      let journalSql = `SELECT id, content, project, created_at, embedding FROM journal_entries WHERE embedding IS NOT NULL`;
      const journalParams: unknown[] = [];
    
      if (project) {
        journalSql += ` AND project = ?`;
        journalParams.push(project);
      }
    
      const journals = db.prepare(journalSql).all(...journalParams) as Array<{
        id: string;
        content: string;
        project: string | null;
        created_at: number;
        embedding: Buffer;
      }>;
    
      const results: SearchResult[] = [];
    
      for (const mem of memories) {
        const memEmbedding = bufferToEmbedding(mem.embedding);
        const score = cosineSimilarity(queryEmbedding, memEmbedding);
        if (score >= minScore) {
          results.push({
            id: mem.id,
            content: mem.content,
            category: mem.category,
            project: mem.project,
            score,
            created_at: mem.created_at,
            source: "memory",
          });
        }
      }
    
      for (const entry of journals) {
        const entryEmbedding = bufferToEmbedding(entry.embedding);
        const score = cosineSimilarity(queryEmbedding, entryEmbedding);
        if (score >= minScore) {
          results.push({
            id: entry.id,
            content: entry.content,
            category: "journal",
            project: entry.project,
            score,
            created_at: entry.created_at,
            source: "journal",
          });
        }
      }
    
      results.sort((a, b) => b.score - a.score);
    
      const updateAccess = db.prepare(
        `UPDATE memories SET accessed_at = ?, access_count = access_count + 1 WHERE id = ?`,
      );
      const now = Date.now();
      for (const r of results.slice(0, topK)) {
        if (r.source === "memory") {
          updateAccess.run(now, r.id);
        }
      }
    
      db.prepare(`INSERT INTO search_log (query, results_count, created_at) VALUES (?, ?, ?)`).run(
        query,
        Math.min(results.length, topK),
        now,
      );
    
      return results.slice(0, topK);
    }
  • The keywordSearch function provides the fallback search when Ollama is unavailable, using token-based keyword matching against memories and journal entries.
    function keywordSearch(
      query: string,
      options: {
        topK: number;
        category?: string;
        project?: string;
        includeArchived: boolean;
        minScore: number;
      },
    ): SearchResult[] {
      const db = getDb();
      const queryTokens = tokenize(query);
      if (queryTokens.length === 0) return [];
    
      let memorySql = `SELECT id, content, category, project, created_at FROM memories WHERE 1=1`;
      const params: unknown[] = [];
    
      if (!options.includeArchived) {
        memorySql += ` AND archived = 0`;
      }
      if (options.category) {
        memorySql += ` AND category = ?`;
        params.push(options.category);
      }
      if (options.project) {
        memorySql += ` AND project = ?`;
        params.push(options.project);
      }
    
      const memories = db.prepare(memorySql).all(...params) as Array<{
        id: string;
        content: string;
        category: string;
        project: string | null;
        created_at: number;
      }>;
    
      let journalSql = `SELECT id, content, project, created_at FROM journal_entries WHERE 1=1`;
      const journalParams: unknown[] = [];
      if (options.project) {
        journalSql += ` AND project = ?`;
        journalParams.push(options.project);
      }
    
      const journals = db.prepare(journalSql).all(...journalParams) as Array<{
        id: string;
        content: string;
        project: string | null;
        created_at: number;
      }>;
    
      const results: SearchResult[] = [];
    
      for (const mem of memories) {
        const score = keywordScore(queryTokens, mem.content);
        if (score >= options.minScore) {
          results.push({
            id: mem.id,
            content: mem.content,
            category: mem.category,
            project: mem.project,
            score,
            created_at: mem.created_at,
            source: "memory",
          });
        }
      }
    
      for (const entry of journals) {
        const score = keywordScore(queryTokens, entry.content);
        if (score >= options.minScore) {
          results.push({
            id: entry.id,
            content: entry.content,
            category: "journal",
            project: entry.project,
            score,
            created_at: entry.created_at,
            source: "journal",
          });
        }
      }
    
      results.sort((a, b) => b.score - a.score);
    
      const now = Date.now();
      const updateAccess = db.prepare(
        `UPDATE memories SET accessed_at = ?, access_count = access_count + 1 WHERE id = ?`,
      );
      for (const r of results.slice(0, options.topK)) {
        if (r.source === "memory") {
          updateAccess.run(now, r.id);
        }
      }
    
      db.prepare(`INSERT INTO search_log (query, results_count, created_at) VALUES (?, ?, ?)`).run(
        query,
        Math.min(results.length, options.topK),
        now,
      );
    
      return results.slice(0, options.topK);
    }
  • The embed function generates embeddings via Ollama (nomic-embed-text model), and cosineSimilarity computes relevance scores for semantic search.
    export async function embed(text: string): Promise<Float32Array | null> {
      const available = await ensureOllama();
      if (!available) return null;
    
      const client = getClient();
      const response = await client.embed({ model: MODEL, input: text });
      return new Float32Array(response.embeddings[0]);
    }
    
    export async function embedMany(texts: string[]): Promise<(Float32Array | null)[]> {
      if (texts.length === 0) return [];
    
      const available = await ensureOllama();
      if (!available) return texts.map(() => null);
    
      const client = getClient();
      const response = await client.embed({ model: MODEL, input: texts });
      return response.embeddings.map((e) => new Float32Array(e));
    }
    
    export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
      let dot = 0;
      let normA = 0;
      let normB = 0;
      for (let i = 0; i < a.length; i++) {
        dot += a[i] * b[i];
        normA += a[i] * a[i];
        normB += b[i] * b[i];
      }
      const denom = Math.sqrt(normA) * Math.sqrt(normB);
      return denom === 0 ? 0 : dot / denom;
    }
    
    export function embeddingToBuffer(embedding: Float32Array): Buffer {
      return Buffer.from(embedding.buffer);
    }
    
    export function bufferToEmbedding(buffer: Buffer): Float32Array {
      return new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4);
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description must carry full burden. It mentions fallback to keyword search if Ollama unavailable, but does not disclose whether it modifies data, auth needs, rate limits, or other behavioral traits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences: first states purpose and scope, second adds fallback behavior. No redundant information, efficient and front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema, but description states results are ranked by meaning-similarity. Complexities like pagination, result structure, or no-result behavior are omitted, but for a simple search tool it's nearly complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so baseline is 3. Description adds context about semantic search but does not provide additional parameter-specific meaning beyond schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it performs semantic search across memories and journal entries, returning results ranked by meaning-similarity. This differentiates it from sibling tools like memory_journal or memory_recent by scope and method.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for semantic similarity search but lacks explicit guidance on when to use versus alternatives like keyword search or other memory tools. No when-not-to-use conditions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DomDemetz/claude-soul'

If you have feedback or need assistance with the MCP directory API, please join our Discord server