feat: index semantic records in qdrant

This commit is contained in:
2026-08-08 18:03:57 +02:00
parent f61648fb69
commit 5e39cfa347
13 changed files with 516 additions and 29 deletions
+78 -1
View File
@@ -152,8 +152,14 @@ class ThothVectorHttpConfig(BaseModel):
direct: DatabaseConfig | None = None
class QdrantConfig(BaseModel):
type: Literal["qdrant"]
base_url: str
collection: str = Field(min_length=1)
VectorResourceConfig = Annotated[
PgvectorDirectConfig | ThothVectorHttpConfig,
PgvectorDirectConfig | ThothVectorHttpConfig | QdrantConfig,
Field(discriminator="type"),
]
@@ -397,6 +403,7 @@ def load_config(path: Path) -> Config:
raise ConfigError(f"Configurazione non valida (atteso un mapping YAML): {path}")
expanded = _resolve_secret_files(_expand_env(raw))
_validate_internal_embedding_contract(expanded, path)
_validate_internal_vector_contract(expanded, path)
translated, used_legacy = translate_legacy_config(expanded)
_populate_legacy_views(translated)
try:
@@ -455,6 +462,7 @@ def load_config(path: Path) -> Config:
else path.resolve().as_posix()
)
_validate_active_embeddings_config(cfg.embeddings, path)
_validate_active_vector_config(cfg.vectors, path)
return cfg
@@ -500,6 +508,42 @@ def _validate_internal_embedding_contract(raw: dict[str, Any], path: Path) -> No
)
def _validate_internal_vector_contract(raw: dict[str, Any], path: Path) -> None:
resources = raw.get("resources")
if not isinstance(resources, dict):
return
vector = resources.get("vector")
if not isinstance(vector, dict):
return
engine = vector.get("engine")
base_url = vector.get("base_url")
collection = vector.get("collection")
allowed = {"engine", "base_url", "collection"}
unexpected = sorted(set(vector) - allowed)
if unexpected:
raise ConfigError(
f"Configurazione non valida in {path}:\n"
f"resources.vector non supporta: {', '.join(unexpected)}"
)
if engine != "qdrant":
raise ConfigError(
f"Configurazione non valida in {path}:\n"
"resources.vector.engine deve essere 'qdrant'"
)
if not isinstance(collection, str) or not collection:
raise ConfigError(
f"Configurazione non valida in {path}:\n"
"resources.vector.collection deve essere valorizzato"
)
if not _is_allowed_internal_qdrant_url(base_url):
raise ConfigError(
f"Configurazione non valida in {path}:\n"
"resources.vector.base_url deve usare http://qdrant:6333 "
"oppure un endpoint loopback di sviluppo su porta 6333"
)
def _validate_active_embeddings_config(
embeddings: "EmbeddingsConfig | None",
path: Path,
@@ -529,6 +573,20 @@ def _validate_active_embeddings_config(
)
def _validate_active_vector_config(
vectors: "VectorResourceConfig | None",
path: Path,
) -> None:
if vectors is None or vectors.type != "qdrant":
return
if not _is_allowed_internal_qdrant_url(vectors.base_url):
raise ConfigError(
f"Configurazione non valida in {path}:\n"
"vectors.base_url deve usare http://qdrant:6333 "
"oppure un endpoint loopback di sviluppo su porta 6333"
)
def _is_allowed_internal_embedding_url(value: Any) -> bool:
if not isinstance(value, str):
return False
@@ -548,6 +606,25 @@ def _is_allowed_internal_embedding_url(value: Any) -> bool:
return host.is_loopback
def _is_allowed_internal_qdrant_url(value: Any) -> bool:
if not isinstance(value, str):
return False
parsed = urlparse(value)
if parsed.scheme != "http" or not parsed.hostname or parsed.port != 6333:
return False
if parsed.params or parsed.query or parsed.fragment:
return False
if parsed.path not in ("", "/"):
return False
if parsed.hostname == "qdrant":
return True
try:
host = ip_address(parsed.hostname)
except ValueError:
return parsed.hostname == "localhost"
return host.is_loopback
def _populate_legacy_views(raw: dict[str, Any]) -> None:
"""Populate old Config attributes for command compatibility during migration."""
dwh = raw.get("dwh")