Data Model: Structured Data Field Mapping and Ingestion¶
Feature: 014-structured-data-ingestion Date: 2025-12-18 Status: Complete
Overview¶
This document defines the data models for structured data ingestion into vector stores. The key innovation is the DynamicStructuredRecord class factory that generates Semantic Kernel-compatible records with user-defined metadata fields.
Entity Relationship Diagram¶
┌─────────────────────────┐ ┌─────────────────────────┐
│ VectorstoreTool │ │ StructuredDataLoader │
│ (Configuration) │ │ (Runtime Processing) │
├─────────────────────────┤ ├─────────────────────────┤
│ name: str │ │ source_path: str │
│ source: str │────▶│ file_type: FileType │
│ id_field: str │ │ id_field: str │
│ vector_field: str|list │ │ vector_fields: list[str]│
│ metadata_fields: list │ │ metadata_fields: list │
│ field_separator: str │ │ field_separator: str │
│ delimiter: str (CSV) │ │ delimiter: str │
│ database: DatabaseConfig│ └───────────┬─────────────┘
└─────────────────────────┘ │
│ generates
▼
┌─────────────────────────────────┐
│ DynamicStructuredRecord │
│ (Vector Store Record) │
├─────────────────────────────────┤
│ id: str (key) │
│ content: str (full-text) │
│ embedding: list[float] (vector) │
│ source_file: str (indexed) │
│ [dynamic metadata fields...] │
└─────────────────────────────────┘
│
│ stored in
▼
┌─────────────────────────────────┐
│ Vector Store Collection │
│ (ChromaDB, Qdrant, Postgres...) │
└─────────────────────────────────┘
Core Entities¶
1. VectorstoreTool Configuration (Extended)¶
The existing VectorstoreTool model in src/holodeck/models/tool.py already has many fields. For structured data, we use/extend:
class VectorstoreTool(BaseModel):
"""Vectorstore tool configuration - extended for structured data."""
# Existing fields (unchanged)
name: str
description: str
type: Literal["vectorstore"] = "vectorstore"
source: str # File path or directory
embedding_model: str | None = None
embedding_dimensions: int | None = None
database: DatabaseConfig | str | None = None
top_k: int = 5
min_similarity_score: float | None = None
# Structured data fields (existing but now required for structured mode)
vector_field: str | list[str] | None = None # Field(s) to embed
meta_fields: list[str] | None = None # Metadata fields to include
# NEW fields for structured data
id_field: str | None = None # Required for structured data
field_separator: str = "\n" # Separator for multiple vector_fields
delimiter: str | None = None # CSV delimiter (auto-detect if None)
record_path: str | None = None # JSON array path (e.g., "data.items")
@model_validator(mode="after")
def validate_structured_config(self) -> "VectorstoreTool":
"""Validate structured data configuration."""
# If any structured-specific field is set, id_field becomes required
if self.vector_field is not None and self.id_field is None:
raise ValueError(
"id_field is required when vector_field is specified "
"(structured data mode)"
)
return self
Field Descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
id_field |
str | Yes (structured) | Field containing unique record identifier |
vector_field |
str | list[str] | Yes (structured) | Field(s) to embed for semantic search |
meta_fields |
list[str] | No | Fields to include in results (default: all) |
field_separator |
str | No | Separator when concatenating multiple vector_fields (default: newline) |
delimiter |
str | No | CSV delimiter (auto-detect if not specified) |
record_path |
str | No | JSON path to array of records (e.g., "data.items") |
2. DynamicStructuredRecord¶
A runtime-generated Semantic Kernel vector store record class.
# Factory function signature
def create_structured_record_class(
dimensions: int = 1536,
metadata_field_names: list[str] | None = None,
collection_name: str = "structured_records",
) -> type[Any]:
"""
Create a StructuredRecord class compatible with Semantic Kernel vector stores.
Args:
dimensions: Embedding vector dimensions
metadata_field_names: Names of metadata fields to include
collection_name: Vector store collection name
Returns:
A dataclass type with @vectorstoremodel decorator applied
"""
Generated Class Structure:
@vectorstoremodel(collection_name="structured_records")
@dataclass
class DynamicStructuredRecord:
# Fixed fields (always present)
id: Annotated[str, VectorStoreField("key")]
content: Annotated[str, VectorStoreField("data", is_full_text_indexed=True)]
embedding: Annotated[list[float] | None, VectorStoreField("vector", dimensions=N)]
source_file: Annotated[str, VectorStoreField("data", is_indexed=True)]
# Dynamic metadata fields (generated based on configuration)
# Example: if metadata_field_names = ["title", "category", "price"]
title: Annotated[str, VectorStoreField("data", is_indexed=True)]
category: Annotated[str, VectorStoreField("data", is_indexed=True)]
price: Annotated[str, VectorStoreField("data")] # Not indexed by default
Field Type Mapping:
| Source Type | Record Field Type | Notes |
|---|---|---|
| String | str | Direct mapping |
| Integer | str | Converted to string |
| Float | str | Converted to string |
| Boolean | str | "true" or "false" |
| List/Array | str | JSON-serialized |
| Object/Dict | str | JSON-serialized |
| Null | str | Empty string "" |
3. StructuredDataLoader¶
Responsible for loading and validating structured data from various sources.
class StructuredDataLoader:
"""Load and iterate over records from structured data sources."""
def __init__(
self,
source_path: str,
id_field: str,
vector_fields: list[str],
metadata_fields: list[str] | None = None,
field_separator: str = "\n",
delimiter: str | None = None,
record_path: str | None = None,
batch_size: int = 10_000,
):
"""
Initialize the loader.
Args:
source_path: Path to data file
id_field: Field containing unique record ID
vector_fields: Fields to embed
metadata_fields: Fields to include as metadata (None = all)
field_separator: Separator for concatenating vector fields
delimiter: CSV delimiter (auto-detect if None)
record_path: JSON path to records array
batch_size: Number of records per batch for large files
"""
def validate_schema(self) -> list[str]:
"""
Validate configuration against source schema.
Returns:
List of available field names
Raises:
ConfigError: If configured fields don't exist in source
"""
def iter_records(self) -> Iterator[dict[str, Any]]:
"""
Iterate over records from the source.
Yields:
Dict with keys: id, content, metadata (dict of field values)
"""
def iter_batches(self) -> Iterator[list[dict[str, Any]]]:
"""
Iterate over batches of records.
Yields:
List of record dicts (up to batch_size)
"""
4. StructuredQueryResult¶
Search result with dynamic metadata fields.
@dataclass
class StructuredQueryResult:
"""Search result from structured data vector store query."""
id: str # Original record ID from id_field
content: str # Concatenated vector field content
score: float # Similarity score (0.0-1.0)
source_file: str # Original source file path
metadata: dict[str, Any] # All metadata field values
def __post_init__(self) -> None:
"""Validate score is in valid range."""
if not (0.0 <= self.score <= 1.0):
raise ValueError(f"Score must be between 0.0 and 1.0, got {self.score}")
State Transitions¶
Record Ingestion States¶
┌─────────────┐
│ Source │
│ File │
└──────┬──────┘
│ load
▼
┌─────────────┐
│ Validated │ ← Schema validation (fields exist)
│ Records │
└──────┬──────┘
│ transform
▼
┌─────────────┐
│ Structured │ ← ID extracted, content concatenated
│ Records │
└──────┬──────┘
│ embed
▼
┌─────────────┐
│ Embedded │ ← Embeddings generated
│ Records │
└──────┬──────┘
│ upsert
▼
┌─────────────┐
│ Stored │ ← In vector database
│ Records │
└─────────────┘
Validation Rules¶
Configuration Validation¶
| Rule | Condition | Error Message |
|---|---|---|
| V1 | id_field required when vector_field set |
"id_field is required for structured data mode" |
| V2 | vector_field XOR vector_fields |
"Cannot specify both vector_field and vector_fields" |
| V3 | Fields must exist in source | "Field '{name}' not found. Available: {fields}" |
| V4 | id_field must have unique values |
"Duplicate ID found: {id}" |
| V5 | Vector field(s) cannot be empty for all records | "No non-empty values found for vector field(s)" |
Runtime Validation¶
| Rule | Condition | Behavior |
|---|---|---|
| R1 | Record has null/empty vector fields | Skip with warning log |
| R2 | Record has duplicate ID | Update existing (upsert) |
| R3 | Field value is non-string | Convert to string |
| R4 | Nested field path invalid | Return empty string |
Example Data¶
CSV Example (products.csv)¶
id,title,description,category,price
1,Widget Pro,"Advanced widget with AI features",Electronics,99.99
2,Super Gadget,"Multi-purpose gadget for daily use",Electronics,149.99
3,Home Helper,"Smart home automation device",Smart Home,199.99
Configuration:
tools:
- name: product_search
type: vectorstore
source: data/products.csv
id_field: id
vector_field: description
meta_fields: [title, category, price]
Generated Records:
[
DynamicStructuredRecord(
id="1",
content="Advanced widget with AI features",
embedding=[...],
source_file="data/products.csv",
title="Widget Pro",
category="Electronics",
price="99.99"
),
# ... more records
]
JSON Example (faqs.json)¶
{
"data": {
"items": [
{
"faq_id": "FAQ001",
"question": "How do I reset my password?",
"answer": "Go to Settings > Security > Reset Password",
"category": "Account"
}
]
}
}
Configuration:
tools:
- name: faq_search
type: vectorstore
source: data/faqs.json
record_path: data.items
id_field: faq_id
vector_fields: [question, answer]
field_separator: "\n\n"
meta_fields: [category]
Generated Content (concatenated):
How do I reset my password?
Go to Settings > Security > Reset Password
Integration Points¶
With Existing VectorstoreTool¶
The structured data loader integrates with the existing vectorstore tool execution:
# In vectorstore_tool.py execution flow
async def execute_vectorstore_tool(tool_config: VectorstoreTool, query: str):
# Detect structured vs unstructured mode
if tool_config.vector_field is not None:
# Structured mode
loader = StructuredDataLoader(
source_path=tool_config.source,
id_field=tool_config.id_field,
vector_fields=normalize_vector_fields(tool_config.vector_field),
metadata_fields=tool_config.meta_fields,
field_separator=tool_config.field_separator,
delimiter=tool_config.delimiter,
record_path=tool_config.record_path,
)
record_class = create_structured_record_class(
dimensions=tool_config.embedding_dimensions or 1536,
metadata_field_names=loader.get_metadata_field_names(),
)
# ... proceed with structured ingestion
else:
# Existing unstructured mode
# ... existing DocumentRecord flow
With Vector Store Collections¶
Uses the same get_collection_factory() from vector_store.py:
factory = get_collection_factory(
provider=database_config.provider,
dimensions=embedding_dimensions,
**connection_kwargs,
)
async with factory() as collection:
# Upsert structured records
await collection.upsert(structured_records)