NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee what's new →
Intermediate Lifecycle

Semantic Deduplication

⏰ ~20 min to implement 📦 Requires: Dakera v0.12+

Keep one memory per fact. Dakera's deduplication merges true duplicates, the same text stored more than once by repeated ingestion, multi-source pipelines or retries. Paraphrases of one fact are a second, application-side step: find them, decide, and replace them with one canonical memory.

Start Free →
Prerequisites
  • Running Dakera server (Quickstart)
  • An agent ID to scope memories to, and a key with write access to its namespace (_dakera_agent_{agent_id})
  • For AutoPilot settings: an admin key with no namespace restriction

The Problem: Redundant Memories Degrade Recall Quality

A research assistant agent processes 50 papers per day. Multiple papers state the same findings in different words. A user preference mentioned in Monday's chat appears again in Thursday's follow-up. A multi-agent pipeline where three agents independently discover and store the same fact creates three near-identical memories.

The result: a recall query for "user's Python preference" returns several memories that say the same thing, each taking up a retrieval slot. Diversity of results collapses. The agent surfaces the same information several times instead of distinct, relevant facts. Deduplication restores recall diversity by ensuring one concept maps to one memory.

Exact Duplicates vs. Paraphrases

POST /v1/knowledge/deduplicate compares an agent's memories pairwise and merges only true duplicates: the same content words in the same order and the same entity tags. "User prefers Python" stored twice is merged. "User prefers Python" and "User codes primarily in Python" are not, and neither are "I have two kids" and "I have three kids". Merging paraphrases is a decision your application makes, with its own rule or an LLM; this page shows both steps.

Architecture

Deduplication has two layers. Dakera removes true duplicates on the server, on request or on a schedule. Your application merges paraphrases where it matters, with ordinary memory calls.

Diagram: One Deduplication Run

Workflow of one deduplication run. Your agent calls POST /v1/knowledge/deduplicate with agent_id and dry_run, or AutoPilot runs it every 6 hours. An agent with more than 5,000 memories to compare gets a 400 and should narrow the run with memory_type. Otherwise Dakera compares the memories pairwise, above the similarity threshold, and checks whether each pair is a true duplicate: the same words in the same order and the same entity tags. Paraphrases and distinct facts are kept. True duplicates are merged: one memory survives, with the absorbed text in _dakera_duplicates and the tag sets merged up to 50. The run answers a report; with dry_run it only reports.Workflow of one deduplication run. Your agent calls POST /v1/knowledge/deduplicate with agent_id and dry_run, or AutoPilot runs it every 6 hours. An agent with more than 5,000 memories to compare gets a 400 and should narrow the run with memory_type. Otherwise Dakera compares the memories pairwise, above the similarity threshold, and checks whether each pair is a true duplicate: the same words in the same order and the same entity tags. Paraphrases and distinct facts are kept. True duplicates are merged: one memory survives, with the absorbed text in _dakera_duplicates and the tag sets merged up to 50. The run answers a report; with dry_run it only reports.
Only true duplicates collapse; paraphrases and distinct facts are left for your application to judge.Full size ↗Full size ↗

Diagram: Dedup Merge Flow

Data flow of the before and after example. Before there are five memories: m-001 and m-002 both say User likes Python (importance 0.70), m-003 says User codes primarily in Python (0.80), and m-004 and m-005 are about hiking and Acme Corp. Dakera deduplication merges m-001 and m-002, which are true duplicates, into one surviving memory and keeps m-003 because it is a paraphrase. Your application then judges the surviving memory and m-003 to be the same fact, stores one canonical memory, User is a Python developer, at the higher importance 0.80, and forgets the two. After: the canonical memory, m-004 and m-005.Data flow of the before and after example. Before there are five memories: m-001 and m-002 both say User likes Python (importance 0.70), m-003 says User codes primarily in Python (0.80), and m-004 and m-005 are about hiking and Acme Corp. Dakera deduplication merges m-001 and m-002, which are true duplicates, into one surviving memory and keeps m-003 because it is a paraphrase. Your application then judges the surviving memory and m-003 to be the same fact, stores one canonical memory, User is a Python developer, at the higher importance 0.80, and forgets the two. After: the canonical memory, m-004 and m-005.
Dakera folds the repeated text; your application folds the paraphrase into one canonical memory.Full size ↗Full size ↗

Real-World Scenario: Research Assistant Deduplicating Paper Summaries

Scenario: PaperMind AI builds a research assistant that ingests academic paper abstracts and stores one summary memory per paper. Researchers submit the same landmark papers from multiple sources (arxiv, Semantic Scholar, direct PDF upload), so the same paper arrives several times.

When two sources carry the same abstract, the stored memories are true duplicates, and PaperMind's deduplicate() call after each import (plus AutoPilot in between) merges them. When the sources word the abstract differently, Dakera keeps both, so PaperMind adds its own check: before storing a summary, it recalls the agent's memories for the paper title and skips the write when its rule says the paper is already there. A query for "transformer attention mechanisms" then returns distinct papers instead of several copies of one.

Step-by-Step Implementation

  1. Ingest memories normally; duplicates are cleaned up afterwards
    Store memories as usual. A write-time check costs a recall per write, so keep it for the places where paraphrases are common (step 4). True duplicates are left to Dakera's deduplication.
  2. Preview with dry_run
    Call deduplicate(agent_id, dry_run=True) to see what would be merged before anything changes. On an agent with more than 5,000 memories to compare, run it per memory_type.
  3. Run deduplication after bulk ingestion, and leave AutoPilot on
    Call deduplicate() after a large import. AutoPilot repeats the same pass every 6 hours by default, so routine cleanup needs no scheduler of your own.
  4. Merge paraphrases in your application
    Recall the memories for a fact, decide which ones state it (your rule or an LLM; recall scores rank results, they are not a similarity cut-off), store one canonical memory with the highest importance and the union of the tags you need, and forget() the originals.
  5. Validate recall diversity afterwards
    Sample a few recall queries before and after a cleanup and compare how many distinct facts come back for the same top_k.

Before & After: Memory Store State

Before: 5 memories, one fact stored three times
[
  { "id": "m-001", "content": "User likes Python", "importance": 0.70 },
  { "id": "m-002", "content": "User likes Python", "importance": 0.70 },
  { "id": "m-003", "content": "User codes primarily in Python", "importance": 0.80 },
  { "id": "m-004", "content": "User enjoys hiking on weekends", "importance": 0.65 },
  { "id": "m-005", "content": "User works at Acme Corp", "importance": 0.90 }
]
// recall("programming language") spends three slots on one fact
// hiking and Acme Corp get pushed out
After: Dakera merges the two copies, your app folds the paraphrase
[
  { "id": "m-006", "content": "User is a Python developer", "importance": 0.80 },
  { "id": "m-004", "content": "User enjoys hiking on weekends", "importance": 0.65 },
  { "id": "m-005", "content": "User works at Acme Corp", "importance": 0.90 }
]
// deduplicate(): m-001 and m-002 were true duplicates; one survives
// your app: the survivor and m-003 state one fact; stored m-006, forgot both

Implementation

# 1. The same fact arrives twice (a retried import, two sources with the same text)
curl -X POST http://localhost:3000/v1/memory/store \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "research-bot", "content": "BERT: Pre-training of Deep Bidirectional Transformers", "importance": 0.85, "memory_type": "semantic", "tags": ["paper", "nlp"]}'

# 2. Preview what deduplication would merge (nothing is changed)
curl -X POST http://localhost:3000/v1/knowledge/deduplicate \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "research-bot", "memory_type": "semantic", "dry_run": true}'

# 3. Merge the true duplicates
curl -X POST http://localhost:3000/v1/knowledge/deduplicate \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "research-bot", "memory_type": "semantic", "dry_run": false}'

# 4. Paraphrases are your call: find them...
curl -X POST http://localhost:3000/v1/memory/recall \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "research-bot", "query": "original transformer paper", "top_k": 10}'

# ...and merge the ones you decide state the same fact into one memory
curl -X POST http://localhost:3000/v1/memory/consolidate \
  -H "Authorization: Bearer $DAKERA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "research-bot", "memory_ids": ["mem_a", "mem_b"]}'
from dakera import DakeraClient

client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")
AGENT = "research-bot"

# Ingest paper summaries; the same abstract can arrive from several sources
papers = [
    ("BERT: Pre-training of Deep Bidirectional Transformers", 0.85),
    ("BERT: Pre-training of Deep Bidirectional Transformers", 0.82),  # same text, second source
    ("Attention Is All You Need: the transformer architecture", 0.90),
    ("The original transformer paper by Vaswani et al.", 0.88),        # paraphrase of the line above
    ("GPT-3: Language Models are Few-Shot Learners", 0.87),
]
for content, importance in papers:
    client.store_memory(agent_id=AGENT, content=content, importance=importance,
                        memory_type="semantic", tags=["paper", "nlp"])

# Dakera: preview, then merge true duplicates (same words, same order, same entity tags)
preview = client.deduplicate(agent_id=AGENT, dry_run=True)
print(preview)
client.deduplicate(agent_id=AGENT, dry_run=False)

# Your application: merge paraphrases of one fact
def same_fact(a: str, b: str) -> bool:
    """Your rule or an LLM call. Dakera's scores rank results; they are not a similarity cut-off."""
    ...

fact = "The original transformer paper (Attention Is All You Need)"
response = client.recall(agent_id=AGENT, query=fact, top_k=10)
same = [m for m in response.memories if same_fact(m.content, fact)]
if len(same) > 1:
    client.store_memory(agent_id=AGENT, content=fact, memory_type="semantic",
                        importance=max(m.importance for m in same), tags=["paper", "nlp"])
    for m in same:
        client.forget(agent_id=AGENT, memory_id=m.id)
import { DakeraClient } from '@dakera-ai/dakera';

const client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-...' });
const AGENT = 'research-bot';

const papers = [
  { content: 'BERT: Pre-training of Deep Bidirectional Transformers', importance: 0.85 },
  { content: 'BERT: Pre-training of Deep Bidirectional Transformers', importance: 0.82 }, // same text
  { content: 'Attention Is All You Need: the transformer architecture', importance: 0.9 },
  { content: 'The original transformer paper by Vaswani et al.', importance: 0.88 },      // paraphrase
];
for (const p of papers) {
  await client.storeMemory(AGENT, { ...p, memory_type: 'semantic', tags: ['paper', 'nlp'] });
}

// Dakera: preview, then merge true duplicates
console.log(await client.deduplicate({ agent_id: AGENT, dry_run: true }));
await client.deduplicate({ agent_id: AGENT, dry_run: false });

// Your application: merge paraphrases you judge to be one fact
declare function sameFact(a: string, b: string): Promise<boolean>; // your rule or an LLM call

const fact = 'The original transformer paper (Attention Is All You Need)';
const { memories } = await client.recall(AGENT, fact, { top_k: 10 });
const same = [];
for (const m of memories) if (await sameFact(m.content, fact)) same.push(m);
if (same.length > 1) {
  await client.storeMemory(AGENT, {
    content: fact,
    memory_type: 'semantic',
    importance: Math.max(...same.map(m => m.importance)),
    tags: ['paper', 'nlp'],
  });
  for (const m of same) await client.forget(AGENT, m.id);
}
use dakera_client::{DakeraClient, DeduplicateRequest, ForgetRequest, RecallRequest, StoreMemoryRequest};

let client = DakeraClient::builder("http://localhost:3000").api_key("dk-...").build()?;
let agent = "research-bot";

for (content, importance) in [
    ("BERT: Pre-training of Deep Bidirectional Transformers", 0.85),
    ("BERT: Pre-training of Deep Bidirectional Transformers", 0.82), // same text
    ("The original transformer paper by Vaswani et al.", 0.88),
] {
    client.store_memory(
        StoreMemoryRequest::new(agent, content)
            .with_importance(importance)
            .with_tags(vec!["paper".into(), "nlp".into()]),
    ).await?;
}

// Dakera: preview, then merge true duplicates
let preview = client.deduplicate(DeduplicateRequest {
    agent_id: agent.into(), threshold: None, memory_type: None, dry_run: true,
}).await?;
println!("{} duplicates found", preview.duplicates_found);
client.deduplicate(DeduplicateRequest {
    agent_id: agent.into(), threshold: None, memory_type: None, dry_run: false,
}).await?;

// Your application: replace paraphrases you judge to be one fact
let fact = "The original transformer paper (Attention Is All You Need)";
let found = client.recall(RecallRequest::new(agent, fact).with_top_k(10)).await?;
let same: Vec<String> = found.memories.iter()
    .filter(|m| same_fact(&m.content, fact)) // your rule or an LLM call
    .map(|m| m.id.clone())
    .collect();
if same.len() > 1 {
    client.store_memory(StoreMemoryRequest::new(agent, fact).with_importance(0.88)).await?;
    client.forget(ForgetRequest::by_ids(agent, same)).await?;
}
ctx := context.Background()
client := dakera.NewClientWithOptions(dakera.ClientOptions{
    BaseURL: "http://localhost:3000",
    APIKey:  os.Getenv("DAKERA_API_KEY"),
})
agent := "research-bot"

for _, content := range []string{
    "BERT: Pre-training of Deep Bidirectional Transformers",
    "BERT: Pre-training of Deep Bidirectional Transformers", // same text
    "The original transformer paper by Vaswani et al.",
} {
    imp := float32(0.85)
    client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{
        Content: content, Importance: &imp, Tags: []string{"paper", "nlp"},
    })
}

// Dakera: preview, then merge true duplicates
preview, _ := client.Deduplicate(ctx, dakera.DeduplicateRequest{AgentID: agent, DryRun: true})
fmt.Printf("%+v\n", preview)
client.Deduplicate(ctx, dakera.DeduplicateRequest{AgentID: agent})

// Your application: replace paraphrases you judge to be one fact
fact := "The original transformer paper (Attention Is All You Need)"
found, _ := client.Recall(ctx, agent, dakera.RecallRequest{Query: fact, TopK: 10})
var same []string
for _, m := range found.Memories {
    if sameFact(m.Content, fact) { // your rule or an LLM call
        same = append(same, m.ID)
    }
}
if len(same) > 1 {
    imp := float32(0.88)
    client.StoreMemory(ctx, agent, dakera.StoreMemoryRequest{Content: fact, Importance: &imp})
    for _, id := range same {
        client.Forget(ctx, agent, id)
    }
}

Duplicates cleaned up on the server

Dakera's deduplication runs on request or every 6 hours with AutoPilot, in the free self-hosted server.

Deploy Free →

SDK Reference

MethodSDKPurpose
POST /v1/knowledge/deduplicateRESTMerge an agent's true duplicates; dry_run only reports
deduplicate(agent_id, threshold, dry_run)PythonThe same call
deduplicate({agent_id, dry_run})TypeScriptThe same call
recall(agent_id, query, top_k)PythonFind the memories that may state one fact
store_memory(agent_id, content, importance, memory_type, tags)PythonStore the canonical memory
forget(agent_id, memory_id)PythonRemove a paraphrase you folded into the canonical memory
POST /v1/memory/consolidateRESTMerge the memory_ids you pass into one memory
autopilot_update_config(enabled, dedup_threshold, dedup_interval_hours)PythonAdmin: AutoPilot's deduplication settings

Performance Considerations

5,000
Memories one deduplicate call compares (400 above it)
6 h
AutoPilot deduplication interval (default)
0.93
AutoPilot candidate similarity (default)

Edge Cases

Edge Case 1: Similar but Factually Distinct Memories

"User worked at Google in 2018" and "User works at Google" look alike but state different facts. Dakera's deduplication never merges them (the words differ). Your paraphrase step might, so make your rule keep dates and tense apart and review its decisions for the first runs. For an updated fact, Dakera's knowledge graph adds a supersedes edge when a newer memory is very similar to an older one and shares an entity with it, and recall then demotes the outdated one.

Edge Case 2: Large Agents

An agent with more than 5,000 memories to compare gets a 400 from deduplicate(). Run it once per memory_type, or split very large stores across agents.

Edge Case 3: Keeping Tag Diversity

Dakera's merge combines the tag sets of the merged memories (capped at 50). Your own paraphrase merge does not: give the canonical memory the union of the tags you need, such as ["python", "backend"] and ["python", "ml"], so it stays reachable through all of them.

Edge Case 4: Ingesting the Same Paper from Different Sources

Identical abstracts from arxiv, Semantic Scholar and direct uploads are true duplicates and are merged by deduplicate(). Abstracts that differ per source are paraphrases: for high-frequency ingestion, recall by title before storing and skip the write when your rule finds the paper; for the rest, run the paraphrase step in a batch job.

Edge Case 5: Coordinating Dedup in Multi-Agent Setups

Deduplication works on one agent's memories. If several agents write to one shared agent_id, Dakera's own passes are serialized (they share one lock), but two copies of your paraphrase job could still create the same canonical memory twice. Run that job from one worker, or serialize it per agent with a lock of your own.

Advanced Configuration: AutoPilot & Scheduling

AutoPilot Deduplication Settings

AutoPilot's deduplication interval and candidate threshold are server settings (DAKERA_AUTOPILOT_DEDUP_INTERVAL_HOURS, DAKERA_AUTOPILOT_DEDUP_THRESHOLD) and can be changed at runtime with an admin key that has no namespace restriction:

client.autopilot_update_config(enabled=True, dedup_threshold=0.93, dedup_interval_hours=6)
client.autopilot_trigger("dedup")   # run a deduplication pass now
status = client.autopilot_status()  # last runs

Scheduling Recommendation

Leave AutoPilot on for routine cleanup. Call deduplicate() yourself after a bulk import, when you want the result before the next AutoPilot run. Schedule the paraphrase step where your agents restate facts often, and review what it merged.

Try it live

Run this pattern against a real Dakera.

The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.