Skip to main content
Glama

Re-embed Entities

graph_reembed
Idempotent

Regenerate semantic-search embeddings for entities. Fill missing embeddings by default; force full re-embed after changing the embed-text recipe.

Instructions

Regenerate semantic-search embeddings for entities. By default only fills missing embeddings (idempotent, fast). With force=true, re-embeds every entity — use after changing the embed-text recipe (e.g. when richer fields are added). At ~10ms per entity, full re-embed of a few hundred nodes finishes in seconds.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
forceNoRe-embed every entity, even ones that already have an embedding. Default false.

Implementation Reference

  • Registration of the graph_reembed MCP tool on the server. Defines the title 'Re-embed Entities', description, input schema (force boolean), and the handler function.
    // ─── Tool: graph_reembed ───
    
    server.registerTool("graph_reembed", {
      title: "Re-embed Entities",
      description:
        "Regenerate semantic-search embeddings for entities. By default only fills missing embeddings " +
        "(idempotent, fast). With force=true, re-embeds every entity — use after changing the embed-text " +
        "recipe (e.g. when richer fields are added). At ~10ms per entity, full re-embed of a few hundred " +
        "nodes finishes in seconds.",
      inputSchema: {
        force: z
          .boolean()
          .optional()
          .default(false)
          .describe("Re-embed every entity, even ones that already have an embedding. Default false."),
      },
      annotations: { idempotentHint: true },
    }, async ({ force }) => {
      try {
        const tenantId = currentTenant();
        // Admins re-embed across all tenants; others re-embed only their own.
        const opts: { force?: boolean; tenantId?: string } = { force: force === true };
        if (!isAdminTenant(tenantId)) opts.tenantId = tenantId;
        const result = await client.backfillEmbeddings(opts);
        return toolResult({ ...result, force: force === true, scope: isAdminTenant(tenantId) ? "all-tenants" : tenantId });
      } catch (err) {
        const e = err instanceof Error ? err : new Error(String(err));
        return toolError(`graph_reembed failed: ${e.message}`);
      }
    });
  • The handler function for graph_reembed. Accepts a `force` parameter, uses currentTenant() to determine scope, and delegates to client.backfillEmbeddings(). Admins re-embed across all tenants; non-admins re-embed only their own tenant.
    }, async ({ force }) => {
      try {
        const tenantId = currentTenant();
        // Admins re-embed across all tenants; others re-embed only their own.
        const opts: { force?: boolean; tenantId?: string } = { force: force === true };
        if (!isAdminTenant(tenantId)) opts.tenantId = tenantId;
        const result = await client.backfillEmbeddings(opts);
        return toolResult({ ...result, force: force === true, scope: isAdminTenant(tenantId) ? "all-tenants" : tenantId });
      } catch (err) {
        const e = err instanceof Error ? err : new Error(String(err));
        return toolError(`graph_reembed failed: ${e.message}`);
      }
    });
  • The backfillEmbeddings method on Neo4jClient. Queries for entities missing embeddings (or all if force=true), computes embeddings via embedText/buildEmbedText in parallel batches, writes them back, and returns counts of embedded/skipped/errors.
    async backfillEmbeddings(
      options: { tenantId?: string; batchSize?: number; force?: boolean } = {},
    ): Promise<{ embedded: number; skipped: number; errors: number }> {
      const batchSize = options.batchSize ?? 50;
      const force = options.force ?? false;
      const tenantId = options.tenantId;
    
      let embedded = 0;
      let errors = 0;
    
      // Track ids we've already processed in this run to ensure forward progress
      // across batches even when nothing is null (force mode just iterates all).
      // Note: ids are namespaced internally by tenant_id when tenant scoped, but
      // the Set holds raw ids — that's fine because the WHERE clause already
      // restricts to the same tenant.
      const processed = new Set<string>();
    
      const tenantClause = tenantId ? "AND n.tenant_id = $tenantId" : "";
    
      while (true) {
        const rows = await this.run(
          `
          MATCH (n:Entity)
          WHERE ${force ? "true" : "n.embedding IS NULL"}
            AND NOT n.id IN $processed
            ${tenantClause}
          RETURN n.id AS id,
                 n.tenant_id AS tenant_id,
                 n.name AS name,
                 [l IN labels(n) WHERE l <> 'Entity'][0] AS type,
                 properties(n) AS props
          LIMIT $batchSize
          `,
          { batchSize, processed: [...processed], ...(tenantId && { tenantId }) },
        );
        if (rows.length === 0) break;
    
        // Embed in parallel using the rich-context recipe
        const embeddings = await Promise.all(
          rows.map(async (r) => {
            const name = String(r["name"] ?? "");
            if (!name) return null;
            const type = r["type"] ? String(r["type"]) : undefined;
            const props = (r["props"] as Record<string, unknown>) ?? {};
            try {
              return await embedText(buildEmbedText(name, type, props));
            } catch { return null; }
          }),
        );
    
        // Write back (matched on tenant_id + id to avoid cross-tenant collisions)
        for (let i = 0; i < rows.length; i++) {
          const id = String(rows[i]["id"]);
          const rowTenantId = String(rows[i]["tenant_id"] ?? "");
          processed.add(id);
          const emb = embeddings[i];
          if (!emb) { errors++; continue; }
          try {
            await this.run(
              `MATCH (n:Entity {tenant_id: $tenantId, id: $id}) SET n.embedding = $embedding`,
              { tenantId: rowTenantId, id, embedding: emb },
            );
            embedded++;
          } catch (err) {
            debugLogClient(`backfill write failed for ${id} (tenant=${rowTenantId}): ${err instanceof Error ? err.message : String(err)}`);
            errors++;
          }
        }
    
        if (rows.length < batchSize) break;
      }
    
      // Count any remaining nulls (only meaningful when force=false)
      const remaining = await this.run(
        `MATCH (n:Entity) WHERE n.embedding IS NULL ${tenantClause} RETURN count(n) AS c`,
        tenantId ? { tenantId } : {},
      );
      const skipped = Number(remaining[0]?.["c"] ?? 0);
    
      return { embedded, skipped, errors };
    }
  • buildEmbedText helper that constructs the text fed into the embedder for an entity. Combines name, type, and high-signal properties (subtype, description, etc.) with em-dash separators.
    export function buildEmbedText(
      name: string,
      type: string | undefined,
      properties: Record<string, unknown> = {},
    ): string {
      const parts: string[] = [name.trim()];
      if (type) parts.push(type);
      // High-signal fields, in priority order. First non-empty wins.
      for (const key of ["subtype", "description", "role", "category", "what", "specialty"]) {
        const v = properties[key];
        if (typeof v === "string" && v.trim()) {
          parts.push(v.trim());
        }
      }
      return parts.join(" — ");
    }
  • embedText function that uses the HuggingFace transformers pipeline to generate 384-dim L2-normalized embeddings for a given text string.
    export async function embedText(text: string): Promise<number[]> {
      const cleaned = text.trim();
      if (!cleaned) return new Array<number>(EMBEDDING_DIM).fill(0);
      const e = await getEmbedder();
      const result = await e(cleaned, { pooling: "mean", normalize: true });
      // result.data is a Float32Array of length 384
      return Array.from(result.data as Float32Array);
    }
Behavior5/5

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

The description adds value beyond the idempotentHint annotation by explaining that the default is idempotent and fast, and providing a time estimate (~10ms per entity). It also clarifies the effect of force=true, giving the agent a clear behavioral model.

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?

The description consists of two concise sentences, front-loading the primary purpose and efficiently conveying the key details about behavior and performance. No unnecessary words.

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

Completeness5/5

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

For a simple tool with a single parameter and no output schema, the description covers the purpose, default vs. forced behavior, use case, and performance estimate. It is complete and self-contained.

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

Parameters4/5

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

The input schema already describes the parameter with a default and a brief description. The tool's description adds meaningful context by explaining when to set force=true (after recipe changes), which goes beyond the schema's description.

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 the tool's action ('Regenerate semantic-search embeddings for entities') and specifies the default behavior (only missing embeddings) versus the forced re-embed mode. This distinguishes it from other sibling tools by focusing on a specific operation.

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

Usage Guidelines4/5

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

The description provides explicit context for when to use force=true ('after changing the embed-text recipe'), indicating a clear usage scenario. However, it does not mention when to avoid the tool or compare it to alternatives among siblings.

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/stevepridemore/graph-memory'

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