docs: add wiki guide for isolated memory spaces

2026-05-11 14:49:57 +09:00
parent c80a52a241
commit ae056a7ad1
4 changed files with 171 additions and 7 deletions

@@ -25,6 +25,7 @@ await db.initialize();
This creates the tables IdentityDB needs: This creates the tables IdentityDB needs:
- `spaces`
- `topics` - `topics`
- `facts` - `facts`
- `fact_topics` - `fact_topics`
@@ -32,7 +33,23 @@ This creates the tables IdentityDB needs:
- `topic_aliases` - `topic_aliases`
- `fact_embeddings` - `fact_embeddings`
## 3. Add structured facts directly ## 3. Create or use isolated memory spaces
If you want independent memory graphs for different people, tenants, projects, or contexts, use spaces.
```ts
await db.upsertSpace({ name: 'A' });
await db.upsertSpace({ name: 'B' });
const spaces = await db.listSpaces();
const alpha = await db.getSpaceByName('A');
```
Any write or read can then opt into a specific space with `spaceName`.
See [Memory Spaces](Memory-Spaces) for the full model.
## 4. Add structured facts directly
Use `addFact()` when your application already knows the topics it wants to attach. Use `addFact()` when your application already knows the topics it wants to attach.
@@ -54,7 +71,7 @@ await db.addFact({
}); });
``` ```
## 4. Model topic hierarchy explicitly ## 5. Model topic hierarchy explicitly
Use `linkTopics()` when you want hierarchy to be explicit rather than inferred. Use `linkTopics()` when you want hierarchy to be explicit rather than inferred.
@@ -74,7 +91,7 @@ This is useful for reasoning such as:
- `Bun` is a kind of `runtime` - `Bun` is a kind of `runtime`
- `PostgreSQL` is a kind of `database` - `PostgreSQL` is a kind of `database`
## 5. Add aliases for canonical topic resolution ## 6. Add aliases for canonical topic resolution
```ts ```ts
await db.addTopicAlias('TypeScript', 'TS'); await db.addTopicAlias('TypeScript', 'TS');
@@ -84,7 +101,7 @@ const canonicalTopic = await db.getTopicByName('TS', { includeFacts: true });
This keeps one canonical topic row while still allowing alternate spellings or shorthand forms. This keeps one canonical topic row while still allowing alternate spellings or shorthand forms.
## 6. Ingest free-form text through an extractor ## 7. Ingest free-form text through an extractor
When your application starts from raw text, use `ingestStatement()`. When your application starts from raw text, use `ingestStatement()`.
@@ -119,7 +136,7 @@ await db.ingestStatement('I have worked with Bun and TypeScript since 2025.', {
See [Extractors](Extractors) for a deeper explanation of the trade-offs. See [Extractors](Extractors) for a deeper explanation of the trade-offs.
## 7. Add semantic search ## 8. Add semantic search
IdentityDB keeps semantic search provider-agnostic through an `EmbeddingProvider` interface. IdentityDB keeps semantic search provider-agnostic through an `EmbeddingProvider` interface.
@@ -147,7 +164,7 @@ const matches = await db.searchFacts({
}); });
``` ```
## 8. Enable duplicate-aware ingestion ## 9. Enable duplicate-aware ingestion
If you also provide an embedding provider during ingestion, IdentityDB can check whether a semantically similar fact already exists. If you also provide an embedding provider during ingestion, IdentityDB can check whether a semantically similar fact already exists.
@@ -161,7 +178,7 @@ await db.ingestStatement('Bun makes TypeScript tooling fast.', {
If a close enough match already exists, IdentityDB can return the existing fact instead of writing a duplicate. If a close enough match already exists, IdentityDB can return the existing fact instead of writing a duplicate.
## 9. Close the connection ## 10. Close the connection
```ts ```ts
await db.close(); await db.close();

@@ -16,6 +16,7 @@ IdentityDB is designed as the answer to those problems.
IdentityDB turns memory into a relational graph with a stable application API: IdentityDB turns memory into a relational graph with a stable application API:
- **Spaces** isolate independent memory graphs such as `A` and `B` so they behave like separate dimensions
- **Topics** are named nodes such as `TypeScript`, `Bun`, `2025`, or `programming language` - **Topics** are named nodes such as `TypeScript`, `Bun`, `2025`, or `programming language`
- **Facts** are statements such as `I have worked with TypeScript since 2025.` - **Facts** are statements such as `I have worked with TypeScript since 2025.`
- **Fact-topic links** connect one fact to many topics, which lets a single statement become a graph edge between concepts - **Fact-topic links** connect one fact to many topics, which lets a single statement become a graph edge between concepts
@@ -30,6 +31,7 @@ This gives you a memory system that is easier to inspect than a black-box vector
- Connect to **SQLite, PostgreSQL, MySQL, and MariaDB** - Connect to **SQLite, PostgreSQL, MySQL, and MariaDB**
- Initialize the required schema automatically - Initialize the required schema automatically
- Add facts and topics directly through a typed API - Add facts and topics directly through a typed API
- Split memory into hard-isolated spaces so one tenant, person, or project cannot accidentally connect to another
- Ingest free-form text through pluggable extractors - Ingest free-form text through pluggable extractors
- Resolve aliases to canonical topics - Resolve aliases to canonical topics
- Traverse parent/child topic relationships - Traverse parent/child topic relationships
@@ -60,6 +62,7 @@ That means IdentityDB can answer more than plain keyword lookup. It can tell you
## Recommended reading order ## Recommended reading order
- [Getting Started](Getting-Started) — installation, initialization, and concrete examples - [Getting Started](Getting-Started) — installation, initialization, and concrete examples
- [Memory Spaces](Memory-Spaces) — how to keep separate memory graphs isolated
- [Extractors](Extractors) — when to use `NaiveExtractor` vs `LlmFactExtractor` - [Extractors](Extractors) — when to use `NaiveExtractor` vs `LlmFactExtractor`
## Repository ## Repository

143
Memory-Spaces.md Normal file

@@ -0,0 +1,143 @@
# Memory Spaces
IdentityDB supports **hard-isolated memory spaces** so different people, tenants, projects, or contexts can live in separate graphs.
Think of each space as a separate dimension:
- topics in space `A` do not automatically connect to topics in space `B`
- aliases are resolved inside one space only
- hierarchy traversal stays inside one space
- semantic search stays inside one space
- duplicate detection stays inside one space
That means you can safely store `TypeScript` in both `A` and `B` without merging them.
## When spaces are useful
Spaces are a good fit when you need to isolate memory by:
- user
- customer or tenant
- project
- environment
- agent persona
- private vs shared knowledge
## Core API
You can manage spaces explicitly:
```ts
await db.upsertSpace({ name: 'A' });
await db.upsertSpace({ name: 'B' });
const spaces = await db.listSpaces();
const alpha = await db.getSpaceByName('A');
```
And then scope reads and writes with `spaceName`:
```ts
await db.addFact({
spaceName: 'A',
statement: 'TypeScript belongs to A.',
topics: [
{ name: 'TypeScript', category: 'entity', granularity: 'concrete' },
],
});
await db.addFact({
spaceName: 'B',
statement: 'TypeScript belongs to B.',
topics: [
{ name: 'TypeScript', category: 'entity', granularity: 'concrete' },
],
});
```
## Same topic name, different spaces
The same normalized topic name can exist independently in multiple spaces.
```ts
const alphaTopic = await db.getTopicByName('TypeScript', { spaceName: 'A' });
const betaTopic = await db.getTopicByName('TypeScript', { spaceName: 'B' });
```
These are separate topic rows with separate facts and relationships.
## Alias isolation
Aliases are also scoped by space.
```ts
await db.addTopicAlias('TypeScript', 'TS', { spaceName: 'A' });
await db.addTopicAlias('TeamSpeak', 'TS', { spaceName: 'B' });
const inA = await db.resolveTopic('TS', { spaceName: 'A' });
const inB = await db.resolveTopic('TS', { spaceName: 'B' });
```
The same alias can therefore mean different canonical topics in different spaces.
## Hierarchy isolation
Hierarchy is isolated too.
```ts
await db.linkTopics({
spaceName: 'A',
parentName: 'programming language',
childName: 'TypeScript',
});
await db.linkTopics({
spaceName: 'B',
parentName: 'language family',
childName: 'TypeScript',
});
const alphaParents = await db.getTopicParents('TypeScript', { spaceName: 'A' });
const betaParents = await db.getTopicParents('TypeScript', { spaceName: 'B' });
```
Those queries can return different results even though the child topic name is identical.
## Semantic search and duplicate detection isolation
Search and duplicate detection also stay inside the requested space.
```ts
await db.indexFactEmbeddings({ provider, spaceName: 'A' });
await db.indexFactEmbeddings({ provider, spaceName: 'B' });
const alphaMatches = await db.searchFacts({
spaceName: 'A',
query: 'TypeScript runtime tooling',
provider,
});
await db.ingestStatement('Bun makes TypeScript tooling fast.', {
spaceName: 'A',
extractor: new NaiveExtractor(),
embeddingProvider: provider,
duplicateThreshold: 0.95,
});
```
A fact in space `B` will not be used as a duplicate candidate for a write in space `A`.
## Default behavior
If you do not provide `spaceName`, IdentityDB uses the **default** space.
That preserves backwards compatibility with older code while still letting new code opt into explicit isolation.
## Practical recommendation
A simple pattern is:
- use one space per user if you want personal memory isolation
- use one space per tenant if you are building multi-tenant software
- use one space per project if the same agent needs independent project memories
- keep shared organizational knowledge in a separate dedicated shared space

@@ -2,5 +2,6 @@
- [Home](Home) - [Home](Home)
- [Getting Started](Getting-Started) - [Getting Started](Getting-Started)
- [Memory Spaces](Memory-Spaces)
- [Extractors](Extractors) - [Extractors](Extractors)
- [Repository](https://git.psw.kr/p-sw/IdentityDB) - [Repository](https://git.psw.kr/p-sw/IdentityDB)