# UKO Indexer — Real-time Index Synchronization The UKO Indexer orchestrates the analysis of resources into UKO triples and simultaneously indexes content into text, vector, and graph backends. It implements the index lifecycle (add/change/remove/maintenance) with graceful degradation when optional backends are unavailable. Based on `docs/specification.md` > ACMS > Real-time Index Synchronization and Custom Index Backends. ## Architecture ``` Resource ──► AnalyzerRegistry.get_for_resource() │ ▼ ContentReader.read_content() │ ▼ Analyzer.analyze(content, resource_uri) │ ▼ attach_provenance(triples, resource, timestamp) │ ├──► GraphIndexBackend.add_triple() (required) ├──► TextIndexBackend.index_document() (optional — graceful degradation) └──► VectorIndexBackend.index_embedding() (optional — graceful degradation) ``` ## UKOIndexer Main service class that produces UKO triples from resources and indexes them into all available backends. ### Constructor | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `analyzer_registry` | `AnalyzerRegistry` | Yes | Registry of domain analyzers | | `graph_backend` | `GraphIndexBackend` | Yes | Backend for UKO triple storage | | `text_backend` | `TextIndexBackend \| None` | No | Backend for full-text indexing | | `vector_backend` | `VectorIndexBackend \| None` | No | Backend for embedding indexing | | `content_reader` | `ContentReader \| None` | No | Reader for resource content (default: `LocationContentReader`) | | `lifecycle_hook` | `IndexLifecycleHook \| None` | No | Callback for lifecycle events (default: `DefaultLifecycleHook`) | | `max_triples` | `int` | No | Maximum triples per resource (default: `50_000`). Must be positive. | ### Methods | Method | Signature | Returns | Description | |--------|-----------|---------|-------------| | `index_resource` | `(resource: Resource, *, project: str)` | `IndexResult` | Full indexing pipeline | | `remove_resource` | `(resource_id: str, *, project: str)` | `None` | Remove from all indices | | `reindex_resource` | `(resource: Resource, *, project: str)` | `IndexResult` | Remove + re-index | ### Properties | Property | Type | Description | |----------|------|-------------| | `analyzer_registry` | `AnalyzerRegistry` | The analyzer registry | | `indexed_resource_count` | `int` | Number of currently indexed resources | | `has_text_backend` | `bool` | Whether text backend is available | | `has_vector_backend` | `bool` | Whether vector backend is available | ### Graceful Degradation If `text_backend` or `vector_backend` is `None`, the corresponding indexing step is silently skipped. The `graph_backend` is always required. Error counts from individual backend failures are recorded in `IndexResult.errors` without aborting the entire indexing operation. ## Index Backends (Write-side Protocols) These are **write-side** protocols for indexing. They are distinct from the existing read-side query protocols (`TextBackend`, `VectorBackend`, `GraphBackend`) in `backends.py`. ### TextIndexBackend | Method | Signature | Returns | |--------|-----------|---------| | `index_document` | `(project: str, doc_id: str, content: str, metadata: dict[str, str])` | `IndexedDocument` | | `search` | `(project: str, query: str, *, limit: int = 20)` | `list[SearchResult]` | | `remove_document` | `(project: str, doc_id: str)` | `None` | | `rebuild_index` | `(project: str)` | `None` | ### VectorIndexBackend | Method | Signature | Returns | |--------|-----------|---------| | `index_embedding` | `(project: str, doc_id: str, embedding: list[float], metadata: dict[str, str])` | `None` | | `search_similar` | `(project: str, query_embedding: list[float], limit: int = 20, min_relevance: float = 0.0)` | `list[SearchResult]` | | `remove_embedding` | `(project: str, doc_id: str)` | `None` | ### GraphIndexBackend | Method | Signature | Returns | |--------|-----------|---------| | `add_triple` | `(project: str, subject: str, predicate: str, obj: str)` | `None` | | `query` | `(project: str, sparql: str)` | `list[dict[str, str]]` | | `remove_triples` | `(project: str, subject: str \| None, predicate: str \| None, obj: str \| None)` | `None` | ## Result Types ### IndexedDocument | Field | Type | Description | |-------|------|-------------| | `project` | `str` | Namespaced project name | | `doc_id` | `str` | Document identifier | | `char_count` | `int` | Character count (non-negative, default 0) | ### SearchResult | Field | Type | Description | |-------|------|-------------| | `doc_id` | `str` | Document identifier (non-empty) | | `content` | `str` | Matched text snippet | | `score` | `float` | Relevance score in `[0.0, 1.0]` | | `metadata` | `dict[str, str]` | Backend-specific metadata | ## Provenance ### ProvenanceMetadata Tracks the origin and validity of indexed triples. | Field | Type | Description | |-------|------|-------------| | `source_resource` | `str` | Resource ULID that produced the triple | | `source_path` | `str` | File path of the source resource | | `source_range` | `str` | Line range (e.g. `"10-25"`), empty if whole file | | `valid_from` | `datetime` | When the triple was indexed (defaults to `now(UTC)`) | | `is_current` | `bool` | Whether the triple is still current (default: `True`) | ### ProvenancedTriple Wrapper combining a `UKOTriple` with its `ProvenanceMetadata`. | Field | Type | Description | |-------|------|-------------| | `triple` | `UKOTriple` | The underlying UKO triple | | `provenance` | `ProvenanceMetadata` | Origin and validity metadata | ### IndexResult Summary returned by `index_resource()` and `reindex_resource()`. | Field | Type | Description | |-------|------|-------------| | `resource_id` | `str` | ULID of the indexed resource | | `triple_count` | `int` | Number of triples produced | | `text_docs_indexed` | `int` | Text documents indexed (0 or 1) | | `embeddings_indexed` | `int` | Embeddings indexed (0 or 1) | | `analyzer_domain` | `str` | Domain of the analyzer used | | `errors` | `tuple[str, ...]` | Non-fatal error messages | ## Protocols ### ContentReader Protocol for reading resource content. Decouples the indexer from the filesystem. | Method | Signature | Returns | |--------|-----------|---------| | `read_content` | `(resource: Resource)` | `str` | **Default implementation**: `LocationContentReader` — reads from `resource.location` via the local filesystem. ### IndexLifecycleHook Callback protocol for index lifecycle events. | Method | Signature | Returns | |--------|-----------|---------| | `on_indexed` | `(result: IndexResult)` | `None` | | `on_removed` | `(resource_id: str, project: str)` | `None` | | `on_error` | `(resource_id: str, error: str)` | `None` | **Default implementation**: `DefaultLifecycleHook` — logs events via structlog. ## In-Memory Stubs For testing, three in-memory stub implementations are provided: - `InMemoryTextIndexBackend` — dict-based full-text storage with substring search - `InMemoryVectorIndexBackend` — dict-based embedding storage (no real similarity) - `InMemoryGraphIndexBackend` — list-based triple storage with pattern matching ## ResourceFileWatcher Monitors filesystem paths via `watchdog` and triggers re-indexing callbacks on file changes with configurable debouncing. ### Constructor | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `indexer` | `UKOIndexer` | Yes | Indexer for re-index on change | | `project` | `str` | Yes | Project namespace | | `debounce_seconds` | `float` | No | Debounce delay (default: `0.5`) | | `on_change` | `Callable[[str, str, FileChangeType], None] \| None` | No | Optional change callback | | `event_bus` | `EventBus \| None` | No | Optional event bus for `RESOURCE_MODIFIED` emission | ### FileChangeType `StrEnum` with values: `CREATED`, `MODIFIED`, `DELETED`, `MOVED`. ### Methods | Method | Signature | Returns | Description | |--------|-----------|---------|-------------| | `watch` | `(resource_id: str, path: Path)` | `None` | Start watching a file path for a resource | | `unwatch` | `(resource_id: str)` | `None` | Stop watching a resource | | `shutdown` | `()` | `None` | Stop all observers and clean up | ## Known Limitations - **Placeholder vector embedding**: The vector backend receives a constant `[1.0]` placeholder instead of a real embedding. Real embedding model integration is tracked as a follow-up to issue #578. ## Spec References - Real-time Index Synchronization: `specification.md` > ACMS > Real-time Index Synchronization - Custom Index Backends: `specification.md` > ACMS > Custom Index Backends - Domain Analyzers: `specification.md` > ACMS > Domain Analyzers - Config keys: `specification.md` > Configuration Service > Index Backend Keys