diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2031ddf..abdc198 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- OpenSearch adapter (`OpenSearchStore`): kNN vector search, full-text search, bulk indexing, document CRUD
- Embedding microservice (`embedding-service/`): sentence-transformers REST API with batch processing and Dockerfile
- `EmbeddingService` port and `EmbeddingClient` HTTP adapter for remote embedding generation
+- Orchestrated ingestion pipeline: Docling → chunking → embedding → OpenSearch indexing (idempotent)
+- Ingestion REST API: `POST /api/ingestion/{jobId}`, `DELETE /api/ingestion/{docId}`, `GET /api/ingestion/status`
+- Production docker-compose with OpenSearch and embedding service
+- E2E Karate test for full ingestion workflow (PDF → chunks in OpenSearch)
+- My Documents screen: search, filter (all/indexed/not indexed), sort (name/date), ingestion status badges
+- Ingest button in Studio: one-click ingestion from completed analysis with progress feedback
### Fixed
diff --git a/frontend/src/pages/StudioPage.vue b/frontend/src/pages/StudioPage.vue
index e96769b..5ef26f9 100644
--- a/frontend/src/pages/StudioPage.vue
+++ b/frontend/src/pages/StudioPage.vue
@@ -94,6 +94,23 @@
{{ analysisStore.running ? t('studio.analyzing') : t('studio.run') }}
+
@@ -459,6 +476,7 @@ import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount, reactive }
import { useRoute, useRouter } from 'vue-router'
import { useDocumentStore } from '../features/document/store'
import { useAnalysisStore } from '../features/analysis/store'
+import { useIngestionStore } from '../features/ingestion/store'
import { DocumentUpload, DocumentList } from '../features/document/index'
import { ResultTabs } from '../features/analysis/index'
import BboxOverlay from '../features/analysis/ui/BboxOverlay.vue'
@@ -472,6 +490,7 @@ const route = useRoute()
const router = useRouter()
const documentStore = useDocumentStore()
const analysisStore = useAnalysisStore()
+const ingestionStore = useIngestionStore()
const { t } = useI18n()
const chunkingEnabled = useFeatureFlag('chunking')
@@ -528,6 +547,14 @@ const pipelineOptions = reactive({
images_scale: 1.0,
})
+const canIngest = computed(() => {
+ return (
+ ingestionStore.available &&
+ analysisStore.currentAnalysis?.status === 'COMPLETED' &&
+ analysisStore.currentAnalysis?.chunksJson != null
+ )
+})
+
const hasAnalysisResults = computed(() => {
return (
analysisStore.currentAnalysis?.status === 'COMPLETED' && analysisStore.currentPages?.length > 0
@@ -564,6 +591,11 @@ async function runAnalysis() {
await analysisStore.run(documentStore.selectedId, { ...pipelineOptions })
}
+async function runIngestion() {
+ if (!analysisStore.currentAnalysis?.id) return
+ await ingestionStore.ingest(analysisStore.currentAnalysis.id)
+}
+
function addMore() {
documentStore.selectedId = null
}
@@ -598,6 +630,7 @@ watch(
onMounted(async () => {
await documentStore.load()
analysisStore.load()
+ ingestionStore.checkAvailability()
// Restore analysis from history via query param
const analysisId = route.query.analysisId
@@ -811,6 +844,21 @@ onBeforeUnmount(() => {
cursor: not-allowed;
}
+.topbar-btn.ingest {
+ background: var(--success);
+ border-color: var(--success);
+ color: white;
+}
+
+.topbar-btn.ingest:hover:not(:disabled) {
+ filter: brightness(1.1);
+}
+
+.topbar-btn.ingest:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
.topbar-btn .btn-icon {
width: 16px;
height: 16px;