Apply Q1–Q3 decisions: GraphDB/SPARQL, reuse Qdrant, self-host embeddings via TEI
- Q1 → Ontotext GraphDB (SPARQL). docs/08 fully rewritten with RDF schema, example SPARQL queries (transitive deps via property paths, time-windowed topic counts, cross-pillar joins via nexa:vectorId). - Q2 → reuse qdrant_scientific with nexa_* collection prefix; docs/09 step 2 now points there explicitly. - Q3 → no OpenAI embeddings. Self-host on the docker host. Use TEI (HuggingFace text-embeddings-inference) — single Rust binary, ~500 MB image, OpenAI-compatible — instead of Ollama, since we only need embeddings. - docs/09 Phase-3.4 add-on simplified to a single Ontotext compose snippet (Neo4j option dropped) plus repo creation curl. - docs/11 Q3 marked resolved; new Q15 picks the model (bge-m3 vs nomic-embed) and adds the open question of whether SAIA already proxies an embedding model that would let us skip TEI entirely. - docs/03 + CLAUDE.md updated with the new decisions so future runs don't re-litigate.
This commit is contained in:
+187
-218
@@ -1,277 +1,246 @@
|
||||
# 08 — GraphRAG: Structural Knowledge & Relations
|
||||
|
||||
> ⚠️ **Open decision (see [11-open-questions](./11-open-questions.md))**: this document describes the design with **Neo4j**. The earlier deployment draft referenced **Ontotext GraphDB** (SPARQL). Pick one before Phase 3.4.
|
||||
|
||||
## GraphRAG für Nexa — Strukturelles Wissen & Beziehungen
|
||||
|
||||
## 🧠 Zwei-Säulen-Memory-Architektur
|
||||
|
||||
Nexa nutzt zwei komplementäre Speicher für vollständiges Verständnis:
|
||||
|
||||
### Säule 1: Qdrant (Vector DB) - Semantische Ähnlichkeit
|
||||
- **Frage**: "Was ist ähnlich, was ist relevant?"
|
||||
- **Speichert**: Embeddings von Memos, Obsidian-Notes, E-Mail-Digests
|
||||
- **Suche**: Cosine Similarity über 1536-dimensionale Vektoren
|
||||
- **Antwort**: Top-k semantisch ähnliche Dokumente (RAG)
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
Frage: "Wie war die JWT-Implementation in unserem letzten Projekt?"
|
||||
→ Qdrant findet: 10 ähnliche Snippets → Prompt-Context
|
||||
```
|
||||
|
||||
### Säule 2: Neo4j (Graph DB) - Strukturelle Beziehungen
|
||||
- **Frage**: "Wer ist beteiligt? Was hängt davon ab? Wie ist es organisiert?"
|
||||
- **Speichert**:
|
||||
- **Knoten (Nodes)**: Projects, Tasks, People, Technologies, Dates
|
||||
- **Kanten (Relationships)**: `USES`, `OWNS`, `DEPENDS_ON`, `MENTIONS`, `CREATED_BY`
|
||||
- **Suche**: Cypher-Queries über Netzwerk-Topologie
|
||||
- **Antwort**: Pfade, zentrale Knoten, Abhängigkeitsanalyse
|
||||
|
||||
**Beispiel:**
|
||||
```cypher
|
||||
MATCH (tech:Technology)-[USES*]->*(service:Service),
|
||||
(tech)-[:MENTIONED_IN]->(task:Task)
|
||||
WHERE tech.name = "JWT" AND task.status = "DONE"
|
||||
RETURN tech, service, task
|
||||
```
|
||||
Decision: graph layer = **Ontotext GraphDB** with **SPARQL** (resolved in [11/Q1](./11-open-questions.md)).
|
||||
Rationale: SPARQL + RDF lets Nexa's memory be browsed and queried with the same standard tooling that's used for any open-data corpus, and it leaves the door open for SHACL / OWL reasoning later.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Neo4j Schema für Nexa
|
||||
## Two-pillar memory
|
||||
|
||||
### Node-Typen
|
||||
| Pillar | Question it answers | Backed by |
|
||||
|--------|---------------------|-----------|
|
||||
| **Qdrant** (vectors) | *"What is similar / relevant?"* | Cosine search over embeddings |
|
||||
| **GraphDB** (RDF) | *"What is connected? What depends on what? Who is involved?"* | SPARQL over a typed graph |
|
||||
|
||||
```cypher
|
||||
(Project {name, startDate, status, priority})
|
||||
(Task {title, description, status, urgency: 1-5, context: "work"|"personal"})
|
||||
(Person {name, role, email})
|
||||
(Technology {name, category: "framework"|"language"|"tool"})
|
||||
(Topic {name, domain})
|
||||
(File {path, type, lastModified})
|
||||
(Note {id, content_hash, created_at})
|
||||
```
|
||||
|
||||
### Relationship-Typen
|
||||
|
||||
| Beziehung | Von | Zu | Beschreibung |
|
||||
|-----------|-----|-----------|-------------|
|
||||
| `OWNS` | Person | Task/Project | Wer ist verantwortlich |
|
||||
| `USES` | Task/Project | Technology | Welche Tech wird verwendet |
|
||||
| `DEPENDS_ON` | Task/Project | Task/Project | Hängt davon ab |
|
||||
| `CREATED_BY` | Note | Person | Autor einer Notiz |
|
||||
| `MENTIONS` | Note/Task | Topic/Technology | Verweist auf |
|
||||
| `RELATED_TO` | Task | Task | Zusammenhängend |
|
||||
| `CHILD_OF` | Task | Project | Task gehört zu Project |
|
||||
| `SCHEDULED_FOR` | Task | Date | Zeitliche Zuordnung |
|
||||
Both pillars are queried in parallel for `#nexa:ask` and merged before SAIA generates the final answer.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Synchronisierungs-Flows
|
||||
## RDF schema
|
||||
|
||||
### 1. **Memos → Neo4j** (automatisch beim Erstellen)
|
||||
Compact, opinionated. One namespace, one ontology file, no v2/v3 inheritance pain.
|
||||
|
||||
Wenn ein Memo mit `- [ ]` erstellt wird:
|
||||
```turtle
|
||||
@prefix nexa: <https://nuclide.systems/nexa/ontology#> .
|
||||
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
||||
@prefix prov: <http://www.w3.org/ns/prov#> .
|
||||
|
||||
```
|
||||
Memo-Content: "Muss JWT-Middleware für Auth-Service refaktorieren"
|
||||
↓
|
||||
SAIA extrahiert Entitäten:
|
||||
- Technologie: "JWT-Middleware", "Auth-Service"
|
||||
- Kontext: "work"
|
||||
- Urgenz: 3
|
||||
↓
|
||||
Erstelle in Neo4j:
|
||||
- Task node (title="JWT-Middleware refaktorieren")
|
||||
- Technology nodes: JWT, Auth-Service
|
||||
- Relationships: Task -[USES]-> JWT, Task -[USES]-> Auth-Service
|
||||
- Task -[CREATED_BY]-> Me (Person node)
|
||||
# Classes
|
||||
nexa:Project a rdfs:Class .
|
||||
nexa:Task a rdfs:Class .
|
||||
nexa:Person a rdfs:Class .
|
||||
nexa:Technology a rdfs:Class .
|
||||
nexa:Topic a rdfs:Class .
|
||||
nexa:Note a rdfs:Class . # Memos / Obsidian / mail digests
|
||||
nexa:File a rdfs:Class .
|
||||
|
||||
# Properties
|
||||
nexa:owns a rdf:Property ; rdfs:domain nexa:Person ; rdfs:range nexa:Task .
|
||||
nexa:uses a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range nexa:Technology .
|
||||
nexa:dependsOn a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range nexa:Task .
|
||||
nexa:childOf a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range nexa:Project .
|
||||
nexa:mentions a rdf:Property ; rdfs:domain nexa:Note ; rdfs:range nexa:Topic .
|
||||
nexa:scheduledFor a rdf:Property ; rdfs:domain nexa:Task ; rdfs:range xsd:dateTime .
|
||||
|
||||
# Datatype properties
|
||||
nexa:status a rdf:Property ; rdfs:range xsd:string . # "needs-action" | "in-progress" | "done"
|
||||
nexa:context a rdf:Property ; rdfs:range xsd:string . # "work" | "personal"
|
||||
nexa:urgency a rdf:Property ; rdfs:range xsd:integer . # 1–5
|
||||
nexa:vectorId a rdf:Property ; rdfs:range xsd:string . # Qdrant point ID — bridges the two pillars
|
||||
nexa:contentHash a rdf:Property ; rdfs:range xsd:string . # for de-dup
|
||||
```
|
||||
|
||||
### 2. **Obsidian → Neo4j** (#nexa:sync-obsidian)
|
||||
|
||||
Beim Sync von Obsidian-Vault:
|
||||
|
||||
```
|
||||
File: obsidian/projects/nexa-deployment.md
|
||||
↓
|
||||
Header Struktur wird zu Graph:
|
||||
- Project "Nexa Deployment"
|
||||
- Technologies: Docker, n8n, Nextcloud
|
||||
- Dependencies: "requires infrastructure setup"
|
||||
↓
|
||||
Erstelle:
|
||||
- Project nodes mit Metadata
|
||||
- Hierarchie: Project -[CONTAINS]-> Task -[USES]-> Technology
|
||||
```
|
||||
|
||||
### 3. **Nextcloud Tasks → Neo4j** (bidirektional)
|
||||
|
||||
Wenn Task in Nextcloud erstellt/gelöcht:
|
||||
|
||||
```
|
||||
Nextcloud Task: "Review JWT implementation" (in Work-List)
|
||||
↓
|
||||
n8n Trigger erkennt Änderung
|
||||
↓
|
||||
Update Neo4j:
|
||||
- Task node status = "active"
|
||||
- Task -[DEPENDS_ON]-> related Task (falls Beziehung existiert)
|
||||
```
|
||||
The `nexa:vectorId` property is the **bridge** between graph and vector store. Every node that has semantic content carries the Qdrant point ID, so a SPARQL hit can trigger a vector lookup and vice versa.
|
||||
|
||||
---
|
||||
|
||||
## 🤖 GraphRAG Queries - Praktische Anwendungen
|
||||
## Sync flows
|
||||
|
||||
### Query 1: "Zeige alle offenen Tasks, die JWT betreffen"
|
||||
### 1. Memos → GraphDB (real-time)
|
||||
|
||||
```cypher
|
||||
MATCH (tech:Technology {name:"JWT"}),
|
||||
(tech)-[:MENTIONED_IN|USES]-(task:Task),
|
||||
(task)-[:CHILD_OF]->(project:Project)
|
||||
WHERE task.status IN ["NEEDS-ACTION", "IN-PROGRESS"]
|
||||
RETURN project.name, task.title, task.urgency
|
||||
ORDER BY task.urgency DESC
|
||||
```
|
||||
Memo content: "Muss JWT-Middleware für Auth-Service refaktorieren"
|
||||
│
|
||||
▼ SAIA extracts entities + relations as JSON
|
||||
│ { tasks: [{title, urgency}], technologies: [...],
|
||||
│ relations: [{type:"uses", from:..., to:...}] }
|
||||
│
|
||||
▼ n8n turns JSON into a SPARQL UPDATE
|
||||
│
|
||||
└──▶ INSERT DATA { ... } against GraphDB repo "nexa_knowledge"
|
||||
```
|
||||
|
||||
### Query 2: "Von welchen Tasks/Projects hängt Auth-Service ab?"
|
||||
### 2. Obsidian → GraphDB (`#nexa:sync-obsidian`)
|
||||
|
||||
```cypher
|
||||
MATCH path = (dependent)-[:DEPENDS_ON*]->(service:Technology {name:"Auth-Service"})
|
||||
RETURN [x IN nodes(path) | x.name], length(path)
|
||||
ORDER BY length(path) ASC
|
||||
```
|
||||
For each Obsidian note: parse front-matter + headings → emit `nexa:Project`, `nexa:Task`, `nexa:Note` triples; `nexa:mentions` for `[[wikilinks]]`.
|
||||
|
||||
### Query 3: "Wer arbeitet an diesem Projekt und auf welchen Technologien?"
|
||||
### 3. Nextcloud Tasks ↔ GraphDB (bidirectional)
|
||||
|
||||
```cypher
|
||||
MATCH (person:Person)-[:OWNS]->(task:Task)-[:CHILD_OF]->(project:Project {name:"Nexa"}),
|
||||
(task)-[:USES]->(tech:Technology)
|
||||
RETURN person.name, collect(task.title), collect(tech.name)
|
||||
```
|
||||
|
||||
### Query 4: "Was ist seit gestern neu an Technologien/Themen gelernt?"
|
||||
|
||||
```cypher
|
||||
MATCH (note:Note)-[:MENTIONS]->(topic:Topic)
|
||||
WHERE note.created_at > datetime.now() - duration('P1D')
|
||||
RETURN DISTINCT topic.name, count(note) as mentions
|
||||
ORDER BY mentions DESC
|
||||
```
|
||||
n8n trigger on Nextcloud CalDAV/Tasks change → `INSERT/DELETE DATA` to keep `nexa:status` and `nexa:scheduledFor` in sync.
|
||||
|
||||
---
|
||||
|
||||
## 🔗 GraphRAG-Enhanced Answer Generation
|
||||
## Example SPARQL queries
|
||||
|
||||
Wenn Nutzer `#nexa:ask` fragt:
|
||||
### Q1 — All open tasks involving JWT, by urgency
|
||||
|
||||
```sparql
|
||||
PREFIX nexa: <https://nuclide.systems/nexa/ontology#>
|
||||
|
||||
SELECT ?taskTitle ?urgency ?projectName
|
||||
WHERE {
|
||||
?tech rdfs:label "JWT" .
|
||||
?task nexa:uses ?tech ;
|
||||
rdfs:label ?taskTitle ;
|
||||
nexa:status ?status ;
|
||||
nexa:urgency ?urgency .
|
||||
FILTER (?status IN ("needs-action", "in-progress"))
|
||||
OPTIONAL { ?task nexa:childOf ?project . ?project rdfs:label ?projectName . }
|
||||
}
|
||||
ORDER BY DESC(?urgency)
|
||||
```
|
||||
#nexa:ask Wie stehen die JWT-Tasks zur Deadline?
|
||||
↓
|
||||
┌─────────────┴──────────────┐
|
||||
↓ ↓
|
||||
[Qdrant] [Neo4j]
|
||||
Semantic Structural
|
||||
Search Search
|
||||
↓ ↓
|
||||
Top-3 Notes (JWT)-[DEPENDS_ON]
|
||||
mit ähnlichem → Connected Tasks
|
||||
Kontext → Critical Path
|
||||
↓ ↓
|
||||
Combine: Merge + Rank
|
||||
↓
|
||||
System Prompt (SAIA):
|
||||
"Basierend auf diesem Wissen und diesen Abhängigkeiten, beantworte:"
|
||||
↓
|
||||
Response mit:
|
||||
- Semantisch relevanter Information (Qdrant)
|
||||
- Strukturellem Kontext (Neo4j)
|
||||
- Abhängigkeitsanalyse
|
||||
|
||||
### Q2 — What does Auth-Service transitively depend on?
|
||||
|
||||
```sparql
|
||||
PREFIX nexa: <https://nuclide.systems/nexa/ontology#>
|
||||
|
||||
SELECT DISTINCT ?dep ?label
|
||||
WHERE {
|
||||
?root rdfs:label "Auth-Service" .
|
||||
?root nexa:dependsOn+ ?dep .
|
||||
?dep rdfs:label ?label .
|
||||
}
|
||||
```
|
||||
|
||||
(`+` is SPARQL property-paths — transitive closure, free.)
|
||||
|
||||
### Q3 — Topics with the most note-mentions in the last day
|
||||
|
||||
```sparql
|
||||
PREFIX nexa: <https://nuclide.systems/nexa/ontology#>
|
||||
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
||||
|
||||
SELECT ?topic (COUNT(?note) AS ?n)
|
||||
WHERE {
|
||||
?note a nexa:Note ;
|
||||
prov:generatedAtTime ?ts ;
|
||||
nexa:mentions ?topic .
|
||||
FILTER (?ts > NOW() - "P1D"^^xsd:duration)
|
||||
}
|
||||
GROUP BY ?topic
|
||||
ORDER BY DESC(?n)
|
||||
LIMIT 10
|
||||
```
|
||||
|
||||
### Q4 — Cross-pillar: "find vectors for tasks blocking project X"
|
||||
|
||||
```sparql
|
||||
PREFIX nexa: <https://nuclide.systems/nexa/ontology#>
|
||||
|
||||
SELECT ?taskTitle ?vectorId
|
||||
WHERE {
|
||||
?proj rdfs:label "Nexa" .
|
||||
?task nexa:childOf ?proj ;
|
||||
nexa:status "in-progress" ;
|
||||
nexa:vectorId ?vectorId ;
|
||||
rdfs:label ?taskTitle .
|
||||
}
|
||||
```
|
||||
|
||||
n8n then takes each `?vectorId`, fetches the embedding from Qdrant, and runs a "more like this" search for richer context.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ n8n Integration (Phase 3)
|
||||
## GraphRAG answer pipeline (`#nexa:ask`)
|
||||
|
||||
```
|
||||
#nexa:ask <question>
|
||||
│
|
||||
┌──────┴──────┐
|
||||
▼ ▼
|
||||
[Qdrant] [GraphDB]
|
||||
semantic structural
|
||||
top-k SPARQL — auto-generated
|
||||
notes paths / dependencies
|
||||
│ │
|
||||
└──────┬──────┘
|
||||
▼
|
||||
merge + rank
|
||||
│
|
||||
▼
|
||||
SAIA prompt:
|
||||
"Given these passages and these relations, answer …"
|
||||
│
|
||||
▼
|
||||
comment under the original memo
|
||||
```
|
||||
|
||||
Auto-generation of SPARQL: SAIA is given the ontology (above) as a system prompt and asked to emit a `SELECT`/`CONSTRUCT` query for the user's natural-language question. n8n executes it, falls back to a templated query on parse failure.
|
||||
|
||||
---
|
||||
|
||||
## n8n integration sketch
|
||||
|
||||
### Workflow: Graph-Sync Trigger
|
||||
|
||||
```
|
||||
[Memos Webhook]
|
||||
↓
|
||||
[Memos Webhook]
|
||||
│
|
||||
[Parse Content]
|
||||
↓
|
||||
[SAIA: Extract Entities]
|
||||
├→ Tasks, Projects, Technologies, People
|
||||
└→ Relationships (uses, depends_on, mentions)
|
||||
↓
|
||||
[Neo4j: Create/Update Nodes & Relationships]
|
||||
↓
|
||||
[Index in Qdrant Metadata: Add graph_node_id]
|
||||
│
|
||||
[SAIA: Extract entities + relations as JSON]
|
||||
│
|
||||
[Build SPARQL UPDATE INSERT DATA { ... }]
|
||||
│
|
||||
[HTTP POST → /repositories/nexa_knowledge/statements]
|
||||
│
|
||||
[Index in Qdrant; write Qdrant point id back via second SPARQL UPDATE]
|
||||
```
|
||||
|
||||
### Workflow: Question Router
|
||||
|
||||
```
|
||||
[#nexa:ask Query]
|
||||
↓
|
||||
[Parallel Search]
|
||||
├→ [Qdrant: Vector Similarity]
|
||||
├→ [Neo4j: Graph Query (auto-generated Cypher)]
|
||||
└→ [Rank & Merge Results]
|
||||
↓
|
||||
[SAIA: Generate Answer with Context]
|
||||
│
|
||||
┌─┴────────────────┐
|
||||
▼ ▼
|
||||
[SAIA: NL → SPARQL] [Qdrant: kNN]
|
||||
│ │
|
||||
[POST → SPARQL endpoint]
|
||||
│ │
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
rank + merge → SAIA answer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 Commands für Graph-Management
|
||||
## Graph-management commands
|
||||
|
||||
### #nexa:graph-status
|
||||
Zeigt Statistiken der Graph-DB
|
||||
```
|
||||
#nexa:graph-status
|
||||
```
|
||||
### `#nexa:graph-status`
|
||||
|
||||
Response:
|
||||
```
|
||||
📊 Neo4j Graph Status:
|
||||
- Nodes: 342 (Tasks: 145, Projects: 12, Technologies: 52, Notes: 133)
|
||||
- Relationships: 1,247
|
||||
- Density: 0.34
|
||||
- Most Connected: "JWT-Middleware" (27 connections)
|
||||
```
|
||||
Returns triple count, class histogram, most-connected entity. Implemented as one SPARQL `SELECT (COUNT)`.
|
||||
|
||||
### #nexa:graph-trace [entity]
|
||||
Zeigt alle Verbindungen für eine Entität
|
||||
```
|
||||
#nexa:graph-trace JWT-Middleware
|
||||
```
|
||||
### `#nexa:graph-trace [entity]`
|
||||
|
||||
### #nexa:graph-rebuild
|
||||
Rekonstruiert Graph aus Obsidian + Memos (einmalig, danach inkrementell)
|
||||
```
|
||||
#nexa:graph-rebuild --force
|
||||
```
|
||||
Returns the 1-hop (and optionally 2-hop) neighbourhood — a `DESCRIBE <iri>` plus a templated outgoing/incoming query.
|
||||
|
||||
### `#nexa:graph-rebuild`
|
||||
|
||||
Clears the named graph and replays Obsidian + Memos. SPARQL: `CLEAR GRAPH <https://nuclide.systems/nexa/runtime>` followed by the import workflow.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Phase 3 Roadmap (Graph Integration)
|
||||
## Why two stores
|
||||
|
||||
- [ ] **3.1 Neo4j Setup**: Collection, Constraints, Indexes
|
||||
- [ ] **3.2 Graph-Sync Workflow**: Automatische Entity Extraction
|
||||
- [ ] **3.3 GraphRAG Pipeline**: Kombinierte Qdrant + Neo4j Queries
|
||||
- [ ] **3.4 Dependency Tracker**: Warnt vor zirkulären Dependencies
|
||||
- [ ] **3.5 Critical Path Analysis**: Berechnet längsten Weg im Graph
|
||||
| Scenario | Qdrant | GraphDB | Best |
|
||||
|----------|--------|---------|------|
|
||||
| "Which note was similar to this one?" | ✅ | ❌ | Qdrant |
|
||||
| "What blocks this task?" | ❌ | ✅ | GraphDB |
|
||||
| "Explain this project" | ✅ context | ✅ structure | both |
|
||||
| "All JWT-related open work" | ✅ semantic | ✅ crisp | both |
|
||||
|
||||
---
|
||||
|
||||
## 💡 Warum zwei DBs?
|
||||
|
||||
| Szenario | Qdrant | Neo4j | Kombi |
|
||||
|----------|--------|-------|-------|
|
||||
| "Welche Note war ähnlich?" | ✅ Best | ❌ | Qdrant |
|
||||
| "Welche Tasks blockieren diesen Task?" | ❌ | ✅ Best | Neo4j |
|
||||
| "Erkläre mir dieses Projekt umfassend" | ✅ Context | ✅ Structure | **Beide!** |
|
||||
| "Finde alle JWT-bezogenen offenen Work" | ✅ Semantic | ✅ Crisp | **Beide!** |
|
||||
|
||||
Kombiniert: **Vollständiges Verständnis** statt nur Suchindex oder nur Struktur.
|
||||
Combined: **complete understanding** rather than a search index *or* a structure index.
|
||||
|
||||
Reference in New Issue
Block a user