docs: add wiki guide for isolated memory spaces
@@ -25,6 +25,7 @@ await db.initialize();
|
||||
|
||||
This creates the tables IdentityDB needs:
|
||||
|
||||
- `spaces`
|
||||
- `topics`
|
||||
- `facts`
|
||||
- `fact_topics`
|
||||
@@ -32,7 +33,23 @@ This creates the tables IdentityDB needs:
|
||||
- `topic_aliases`
|
||||
- `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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -74,7 +91,7 @@ This is useful for reasoning such as:
|
||||
- `Bun` is a kind of `runtime`
|
||||
- `PostgreSQL` is a kind of `database`
|
||||
|
||||
## 5. Add aliases for canonical topic resolution
|
||||
## 6. Add aliases for canonical topic resolution
|
||||
|
||||
```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.
|
||||
|
||||
## 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()`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 7. Add semantic search
|
||||
## 8. Add semantic search
|
||||
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 9. Close the connection
|
||||
## 10. Close the connection
|
||||
|
||||
```ts
|
||||
await db.close();
|
||||
|
||||
3
Home.md
3
Home.md
@@ -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:
|
||||
|
||||
- **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`
|
||||
- **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
|
||||
@@ -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**
|
||||
- Initialize the required schema automatically
|
||||
- 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
|
||||
- Resolve aliases to canonical topics
|
||||
- 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
|
||||
|
||||
- [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`
|
||||
|
||||
## Repository
|
||||
|
||||
143
Memory-Spaces.md
Normal file
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)
|
||||
- [Getting Started](Getting-Started)
|
||||
- [Memory Spaces](Memory-Spaces)
|
||||
- [Extractors](Extractors)
|
||||
- [Repository](https://git.psw.kr/p-sw/IdentityDB)
|
||||
|
||||
Reference in New Issue
Block a user