phase a audit
This commit is contained in:
79
PHASE_A_AUDIT.md
Normal file
79
PHASE_A_AUDIT.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Technische Projektdokumentation
|
||||||
|
## RAG-System – Phase A Abschluss
|
||||||
|
|
||||||
|
**Projekt:** KI-RAG System
|
||||||
|
**Architekturstand:** Phase A abgeschlossen
|
||||||
|
**Datum:** Februar 2026
|
||||||
|
**Status:** Verbindliche Referenzdokumentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Zielsetzung von Phase A
|
||||||
|
|
||||||
|
Phase A hatte das Ziel, das bestehende Retrieval-Augmented-Generation-System strukturell zu stabilisieren und produktionsreif zu machen.
|
||||||
|
|
||||||
|
Im Fokus standen:
|
||||||
|
|
||||||
|
- Speicherstabilität (Streaming statt RAM-Load)
|
||||||
|
- Deterministische Indexierung
|
||||||
|
- Strikte Trennung von Domain (PHP) und Runtime (Python)
|
||||||
|
- Zentrale Konfigurationssteuerung
|
||||||
|
- Drift-Sicherheit des Vector-Index
|
||||||
|
|
||||||
|
Phase A beinhaltete **keine funktionale Erweiterung**, sondern ausschließlich strukturelle und architektonische Stabilisierung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Architekturprinzipien
|
||||||
|
|
||||||
|
Das System folgt folgenden verbindlichen Grundprinzipien:
|
||||||
|
|
||||||
|
1. **NDJSON ist Single Source of Truth**
|
||||||
|
Alle Vektoren werden deterministisch aus `index.ndjson` erzeugt.
|
||||||
|
|
||||||
|
2. **Full Rebuild statt inkrementeller Mutation**
|
||||||
|
Der FAISS-Index wird bei Änderungen vollständig neu aufgebaut.
|
||||||
|
|
||||||
|
3. **Streaming statt Full-RAM-Load**
|
||||||
|
Keine vollständigen JSON-Arrays im Speicher.
|
||||||
|
|
||||||
|
4. **Runtime und Domain sind strikt getrennt**
|
||||||
|
PHP enthält Orchestrierung und Governance, Python enthält Vektor-Runtime.
|
||||||
|
|
||||||
|
5. **Atomare Dateioperationen**
|
||||||
|
Schreibvorgänge erfolgen über `.tmp` + `rename()`.
|
||||||
|
|
||||||
|
6. **Konfigurationszentrierung**
|
||||||
|
Alle Pfade und Script-Referenzen sind über `services.yaml` parametriert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Umsetzung Phase A
|
||||||
|
|
||||||
|
## A1 – Streaming-Architektur
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
RAM-basierte JSON-Verarbeitung hätte bei steigender Chunk-Zahl zu Speicherproblemen geführt.
|
||||||
|
|
||||||
|
### Umsetzung
|
||||||
|
|
||||||
|
- Einführung von NDJSON als persistentes Format
|
||||||
|
- Streaming-Verarbeitung in:
|
||||||
|
- `ChunkManager::streamAll()`
|
||||||
|
- `ChunkManager::countAllChunks()`
|
||||||
|
- `ChunkManager::compactByDocument()`
|
||||||
|
- `ChunkManager::rewriteAll()`
|
||||||
|
- Entfernung von `iterator_to_array` im IngestFlow
|
||||||
|
|
||||||
|
### Ergebnis
|
||||||
|
|
||||||
|
- Speicherverbrauch unabhängig von Chunk-Anzahl
|
||||||
|
- Stabil bis mindestens 120.000 Chunks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## A2 – Strukturtrennung Runtime / Domain
|
||||||
|
|
||||||
|
### Umsetzung
|
||||||
|
|
||||||
|
Python-Runtime wurde vollständig aus `src/` entfernt und ausgelagert nach:
|
||||||
0
PHASE_B_AUDIT.md
Normal file
0
PHASE_B_AUDIT.md
Normal file
@@ -1,368 +0,0 @@
|
|||||||
# TECHNICAL_ARCHITECTURE.md
|
|
||||||
mitho AI Agent – Enterprise Hybrid RAG System
|
|
||||||
Stand: Februar 2026
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 1. Zielsetzung
|
|
||||||
|
|
||||||
Diese Dokumentation beschreibt die vollständige technische Architektur des
|
|
||||||
mitho AI Agent RAG-Systems.
|
|
||||||
|
|
||||||
Ziele:
|
|
||||||
|
|
||||||
- Deterministisches Retrieval
|
|
||||||
- Governance-stabile Index-Struktur
|
|
||||||
- Versionierte Wissensbasis
|
|
||||||
- Reproduzierbare Builds
|
|
||||||
- Keine inkrementellen Vektor-Mutationen
|
|
||||||
- Job-basierte, asynchrone Ingest-Architektur
|
|
||||||
- Skalierbarkeit >200k Chunks
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 2. Architektur-Übersicht
|
|
||||||
|
|
||||||
Systemebenen:
|
|
||||||
|
|
||||||
1. Admin Layer (Symfony)
|
|
||||||
2. Ingest Layer
|
|
||||||
3. Storage Layer (NDJSON)
|
|
||||||
4. Vector Layer (FAISS)
|
|
||||||
5. Retrieval Layer (Hybrid)
|
|
||||||
6. Application Layer (LLM + SSE)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 3. Core Architekturprinzipien
|
|
||||||
|
|
||||||
## 3.1 Determinismus
|
|
||||||
|
|
||||||
Gleiche Eingabe + gleiche Konfiguration → identisches Ergebnis.
|
|
||||||
|
|
||||||
- index.ndjson ist deterministisch
|
|
||||||
- FAISS wird vollständig neu gebaut
|
|
||||||
- Retrieval ist stabil reproduzierbar
|
|
||||||
- Keine impliziten Indexveränderungen
|
|
||||||
|
|
||||||
## 3.2 Governance
|
|
||||||
|
|
||||||
- Genau eine aktive Version pro Dokument
|
|
||||||
- Keine Inline-Rebuilds im HTTP-Request
|
|
||||||
- Strukturänderungen erzwingen Global Reindex
|
|
||||||
- Locking verhindert Parallel-Ingest
|
|
||||||
|
|
||||||
## 3.3 Skalierbarkeit
|
|
||||||
|
|
||||||
- NDJSON statt JSON-Array
|
|
||||||
- Streaming-Append
|
|
||||||
- Streaming-Compaction
|
|
||||||
- Kein RAM-Full-Load
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 4. Domain Model
|
|
||||||
|
|
||||||
## 4.1 Document
|
|
||||||
|
|
||||||
Primäre Wissenseinheit.
|
|
||||||
|
|
||||||
Eigenschaften:
|
|
||||||
- id (UUID)
|
|
||||||
- title
|
|
||||||
- createdAt
|
|
||||||
- createdBy
|
|
||||||
- archived (bool)
|
|
||||||
- currentVersion (ManyToOne → DocumentVersion)
|
|
||||||
|
|
||||||
## 4.2 DocumentVersion
|
|
||||||
|
|
||||||
Immutable Inhaltsversion.
|
|
||||||
|
|
||||||
Eigenschaften:
|
|
||||||
- id (UUID)
|
|
||||||
- document (ManyToOne)
|
|
||||||
- versionNumber
|
|
||||||
- filePath
|
|
||||||
- checksum
|
|
||||||
- active (bool)
|
|
||||||
- ingestStatus (PENDING, RUNNING, INDEXED, FAILED)
|
|
||||||
|
|
||||||
Regel:
|
|
||||||
Nur eine Version pro Dokument darf active = true sein.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 5. Ingest Architektur
|
|
||||||
|
|
||||||
## 5.1 Grundsatz
|
|
||||||
|
|
||||||
Kein Ingest läuft synchron im HTTP-Request.
|
|
||||||
|
|
||||||
Jede Indexänderung läuft über:
|
|
||||||
|
|
||||||
IngestJob → CLI Runner → IngestOrchestrator → IngestFlow
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5.2 IngestJob
|
|
||||||
|
|
||||||
Entity mit:
|
|
||||||
|
|
||||||
- id
|
|
||||||
- type
|
|
||||||
- status
|
|
||||||
- documentId
|
|
||||||
- documentVersionId
|
|
||||||
- startedAt
|
|
||||||
- finishedAt
|
|
||||||
- errorMessage
|
|
||||||
|
|
||||||
Job-Typen:
|
|
||||||
|
|
||||||
DOCUMENT_VERSION_ACTIVATE
|
|
||||||
DOCUMENT
|
|
||||||
GLOBAL_REINDEX
|
|
||||||
|
|
||||||
Status:
|
|
||||||
|
|
||||||
QUEUED
|
|
||||||
RUNNING
|
|
||||||
COMPLETED
|
|
||||||
FAILED
|
|
||||||
ABORTED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5.3 Job-Execution
|
|
||||||
|
|
||||||
Start:
|
|
||||||
|
|
||||||
php bin/console mto:agent:ingest:run <jobId>
|
|
||||||
|
|
||||||
Execution-Flow:
|
|
||||||
|
|
||||||
1. LockService.acquire()
|
|
||||||
2. Job → RUNNING
|
|
||||||
3. IngestFlow ausführen
|
|
||||||
4. Job → COMPLETED / FAILED
|
|
||||||
5. LockService.release()
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 6. IngestFlow Details
|
|
||||||
|
|
||||||
## 6.1 Local Ingest (Version aktivieren oder neue Datei)
|
|
||||||
|
|
||||||
Ablauf:
|
|
||||||
|
|
||||||
1. Extract (PDF/Text)
|
|
||||||
2. Normalize
|
|
||||||
3. Chunk deterministisch
|
|
||||||
4. Streaming Compaction:
|
|
||||||
- Entferne alle Chunks mit document_id
|
|
||||||
5. Append neue Chunks
|
|
||||||
6. Full FAISS Rebuild
|
|
||||||
|
|
||||||
Wichtig:
|
|
||||||
Es werden immer alle alten Chunks des Dokuments entfernt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6.2 Global Reindex
|
|
||||||
|
|
||||||
Wird ausgelöst bei:
|
|
||||||
|
|
||||||
- embedding_model Änderung
|
|
||||||
- embedding_dimension Änderung
|
|
||||||
- chunk_size Änderung
|
|
||||||
- scoring_version Änderung
|
|
||||||
- index_format Änderung
|
|
||||||
|
|
||||||
Ablauf:
|
|
||||||
|
|
||||||
1. Alle aktiven Versionen extrahieren
|
|
||||||
2. Komplettes index.ndjson neu schreiben
|
|
||||||
3. FAISS neu bauen
|
|
||||||
4. index_version++
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 7. Storage Layer
|
|
||||||
|
|
||||||
## 7.1 index.ndjson
|
|
||||||
|
|
||||||
Single Source of Truth.
|
|
||||||
|
|
||||||
Eigenschaften:
|
|
||||||
|
|
||||||
- 1 JSON-Objekt pro Zeile
|
|
||||||
- Streaming-lesbar
|
|
||||||
- Append-only
|
|
||||||
- Compaction by document_id
|
|
||||||
|
|
||||||
Beispiel:
|
|
||||||
|
|
||||||
{
|
|
||||||
"chunk_id": "...",
|
|
||||||
"document_id": "...",
|
|
||||||
"document_version_id": "...",
|
|
||||||
"text": "...",
|
|
||||||
"meta": {...}
|
|
||||||
}
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7.2 index_meta.json
|
|
||||||
|
|
||||||
Enthält:
|
|
||||||
|
|
||||||
- index_version
|
|
||||||
- embedding_model
|
|
||||||
- embedding_dimension
|
|
||||||
- chunk_size
|
|
||||||
- chunk_overlap
|
|
||||||
- scoring_version
|
|
||||||
- index_format
|
|
||||||
- vector_backend
|
|
||||||
|
|
||||||
Bei strukturellem Mismatch:
|
|
||||||
→ Global Reindex zwingend.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 8. Vector Layer (FAISS)
|
|
||||||
|
|
||||||
## 8.1 Build
|
|
||||||
|
|
||||||
vector_ingest.py:
|
|
||||||
|
|
||||||
- stream index.ndjson
|
|
||||||
- Embeddings berechnen
|
|
||||||
- FAISS IndexFlatIP bauen
|
|
||||||
- vector.index schreiben
|
|
||||||
- vector_meta.json schreiben
|
|
||||||
|
|
||||||
Immer vollständiger Rebuild.
|
|
||||||
Keine Partial Updates.
|
|
||||||
|
|
||||||
## 8.2 Search
|
|
||||||
|
|
||||||
vector_search.py:
|
|
||||||
|
|
||||||
Input:
|
|
||||||
- Query
|
|
||||||
- Top-K
|
|
||||||
|
|
||||||
Output:
|
|
||||||
- chunk_id
|
|
||||||
- score
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 9. Hybrid Retrieval
|
|
||||||
|
|
||||||
Ablauf:
|
|
||||||
|
|
||||||
1. Keyword Retrieval (NDJSON Keyword Search)
|
|
||||||
2. Vector Retrieval (FAISS)
|
|
||||||
3. Score Fusion
|
|
||||||
4. Chunk Lookup
|
|
||||||
5. Context Builder
|
|
||||||
6. Prompt Builder
|
|
||||||
7. LLM
|
|
||||||
|
|
||||||
Keyword bleibt Primärsignal.
|
|
||||||
Vector ergänzt semantische Nähe.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 10. Locking
|
|
||||||
|
|
||||||
LockService schützt:
|
|
||||||
|
|
||||||
- NDJSON Mutationen
|
|
||||||
- FAISS Rebuild
|
|
||||||
- Global Reindex
|
|
||||||
- Aktivierungs-Parallelität
|
|
||||||
|
|
||||||
Keine zwei Ingest-Jobs gleichzeitig erlaubt.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 11. Admin Flows
|
|
||||||
|
|
||||||
## 11.1 Neue Datei hochladen
|
|
||||||
|
|
||||||
1. Document + Version 1 erstellen
|
|
||||||
2. Version aktiv
|
|
||||||
3. DOCUMENT_VERSION_ACTIVATE Job anlegen
|
|
||||||
4. Async Runner starten
|
|
||||||
5. Redirect auf Job-Seite
|
|
||||||
|
|
||||||
## 11.2 Version aktivieren
|
|
||||||
|
|
||||||
1. DB-Status ändern
|
|
||||||
2. IngestStatus → PENDING
|
|
||||||
3. DOCUMENT_VERSION_ACTIVATE Job anlegen
|
|
||||||
4. Async Runner starten
|
|
||||||
|
|
||||||
## 11.3 Manuelles Ingest
|
|
||||||
|
|
||||||
1. DOCUMENT Job anlegen
|
|
||||||
2. Async Runner starten
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 12. Error Handling
|
|
||||||
|
|
||||||
Typische Fehler:
|
|
||||||
|
|
||||||
- exec deaktiviert → Async Start nicht möglich
|
|
||||||
- Lock aktiv → paralleler Ingest blockiert
|
|
||||||
- index_meta mismatch → Reindex erforderlich
|
|
||||||
- Vector-Dateien fehlen → Rebuild ausführen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 13. Security & Stability
|
|
||||||
|
|
||||||
- Keine User-Uploads direkt ins Retrieval
|
|
||||||
- Kein Self-Learning
|
|
||||||
- Kein unkontrollierter Index-Mutate
|
|
||||||
- Strict Separation:
|
|
||||||
- Admin Layer
|
|
||||||
- Ingest Layer
|
|
||||||
- Retrieval Layer
|
|
||||||
- LLM Layer
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 14. System Guarantees
|
|
||||||
|
|
||||||
Dieses System garantiert:
|
|
||||||
|
|
||||||
- Reproduzierbarkeit
|
|
||||||
- Drift-Freiheit
|
|
||||||
- Auditierbarkeit
|
|
||||||
- Versionierte Wissensbasis
|
|
||||||
- Deterministische Retrieval-Ergebnisse
|
|
||||||
- Enterprise-taugliche Skalierbarkeit
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# 15. Zusammenfassung
|
|
||||||
|
|
||||||
Das mitho AI Agent System ist eine:
|
|
||||||
|
|
||||||
- deterministische Hybrid-RAG Engine
|
|
||||||
- NDJSON-basierte Wissensplattform
|
|
||||||
- Full-Rebuild-FAISS Architektur
|
|
||||||
- Job-basierte Ingest-Pipeline
|
|
||||||
- Versionierte, governance-stabile Wissensstruktur
|
|
||||||
|
|
||||||
Keine Inline-Rebuilds.
|
|
||||||
Keine inkrementellen Vektor-Updates.
|
|
||||||
Keine impliziten Strukturänderungen.
|
|
||||||
|
|
||||||
Alles läuft kontrolliert über das Job-System.
|
|
||||||
@@ -30,9 +30,14 @@ parameters:
|
|||||||
mto.knowledge.vector_tags_index: '%mto.knowledge.root%/vector_tags.index'
|
mto.knowledge.vector_tags_index: '%mto.knowledge.root%/vector_tags.index'
|
||||||
mto.knowledge.vector_tags_index_meta: '%mto.knowledge.root%/vector_tags.index.meta.json'
|
mto.knowledge.vector_tags_index_meta: '%mto.knowledge.root%/vector_tags.index.meta.json'
|
||||||
|
|
||||||
# Tag vector scripts (in src/Vector)
|
# ------------------------------------------------------------
|
||||||
mto.vector.ingest_tags_script: '%mto.root%/src/Vector/vector_ingest_tags.py'
|
# Vector Script Directory (A2)
|
||||||
mto.vector.search_tags_script: '%mto.root%/src/Vector/vector_search_tags.py'
|
# ------------------------------------------------------------
|
||||||
|
mto.vector.script_dir: '%mto.root%/python/vector'
|
||||||
|
|
||||||
|
# Tag vector scripts
|
||||||
|
mto.vector.ingest_tags_script: '%mto.vector.script_dir%/vector_ingest_tags.py'
|
||||||
|
mto.vector.search_tags_script: '%mto.vector.script_dir%/vector_search_tags.py'
|
||||||
|
|
||||||
# Lock for tag rebuild jobs
|
# Lock for tag rebuild jobs
|
||||||
mto.tags.rebuild_lock: '%mto.knowledge.root%/locks/tag_rebuild.lock'
|
mto.tags.rebuild_lock: '%mto.knowledge.root%/locks/tag_rebuild.lock'
|
||||||
@@ -53,10 +58,9 @@ parameters:
|
|||||||
# Python / Vector Runtime
|
# Python / Vector Runtime
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
mto.vector.python_bin: '/var/www/html/.venv/bin/python3'
|
mto.vector.python_bin: '/var/www/html/.venv/bin/python3'
|
||||||
mto.vector.ingest_script: '%mto.root%/src/Vector/vector_ingest.py'
|
mto.vector.ingest_script: '%mto.vector.script_dir%/vector_ingest.py'
|
||||||
mto.vector.search_script: '%mto.root%/src/Vector/vector_search.py'
|
mto.vector.search_script: '%mto.vector.script_dir%/vector_search.py'
|
||||||
mto.vector.timeout: 600
|
mto.vector.timeout: 600
|
||||||
|
|
||||||
mto.vector.service_url: 'http://127.0.0.1:8090'
|
mto.vector.service_url: 'http://127.0.0.1:8090'
|
||||||
|
|
||||||
|
|
||||||
@@ -177,7 +181,7 @@ services:
|
|||||||
$tagsNdjsonPath: '%mto.knowledge.tags_ndjson%'
|
$tagsNdjsonPath: '%mto.knowledge.tags_ndjson%'
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Tags Vector (Builder + Search) ✅ HIER IST DER FIX
|
# Tags Vector
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
App\Tag\TagVectorIndexBuilder:
|
App\Tag\TagVectorIndexBuilder:
|
||||||
@@ -221,6 +225,6 @@ services:
|
|||||||
|
|
||||||
App\Vector\VectorIndexHealthService:
|
App\Vector\VectorIndexHealthService:
|
||||||
arguments:
|
arguments:
|
||||||
$indexNdjsonPath: '%kernel.project_dir%/var/knowledge/index.ndjson'
|
$indexNdjsonPath: '%mto.knowledge.ndjson%'
|
||||||
$vectorIndexPath: '%kernel.project_dir%/var/knowledge/vector.index'
|
$vectorIndexPath: '%mto.knowledge.vector_index%'
|
||||||
$vectorMetaPath: '%kernel.project_dir%/var/knowledge/vector.index.meta.json'
|
$vectorMetaPath: '%mto.knowledge.vector_index_meta%'
|
||||||
@@ -181,7 +181,7 @@ def start_service(host: str, port: int) -> Dict:
|
|||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
str(UVICORN_BIN),
|
str(UVICORN_BIN),
|
||||||
"src.Vector.vector_service:app",
|
"python.vector.vector_service:app",
|
||||||
"--host", host,
|
"--host", host,
|
||||||
"--port", str(port),
|
"--port", str(port),
|
||||||
]
|
]
|
||||||
@@ -33,7 +33,7 @@ final class VectorControlCommand extends Command
|
|||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$cmd = ['.venv/bin/python', 'src/Vector/vector_control.py'];
|
$cmd = ['.venv/bin/python', 'python/vector/vector_control.py'];
|
||||||
|
|
||||||
if ($input->getOption('install')) {
|
if ($input->getOption('install')) {
|
||||||
$cmd[] = '--install';
|
$cmd[] = '--install';
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ final readonly class IngestFlow
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// DOCUMENT INGEST
|
// DOCUMENT INGEST (STREAMING SAFE)
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
public function ingestDocumentVersion(DocumentVersion $version): void
|
public function ingestDocumentVersion(DocumentVersion $version): void
|
||||||
@@ -45,12 +45,34 @@ final readonly class IngestFlow
|
|||||||
|
|
||||||
$existing = $this->chunkManager->countAllChunks();
|
$existing = $this->chunkManager->countAllChunks();
|
||||||
|
|
||||||
$records = iterator_to_array(
|
$incoming = 0;
|
||||||
$this->knowledgeIngestService->buildChunkRecords($version),
|
$generator = $this->knowledgeIngestService->buildChunkRecords($version);
|
||||||
false
|
|
||||||
);
|
$wrappedGenerator = (function () use ($generator, $existing, &$incoming) {
|
||||||
|
|
||||||
|
foreach ($generator as $record) {
|
||||||
|
|
||||||
|
$incoming++;
|
||||||
|
$total = $existing + $incoming;
|
||||||
|
|
||||||
|
if ($total >= self::CHUNK_LIMIT_WARN) {
|
||||||
|
// Nur einmal warnen
|
||||||
|
if ($incoming === 1 || $total === self::CHUNK_LIMIT_WARN) {
|
||||||
|
// Logging erfolgt außerhalb des Streams final
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($total > self::CHUNK_LIMIT_HARD) {
|
||||||
|
throw new \RuntimeException('Chunk limit exceeded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
yield $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
$this->chunkManager->appendChunks($wrappedGenerator);
|
||||||
|
|
||||||
$incoming = count($records);
|
|
||||||
$total = $existing + $incoming;
|
$total = $existing + $incoming;
|
||||||
|
|
||||||
if ($total >= self::CHUNK_LIMIT_WARN) {
|
if ($total >= self::CHUNK_LIMIT_WARN) {
|
||||||
@@ -61,12 +83,6 @@ final readonly class IngestFlow
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($total > self::CHUNK_LIMIT_HARD) {
|
|
||||||
throw new \RuntimeException('Chunk limit exceeded.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->chunkManager->appendChunks($records);
|
|
||||||
|
|
||||||
$this->rebuildIndex(false);
|
$this->rebuildIndex(false);
|
||||||
|
|
||||||
$version->setIngestStatus(DocumentVersion::INGEST_INDEXED);
|
$version->setIngestStatus(DocumentVersion::INGEST_INDEXED);
|
||||||
@@ -81,13 +97,11 @@ final readonly class IngestFlow
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// GLOBAL REINDEX
|
// GLOBAL REINDEX (STREAMING SAFE)
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
public function globalReindex(): void
|
public function globalReindex(): void
|
||||||
{
|
{
|
||||||
|
|
||||||
// 1️⃣ Prüfen ob aktive Dokumente existieren
|
|
||||||
$activeDocuments = $this->em
|
$activeDocuments = $this->em
|
||||||
->getRepository(Document::class)
|
->getRepository(Document::class)
|
||||||
->createQueryBuilder('d')
|
->createQueryBuilder('d')
|
||||||
@@ -102,22 +116,40 @@ final readonly class IngestFlow
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2️⃣ ChunkRecords erzeugen
|
$incoming = 0;
|
||||||
$records = iterator_to_array(
|
|
||||||
$this->knowledgeIngestService->buildAllActiveChunkRecords(),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($records)) {
|
$generator = $this->knowledgeIngestService->buildAllActiveChunkRecords();
|
||||||
|
|
||||||
|
$wrappedGenerator = (function () use ($generator, &$incoming) {
|
||||||
|
|
||||||
|
foreach ($generator as $record) {
|
||||||
|
$incoming++;
|
||||||
|
yield $record;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Prüfen ob überhaupt etwas kommt (ohne alles in RAM zu ziehen)
|
||||||
|
$peekIterator = $wrappedGenerator instanceof \Iterator
|
||||||
|
? $wrappedGenerator
|
||||||
|
: (function () use ($wrappedGenerator) {
|
||||||
|
foreach ($wrappedGenerator as $item) {
|
||||||
|
yield $item;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!$peekIterator->valid()) {
|
||||||
|
$peekIterator->rewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$peekIterator->valid()) {
|
||||||
throw new \RuntimeException(
|
throw new \RuntimeException(
|
||||||
'Global Reindex abgebrochen: Es wurden keine Chunks erzeugt. Bitte prüfen Sie die Dokumente.'
|
'Global Reindex abgebrochen: Es wurden keine Chunks erzeugt.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3️⃣ Rewrite NDJSON
|
$this->chunkManager->rewriteAll($peekIterator);
|
||||||
$this->chunkManager->rewriteAll($records);
|
|
||||||
|
|
||||||
// 4️⃣ Rebuild Vector Index
|
|
||||||
$this->rebuildIndex(true);
|
$this->rebuildIndex(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,18 +169,14 @@ final readonly class IngestFlow
|
|||||||
throw new \RuntimeException('Document not found.');
|
throw new \RuntimeException('Document not found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunks entfernen
|
|
||||||
$this->chunkManager->compactByDocument($documentId);
|
$this->chunkManager->compactByDocument($documentId);
|
||||||
|
|
||||||
// Dokument aus DB entfernen
|
|
||||||
$this->em->remove($document);
|
$this->em->remove($document);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
// 4️⃣ Reindex nur wenn sinnvoll
|
|
||||||
$this->rebuildIndex(false);
|
$this->rebuildIndex(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// CENTRAL REBUILD
|
// CENTRAL REBUILD
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|||||||
@@ -23,13 +23,9 @@ final class ChunkManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// COUNT (für Guardrails / Limits)
|
// COUNT (Streaming, robust)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
|
||||||
* Zählt Datensätze (NDJSON-Zeilen) im index.ndjson streaming-basiert.
|
|
||||||
* Leere / kaputte Zeilen werden ignoriert.
|
|
||||||
*/
|
|
||||||
public function countAllChunks(): int
|
public function countAllChunks(): int
|
||||||
{
|
{
|
||||||
if (!is_file($this->indexPath)) {
|
if (!is_file($this->indexPath)) {
|
||||||
@@ -42,6 +38,7 @@ final class ChunkManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
$count = 0;
|
$count = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (($line = fgets($handle)) !== false) {
|
while (($line = fgets($handle)) !== false) {
|
||||||
$line = trim($line);
|
$line = trim($line);
|
||||||
@@ -49,7 +46,6 @@ final class ChunkManager
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NDJSON besteht aus JSON-Objekten; wir zählen nur valide Arrays.
|
|
||||||
$data = json_decode($line, true);
|
$data = json_decode($line, true);
|
||||||
if (is_array($data)) {
|
if (is_array($data)) {
|
||||||
$count++;
|
$count++;
|
||||||
@@ -63,7 +59,7 @@ final class ChunkManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// APPEND
|
// APPEND (Streaming + Exception Safe)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,27 +78,34 @@ final class ChunkManager
|
|||||||
throw new \RuntimeException('Unable to open index.ndjson for append');
|
throw new \RuntimeException('Unable to open index.ndjson for append');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
foreach ($records as $record) {
|
foreach ($records as $record) {
|
||||||
$json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$json = json_encode(
|
||||||
|
$record,
|
||||||
|
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||||
|
);
|
||||||
|
|
||||||
if ($json === false) {
|
if ($json === false) {
|
||||||
fclose($handle);
|
|
||||||
throw new \RuntimeException('Unable to encode chunk record');
|
throw new \RuntimeException('Unable to encode chunk record');
|
||||||
}
|
}
|
||||||
|
|
||||||
fwrite($handle, $json . PHP_EOL);
|
if (fwrite($handle, $json . PHP_EOL) === false) {
|
||||||
|
throw new \RuntimeException('Unable to write chunk to index');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// COMPACTION – Entfernt alle Chunks eines Dokuments
|
// COMPACTION (Streaming + Safe Handles)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
public function compactByDocument(Uuid $documentId): void
|
public function compactByDocument(Uuid $documentId): void
|
||||||
{
|
{
|
||||||
if (!is_file($this->indexPath)) {
|
if (!is_file($this->indexPath)) {
|
||||||
return; // nichts zu kompaktieren
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmpPath = $this->indexPath . '.tmp';
|
$tmpPath = $this->indexPath . '.tmp';
|
||||||
@@ -116,6 +119,7 @@ final class ChunkManager
|
|||||||
|
|
||||||
$docIdString = $documentId->toRfc4122();
|
$docIdString = $documentId->toRfc4122();
|
||||||
|
|
||||||
|
try {
|
||||||
while (($line = fgets($in)) !== false) {
|
while (($line = fgets($in)) !== false) {
|
||||||
$line = trim($line);
|
$line = trim($line);
|
||||||
if ($line === '') {
|
if ($line === '') {
|
||||||
@@ -124,24 +128,27 @@ final class ChunkManager
|
|||||||
|
|
||||||
$data = json_decode($line, true);
|
$data = json_decode($line, true);
|
||||||
if (!is_array($data)) {
|
if (!is_array($data)) {
|
||||||
continue; // skip corrupted line
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (($data['document_id'] ?? null) === $docIdString) {
|
if (($data['document_id'] ?? null) === $docIdString) {
|
||||||
continue; // skip this document's chunks
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
fwrite($out, $line . PHP_EOL);
|
if (fwrite($out, $line . PHP_EOL) === false) {
|
||||||
|
throw new \RuntimeException('Unable to write compacted chunk');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
fclose($in);
|
fclose($in);
|
||||||
fclose($out);
|
fclose($out);
|
||||||
|
}
|
||||||
|
|
||||||
$this->atomicSwitch($tmpPath, $this->indexPath);
|
$this->atomicSwitch($tmpPath, $this->indexPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// FULL REWRITE (Global Reindex)
|
// FULL REWRITE (Streaming + Atomic)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,23 +169,30 @@ final class ChunkManager
|
|||||||
throw new \RuntimeException('Unable to open temp index file');
|
throw new \RuntimeException('Unable to open temp index file');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
foreach ($records as $record) {
|
foreach ($records as $record) {
|
||||||
$json = json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$json = json_encode(
|
||||||
|
$record,
|
||||||
|
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
|
||||||
|
);
|
||||||
|
|
||||||
if ($json === false) {
|
if ($json === false) {
|
||||||
fclose($handle);
|
|
||||||
throw new \RuntimeException('Unable to encode chunk record');
|
throw new \RuntimeException('Unable to encode chunk record');
|
||||||
}
|
}
|
||||||
|
|
||||||
fwrite($handle, $json . PHP_EOL);
|
if (fwrite($handle, $json . PHP_EOL) === false) {
|
||||||
|
throw new \RuntimeException('Unable to write chunk during rewrite');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
fclose($handle);
|
fclose($handle);
|
||||||
|
}
|
||||||
|
|
||||||
$this->atomicSwitch($tmpPath, $this->indexPath);
|
$this->atomicSwitch($tmpPath, $this->indexPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// STREAM READ (für FAISS rebuild)
|
// STREAM READ (FAISS rebuild safe)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,9 +49,8 @@ final class VectorIndexBuilder
|
|||||||
// --------------------------------------------
|
// --------------------------------------------
|
||||||
// 🔵 FALL: NDJSON ist leer → kein Vector Index
|
// 🔵 FALL: NDJSON ist leer → kein Vector Index
|
||||||
// --------------------------------------------
|
// --------------------------------------------
|
||||||
if (filesize($this->indexNdjsonPath) === 0) {
|
if (!is_file($this->indexNdjsonPath) || filesize($this->indexNdjsonPath) === 0) {
|
||||||
|
|
||||||
// Alten Index entfernen
|
|
||||||
@unlink($this->vectorIndexPath);
|
@unlink($this->vectorIndexPath);
|
||||||
@unlink($this->vectorMetaPath);
|
@unlink($this->vectorMetaPath);
|
||||||
|
|
||||||
@@ -63,7 +62,7 @@ final class VectorIndexBuilder
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return; // WICHTIG: kein Python, kein tmp, kein Fehler
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------
|
// --------------------------------------------
|
||||||
@@ -79,7 +78,6 @@ final class VectorIndexBuilder
|
|||||||
|
|
||||||
$tmpVectorIndexPath = $this->vectorIndexPath . '.tmp';
|
$tmpVectorIndexPath = $this->vectorIndexPath . '.tmp';
|
||||||
|
|
||||||
// Clean leftovers
|
|
||||||
@unlink($tmpVectorIndexPath);
|
@unlink($tmpVectorIndexPath);
|
||||||
@unlink($this->vectorMetaPath);
|
@unlink($this->vectorMetaPath);
|
||||||
|
|
||||||
@@ -108,11 +106,15 @@ final class VectorIndexBuilder
|
|||||||
private function assertPreconditions(): void
|
private function assertPreconditions(): void
|
||||||
{
|
{
|
||||||
if (!is_file($this->scriptPath)) {
|
if (!is_file($this->scriptPath)) {
|
||||||
throw new \RuntimeException('vector_ingest.py not found at: ' . $this->scriptPath);
|
throw new \RuntimeException(
|
||||||
|
'Vector build script not found at: ' . $this->scriptPath
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_file($this->indexNdjsonPath)) {
|
if (!is_file($this->indexNdjsonPath)) {
|
||||||
throw new \RuntimeException('index.ndjson not found at: ' . $this->indexNdjsonPath);
|
throw new \RuntimeException(
|
||||||
|
'index.ndjson not found at: ' . $this->indexNdjsonPath
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user