Data Model: Unstructured Vector Ingestion and Search¶
Feature: 008-unstructured-vector-ingestion-search Date: 2025-11-23 Purpose: Define entities, relationships, and validation rules
Entity Overview¶
┌─────────────────────┐
│ VectorStoreConfig │ (YAML configuration)
│ - type: vectorstore│
│ - source │
│ - embedding_model │
│ - database │
│ - top_k │
│ - min_similarity │
└──────────┬──────────┘
│ configures
▼
┌─────────────────────┐ ingests ┌──────────────────┐
│ VectorStoreTool │◄──────────────────│ SourceFile │
│ - config │ │ - path │
│ - vector_store │ │ - mtime │
│ - embedding_svc │ │ - content │
└──────────┬──────────┘ └──────────────────┘
│ stores/retrieves
▼
┌─────────────────────┐ contains ┌──────────────────┐
│ VectorStore │◄──────────────────│ DocumentRecord │
│ - backend (Redis/ │ │ - id │
│ in-memory) │ │ - content │
│ - collection_name │ │ - source_path │
└─────────────────────┘ │ - chunk_index │
│ - mtime │
│ - embedding │
└──────────────────┘
│ executes
▼
┌─────────────────────┐ returns ┌──────────────────┐
│ SearchQuery │◄──────────────────│ QueryResult │
│ - query_text │ │ - content │
│ - top_k │ │ - score │
│ - min_score │ │ - source_path │
└─────────────────────┘ │ - chunk_index │
└──────────────────┘
Core Entities¶
1. VectorStoreConfig (Configuration Model)¶
Purpose: YAML-based configuration for vectorstore tools
Source: Defined in agent.yaml, parsed by ConfigLoader
Fields:
@dataclass
class VectorStoreConfig:
"""Configuration for vectorstore tool.
Defined in agent.yaml under tools section:
tools:
- type: vectorstore
name: knowledge_base
source: data/docs/ # or data/faq.md
embedding_model: text-embedding-3-small # optional
database: # optional
provider: redis
connection_string: redis://localhost:6379
index_name: holodeck_vectors
top_k: 5 # optional, default 5
min_similarity_score: 0.7 # optional, default None
"""
type: Literal["vectorstore"]
name: str
source: str # File path or directory path
embedding_model: str | None = None # Optional: defaults to provider default
database: DatabaseConfig | None = None # Optional: defaults to in-memory
top_k: int = 5 # Number of results to return
min_similarity_score: float | None = None # Minimum relevance threshold
@dataclass
class DatabaseConfig:
"""Vector database connection configuration."""
provider: Literal["redis"] # Future: "postgres", "azure_search"
connection_string: str # Redis: "redis://host:port" or "rediss://..." for SSL
index_name: str = "holodeck_vectors" # Collection/index name
vector_algorithm: Literal["HNSW", "FLAT"] = "HNSW"
distance_metric: Literal["COSINE", "L2", "IP"] = "COSINE"
Validation Rules:
- type MUST be "vectorstore"
- source MUST be non-empty string pointing to valid file or directory path
- embedding_model if specified MUST be supported by configured LLM provider
- top_k MUST be positive integer (default: 5)
- min_similarity_score if specified MUST be float between 0.0 and 1.0
- database.provider currently only supports "redis" (in-memory fallback automatic)
- database.connection_string MUST be valid Redis URL format if provider is "redis"
State Transitions: N/A (immutable configuration)
Related Requirements: FR-001, FR-002, FR-006, FR-008, FR-010
2. VectorStoreTool (Runtime Tool Instance)¶
Purpose: Executable tool instance that performs semantic search operations
Lifecycle: 1. Initialization: Created from VectorStoreConfig during agent startup 2. Ingestion: Loads source files, chunks content, generates embeddings, stores in vector database 3. Query: Accepts search queries, generates query embedding, retrieves top-k results 4. Cleanup: Releases resources on agent shutdown
Fields:
class VectorStoreTool:
"""Vectorstore tool for semantic search over unstructured data."""
config: VectorStoreConfig # Configuration from YAML
vector_store: VectorStore # Backend storage (Redis or in-memory)
embedding_service: TextEmbedding # Embedding generation service
file_processor: FileProcessor # Existing file-to-markdown converter
text_chunker: RecursiveCharacterTextSplitter # Text chunking
# State
is_initialized: bool = False
last_ingest_time: datetime | None = None
document_count: int = 0
Methods:
async def initialize(self, force_ingest: bool = False) -> None:
"""Initialize tool and ingest source files if needed."""
async def search(self, query: str) -> list[QueryResult]:
"""Execute semantic search and return results."""
async def _ingest_source(self, force: bool) -> None:
"""Ingest files from configured source."""
async def _process_file(self, file_path: Path) -> list[DocumentRecord]:
"""Process single file: convert to markdown, chunk, embed."""
async def _needs_reingest(self, file_path: Path) -> bool:
"""Check if file needs re-ingestion based on mtime."""
Validation Rules:
- Tool MUST call initialize() before first search() call
- search() query MUST be non-empty string
- Source files MUST exist at initialization time (FileNotFoundError if missing)
State Transitions:
[Created] --initialize()--> [Initialized] --search()--> [Ready]
↑ │
└────────────┘
(continuous queries)
Related Requirements: FR-001, FR-003, FR-004, FR-005, FR-006, FR-012
3. DocumentRecord (Vector Store Record)¶
Purpose: Represents a single document chunk with embedding stored in vector database
Storage: Persisted in Redis (production) or in-memory (development)
Fields:
from dataclasses import dataclass
from typing import Annotated
from semantic_kernel.data import (
VectorStoreRecordKeyField,
VectorStoreRecordDataField,
VectorStoreRecordVectorField
)
@dataclass
class DocumentRecord:
"""Vector store record for document chunks.
Stored in Redis or in-memory vector store. Each document file is split
into multiple chunks, each with its own embedding.
"""
# Primary key: {source_path}_chunk_{index}
id: Annotated[str, VectorStoreRecordKeyField()] = field(default="")
# Chunk content (markdown text)
content: Annotated[str, VectorStoreRecordDataField()] = field(default="")
# Original source file path
source_path: Annotated[str, VectorStoreRecordDataField()] = field(default="")
# Chunk index within document (0-indexed)
chunk_index: Annotated[int, VectorStoreRecordDataField()] = field(default=0)
# File modification time (for change detection)
mtime: Annotated[float, VectorStoreRecordDataField()] = field(default=0.0)
# Embedding vector (dimensions vary by model)
embedding: Annotated[list[float], VectorStoreRecordVectorField(
dimensions=1536, # text-embedding-3-small default
distance_function="cosine"
)] = field(default_factory=list)
# Optional metadata
file_type: Annotated[str, VectorStoreRecordDataField()] = field(default="")
file_size_bytes: Annotated[int, VectorStoreRecordDataField()] = field(default=0)
Validation Rules:
- id MUST be unique across all records in collection
- id MUST follow format: {source_path}_chunk_{chunk_index}
- content MUST be non-empty string (empty chunks skipped during ingestion)
- source_path MUST be valid file path string
- chunk_index MUST be non-negative integer
- mtime MUST be valid Unix timestamp (float)
- embedding length MUST match configured embedding model dimensions
Relationships: - Multiple DocumentRecords → One SourceFile (one-to-many: file split into chunks) - DocumentRecord → VectorStore collection (stored in)
Related Requirements: FR-004, FR-005, FR-007, FR-021
4. SourceFile (Input Entity)¶
Purpose: Represents a source file to be ingested (file or directory entry)
Lifecycle: Temporary entity during ingestion, not persisted
Fields:
@dataclass
class SourceFile:
"""Source file to be ingested into vector store."""
path: Path # Absolute file path
content: str = "" # Markdown content (populated via FileProcessor)
mtime: float = 0.0 # File modification time
size_bytes: int = 0 # File size
file_type: str = "" # File extension (.txt, .md, .pdf, etc.)
chunks: list[str] = field(default_factory=list) # Text chunks (populated by chunker)
Validation Rules:
- path MUST exist and be readable (FR-020)
- path MUST have supported file extension: .txt, .md, .pdf, .csv, .json (FR-003)
- size_bytes SHOULD be ≤ 100MB (warning logged if exceeded, FR-016)
- content MUST be non-empty after processing (empty files skipped with warning, FR-018)
State Transitions:
[Discovered] --FileProcessor--> [Converted to Markdown]
▼
[Chunked] --EmbeddingService--> [Embedded] --VectorStore--> [Stored]
Related Requirements: FR-003, FR-004, FR-014, FR-015, FR-016, FR-018, FR-019, FR-020
5. QueryResult (Output Entity)¶
Purpose: Represents a single search result returned from vector store query
Lifecycle: Created during search operation, returned to agent LLM
Fields:
@dataclass
class QueryResult:
"""Search result from vector store query."""
content: str # Matched document chunk content
score: float # Relevance/similarity score (0.0-1.0, higher is better)
source_path: str # Original source file path
chunk_index: int # Chunk index within source file
metadata: dict[str, Any] = field(default_factory=dict) # Additional metadata
Validation Rules:
- score MUST be float between 0.0 and 1.0
- score MUST be ≥ min_similarity_score if configured (FR-006, FR-013)
- Results MUST be ordered by descending score (FR-013)
- Result count MUST be ≤ top_k (FR-006, FR-013)
Relationships: - QueryResult ← DocumentRecord (derived from) - Multiple QueryResults → One SearchQuery (returned from)
Related Requirements: FR-006, FR-007, FR-013
6. SearchQuery (Input Entity)¶
Purpose: Represents a semantic search query from the agent
Lifecycle: Created when agent calls vectorstore tool, discarded after results returned
Fields:
@dataclass
class SearchQuery:
"""Semantic search query."""
query_text: str # Natural language query
top_k: int = 5 # Number of results to return
min_similarity_score: float | None = None # Minimum score threshold
# Internal (populated during execution)
query_embedding: list[float] = field(default_factory=list)
Validation Rules:
- query_text MUST be non-empty string
- top_k MUST be positive integer (default from config or 5)
- min_similarity_score if specified MUST be float between 0.0 and 1.0
Related Requirements: FR-006, FR-013
7. VectorStore (Storage Abstraction)¶
Purpose: Abstraction layer over Redis and in-memory vector storage backends
Implementation: Wrapper around Semantic Kernel's VectorStore connectors
Fields:
class VectorStore:
"""Vector store abstraction (Redis or in-memory)."""
backend: RedisStore | VolatileVectorStore # Semantic Kernel connector
collection: VectorStoreRecordCollection[str, DocumentRecord]
collection_name: str
Methods:
async def upsert(self, record: DocumentRecord) -> None:
"""Insert or update document record."""
async def get(self, record_id: str) -> DocumentRecord | None:
"""Retrieve record by ID."""
async def delete(self, record_id: str) -> None:
"""Delete record by ID."""
async def search(self, query_embedding: list[float], top_k: int, min_score: float | None) -> list[QueryResult]:
"""Perform similarity search."""
async def delete_by_source(self, source_path: str) -> None:
"""Delete all records for a given source file."""
Backends: - Redis (production): Persistent, multi-process, scalable (10k+ documents) - In-memory (development): Ephemeral, single-process, fast for small datasets (<1000 documents)
Related Requirements: FR-010, FR-011, FR-017
Entity Relationships¶
Cardinality¶
VectorStoreConfig (1) ─── creates ───> (1) VectorStoreTool
VectorStoreTool (1) ─── uses ───> (1) VectorStore
VectorStoreTool (1) ─── processes ───> (0..*) SourceFile
SourceFile (1) ─── generates ───> (1..*) DocumentRecord
VectorStore (1) ─── contains ───> (0..*) DocumentRecord
SearchQuery (1) ─── returns ───> (0..top_k) QueryResult
QueryResult (1) ─── derived_from ───> (1) DocumentRecord
Key Constraints¶
- Source Path Uniqueness: Multiple DocumentRecords can share the same
source_path(one per chunk) - ID Uniqueness: Each DocumentRecord MUST have unique
idacross entire collection - Embedding Dimensions: All DocumentRecords in a collection MUST have same embedding dimensions
- Search Ordering: QueryResults MUST be ordered by descending score (FR-013)
- Modification Tracking: DocumentRecord
mtimeMUST match source filemtimeor trigger re-ingestion
Data Flow Diagrams¶
Ingestion Flow¶
┌─────────────┐
│ agent.yaml │
│ source: │
│ data/docs/│
└──────┬──────┘
│ parse config
▼
┌─────────────────┐
│ VectorStoreTool │
│ .initialize() │
└────────┬────────┘
│ discover files
▼
┌──────────────────────┐
│ Directory Scanner │
│ (*.txt, *.md, *.pdf) │
└─────────┬────────────┘
│ for each file
▼
┌──────────────────────┐
│ FileProcessor │
│ (convert to markdown)│
└─────────┬────────────┘
│ markdown content
▼
┌──────────────────────┐
│ TextChunker │
│ (512 tokens, overlap)│
└─────────┬────────────┘
│ chunks
▼
┌──────────────────────┐
│ EmbeddingService │
│ (batch embed chunks) │
└─────────┬────────────┘
│ embeddings
▼
┌──────────────────────┐
│ VectorStore │
│ .upsert(DocumentRec) │
└──────────────────────┘
Query Flow¶
┌─────────────┐
│ Agent LLM │
│ calls tool: │
│ search(q) │
└──────┬──────┘
│ query text
▼
┌─────────────────┐
│ VectorStoreTool │
│ .search(query) │
└────────┬────────┘
│ embed query
▼
┌──────────────────────┐
│ EmbeddingService │
│ (generate embedding) │
└─────────┬────────────┘
│ query_embedding
▼
┌──────────────────────┐
│ VectorStore │
│ .search(embedding, │
│ top_k=5, │
│ min_score=0.7) │
└─────────┬────────────┘
│ ranked results
▼
┌──────────────────────┐
│ QueryResult[] │
│ (content + score + │
│ source_path) │
└─────────┬────────────┘
│ return to LLM
▼
┌─────────────┐
│ Agent LLM │
│ uses context│
└─────────────┘
Validation Matrix¶
| Entity | Field | Validation Rule | Error Type | Related FR |
|---|---|---|---|---|
| VectorStoreConfig | type | Must be "vectorstore" | ConfigError | FR-001 |
| VectorStoreConfig | source | Non-empty, valid path | ConfigError | FR-002 |
| VectorStoreConfig | top_k | Positive integer | ConfigError | FR-006 |
| VectorStoreConfig | min_similarity_score | 0.0 ≤ x ≤ 1.0 or None | ConfigError | FR-006 |
| SourceFile | path | Must exist, readable | FileNotFoundError | FR-020 |
| SourceFile | file_type | In [.txt, .md, .pdf, .csv, .json] | ValidationError | FR-003 |
| SourceFile | size_bytes | Warn if > 100MB | Warning (logged) | FR-016 |
| SourceFile | content | Non-empty after processing | Warning (skip file) | FR-018 |
| DocumentRecord | id | Unique, follows format | RuntimeError | Internal |
| DocumentRecord | content | Non-empty | ValidationError | FR-004 |
| DocumentRecord | embedding | Length matches model dims | ValidationError | FR-005 |
| SearchQuery | query_text | Non-empty string | ValueError | FR-006 |
| QueryResult | score | 0.0 ≤ x ≤ 1.0 | RuntimeError | FR-013 |
Schema Evolution Strategy¶
Embedding Dimension Changes:
- If embedding_model changes in config → User MUST run --force-ingest
- Tool detects dimension mismatch and raises clear error with remediation steps
New Fields:
- Adding optional fields to DocumentRecord → Backward compatible (default values)
- Adding required fields → Requires migration or --force-ingest
Backend Migration: - Redis → In-memory: No migration needed (re-ingest on startup) - In-memory → Redis: Automatic on first startup (ephemeral data lost)
Version Tracking (Future): - Store schema version in vector store metadata - Detect version mismatches and trigger migrations
Summary¶
This data model defines 7 core entities supporting the vectorstore tool feature:
- VectorStoreConfig: YAML configuration (immutable)
- VectorStoreTool: Runtime tool instance (stateful)
- DocumentRecord: Persistent vector store record (stored in Redis/in-memory)
- SourceFile: Temporary ingestion entity (ephemeral)
- QueryResult: Search result output (ephemeral)
- SearchQuery: Query input (ephemeral)
- VectorStore: Storage abstraction (stateful)
All entities follow validation rules from functional requirements (FR-001 through FR-023) and support the three user stories (P1-P3) defined in the feature specification.