Semantic Deduplication
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 →- 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.
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.
- Dakera, on request:
deduplicate()(POST /v1/knowledge/deduplicate) takes anagent_id, an optionalthresholdandmemory_type, anddry_run. Withdry_run: trueit only reports. It refuses (400) an agent with more than 5,000 memories to compare; narrow the run withmemory_type. - What a merge keeps: one memory survives; the absorbed text is kept in its
_dakera_duplicatesmetadata and the tag sets are merged (capped at 50).duplicates_skipped_changedcounts duplicates left alone because a record changed during the run. - Dakera, on a schedule: AutoPilot is on by default and runs the same deduplication every 6 hours for candidates above a cosine similarity of 0.93, keeping the memory with the higher retention score. A permanent memory is never folded into one that expires.
- Your application, for paraphrases: recall the memories that may state a fact, decide which ones do, store one canonical memory and
forget()the rest, or pass their IDs toPOST /v1/memory/consolidate.
Diagram: One Deduplication Run
Diagram: Dedup Merge Flow
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
-
Ingest memories normally; duplicates are cleaned up afterwardsStore 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.
-
Preview with dry_runCall
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 permemory_type. -
Run deduplication after bulk ingestion, and leave AutoPilot onCall
deduplicate()after a large import. AutoPilot repeats the same pass every 6 hours by default, so routine cleanup needs no scheduler of your own. -
Merge paraphrases in your applicationRecall 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. -
Validate recall diversity afterwardsSample 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
[
{ "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
[
{ "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.
SDK Reference
| Method | SDK | Purpose |
|---|---|---|
POST /v1/knowledge/deduplicate | REST | Merge an agent's true duplicates; dry_run only reports |
deduplicate(agent_id, threshold, dry_run) | Python | The same call |
deduplicate({agent_id, dry_run}) | TypeScript | The same call |
recall(agent_id, query, top_k) | Python | Find the memories that may state one fact |
store_memory(agent_id, content, importance, memory_type, tags) | Python | Store the canonical memory |
forget(agent_id, memory_id) | Python | Remove a paraphrase you folded into the canonical memory |
POST /v1/memory/consolidate | REST | Merge the memory_ids you pass into one memory |
autopilot_update_config(enabled, dedup_threshold, dedup_interval_hours) | Python | Admin: AutoPilot's deduplication settings |
Performance Considerations
- Narrow large agents. Above 5,000 memories to compare, a run is refused; pass
memory_typeto deduplicate one type at a time. - Preview before you merge.
dry_run: truereports without writing, so you can check a run against your expectations first. - One pass at a time. AutoPilot and decay passes share one lock (across replicas through Redis), and in a cluster they run on the elected leader only.
- Paraphrase checks cost a recall each. Run them where agents restate facts often, or in a batch job, rather than on every write.
Edge Cases
"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.
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.
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.
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.
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.
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.