app/routers/qdrant_router.py aktualisiert
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 2s

This commit is contained in:
Lars 2025-09-02 10:30:01 +02:00
parent 3dea8ad999
commit aeead3746b

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Any, Optional, List from typing import Any, Optional, List
import uuid import uuid
from fastapi import APIRouter, HTTPException from fastapi import APIRouter
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from qdrant_client import QdrantClient from qdrant_client import QdrantClient
from qdrant_client.http.models import ( from qdrant_client.http.models import (
@ -20,26 +20,20 @@ from ..embeddings import embed_texts
router = APIRouter(prefix="/qdrant", tags=["qdrant"]) router = APIRouter(prefix="/qdrant", tags=["qdrant"])
def _client() -> QdrantClient: def _client() -> QdrantClient:
s = get_settings() s = get_settings()
return QdrantClient(url=s.QDRANT_URL, api_key=s.QDRANT_API_KEY) return QdrantClient(url=s.QDRANT_URL, api_key=s.QDRANT_API_KEY)
def _col(name: str) -> str: def _col(name: str) -> str:
return f"{get_settings().COLLECTION_PREFIX}_{name}" return f"{get_settings().COLLECTION_PREFIX}_{name}"
def _uuid5(s: str) -> str: def _uuid5(s: str) -> str:
"""Deterministic UUIDv5 from arbitrary string (server-side point id).""" """Deterministic UUIDv5 from arbitrary string (server-side point id)."""
return str(uuid.uuid5(uuid.NAMESPACE_URL, s)) return str(uuid.uuid5(uuid.NAMESPACE_URL, s))
# --- Models --- # --- Models ---
class BaseMeta(BaseModel): class BaseMeta(BaseModel):
note_id: str = Field( note_id: str = Field(..., description="Stable ID of the note (e.g., hash of vault-relative path)")
..., description="Stable ID of the note (e.g., hash of vault-relative path)"
)
title: Optional[str] = Field(None, description="Note or chunk title") title: Optional[str] = Field(None, description="Note or chunk title")
path: Optional[str] = Field(None, description="Vault-relative path to the .md file") path: Optional[str] = Field(None, description="Vault-relative path to the .md file")
Typ: Optional[str] = None Typ: Optional[str] = None
@ -47,19 +41,14 @@ class BaseMeta(BaseModel):
tags: Optional[List[str]] = None tags: Optional[List[str]] = None
Rolle: Optional[List[str]] = None # allow list Rolle: Optional[List[str]] = None # allow list
class UpsertChunkRequest(BaseMeta): class UpsertChunkRequest(BaseMeta):
chunk_id: str = Field(..., description="Stable ID of the chunk within the note") chunk_id: str = Field(..., description="Stable ID of the chunk within the note")
text: str = Field(..., description="Chunk text content") text: str = Field(..., description="Chunk text content")
links: Optional[List[str]] = Field( links: Optional[List[str]] = Field(default=None, description="Outbound links detected in the chunk")
default=None, description="Outbound links detected in the chunk"
)
class UpsertNoteRequest(BaseMeta): class UpsertNoteRequest(BaseMeta):
text: Optional[str] = Field(None, description="Full note text (optional)") text: Optional[str] = Field(None, description="Full note text (optional)")
class UpsertEdgeRequest(BaseModel): class UpsertEdgeRequest(BaseModel):
src_note_id: str src_note_id: str
dst_note_id: Optional[str] = None dst_note_id: Optional[str] = None
@ -68,7 +57,6 @@ class UpsertEdgeRequest(BaseModel):
relation: str = Field(default="links_to") relation: str = Field(default="links_to")
link_text: Optional[str] = None link_text: Optional[str] = None
class QueryRequest(BaseModel): class QueryRequest(BaseModel):
query: str query: str
limit: int = 5 limit: int = 5
@ -76,7 +64,6 @@ class QueryRequest(BaseModel):
path: Optional[str] = None path: Optional[str] = None
tags: Optional[List[str]] = None tags: Optional[List[str]] = None
# --- Helpers --- # --- Helpers ---
def _ensure_collections(): def _ensure_collections():
s = get_settings() s = get_settings()
@ -85,27 +72,17 @@ def _ensure_collections():
try: try:
cli.get_collection(_col("chunks")) cli.get_collection(_col("chunks"))
except Exception: except Exception:
cli.recreate_collection( cli.recreate_collection(_col("chunks"), vectors_config=VectorParams(size=s.VECTOR_SIZE, distance=Distance.COSINE))
_col("chunks"),
vectors_config=VectorParams(size=s.VECTOR_SIZE, distance=Distance.COSINE),
)
# notes # notes
try: try:
cli.get_collection(_col("notes")) cli.get_collection(_col("notes"))
except Exception: except Exception:
cli.recreate_collection( cli.recreate_collection(_col("notes"), vectors_config=VectorParams(size=s.VECTOR_SIZE, distance=Distance.COSINE))
_col("notes"),
vectors_config=VectorParams(size=s.VECTOR_SIZE, distance=Distance.COSINE),
)
# edges (dummy vector of size 1) # edges (dummy vector of size 1)
try: try:
cli.get_collection(_col("edges")) cli.get_collection(_col("edges"))
except Exception: except Exception:
cli.recreate_collection( cli.recreate_collection(_col("edges"), vectors_config=VectorParams(size=1, distance=Distance.COSINE))
_col("edges"),
vectors_config=VectorParams(size=1, distance=Distance.COSINE),
)
@router.post("/upsert_chunk", summary="Upsert a chunk into mindnet_chunks") @router.post("/upsert_chunk", summary="Upsert a chunk into mindnet_chunks")
def upsert_chunk(req: UpsertChunkRequest) -> dict: def upsert_chunk(req: UpsertChunkRequest) -> dict:
@ -114,16 +91,12 @@ def upsert_chunk(req: UpsertChunkRequest) -> dict:
vec = embed_texts([req.text])[0] vec = embed_texts([req.text])[0]
payload: dict[str, Any] = req.model_dump() payload: dict[str, Any] = req.model_dump()
payload.pop("text", None) payload.pop("text", None)
# Also store short preview payload["preview"] = (req.text[:240] + "") if len(req.text) > 240 else req.text
payload["preview"] = (
(req.text[:240] + "") if len(req.text) > 240 else req.text
)
qdrant_id = _uuid5(f"chunk:{req.chunk_id}") qdrant_id = _uuid5(f"chunk:{req.chunk_id}")
pt = PointStruct(id=qdrant_id, vector=vec, payload=payload) pt = PointStruct(id=qdrant_id, vector=vec, payload=payload)
cli.upsert(collection_name=_col("chunks"), points=[pt]) cli.upsert(collection_name=_col("chunks"), points=[pt])
return {"status": "ok", "id": qdrant_id} return {"status": "ok", "id": qdrant_id}
@router.post("/upsert_note", summary="Upsert a note into mindnet_notes") @router.post("/upsert_note", summary="Upsert a note into mindnet_notes")
def upsert_note(req: UpsertNoteRequest) -> dict: def upsert_note(req: UpsertNoteRequest) -> dict:
_ensure_collections() _ensure_collections()
@ -137,24 +110,18 @@ def upsert_note(req: UpsertNoteRequest) -> dict:
cli.upsert(collection_name=_col("notes"), points=[pt]) cli.upsert(collection_name=_col("notes"), points=[pt])
return {"status": "ok", "id": qdrant_id} return {"status": "ok", "id": qdrant_id}
@router.post("/upsert_edge", summary="Upsert a graph edge into mindnet_edges") @router.post("/upsert_edge", summary="Upsert a graph edge into mindnet_edges")
def upsert_edge(req: UpsertEdgeRequest) -> dict: def upsert_edge(req: UpsertEdgeRequest) -> dict:
_ensure_collections() _ensure_collections()
cli = _client() cli = _client()
payload = req.model_dump() payload = req.model_dump()
# dummy vector
vec = [0.0] vec = [0.0]
raw_edge_id = ( raw_edge_id = f"{req.src_note_id}|{req.src_chunk_id or ''}->{req.dst_note_id or ''}|{req.dst_chunk_id or ''}|{req.relation}"
f"{req.src_note_id}|{req.src_chunk_id or ''}->"
f"{req.dst_note_id or ''}|{req.dst_chunk_id or ''}|{req.relation}"
)
qdrant_id = _uuid5(f"edge:{raw_edge_id}") qdrant_id = _uuid5(f"edge:{raw_edge_id}")
pt = PointStruct(id=qdrant_id, vector=vec, payload=payload) pt = PointStruct(id=qdrant_id, vector=vec, payload=payload)
cli.upsert(collection_name=_col("edges"), points=[pt]) cli.upsert(collection_name=_col("edges"), points=[pt])
return {"status": "ok", "id": qdrant_id} return {"status": "ok", "id": qdrant_id}
@router.post("/query", summary="Vector query over mindnet_chunks with optional filters") @router.post("/query", summary="Vector query over mindnet_chunks with optional filters")
def query(req: QueryRequest) -> dict: def query(req: QueryRequest) -> dict:
_ensure_collections() _ensure_collections()
@ -168,25 +135,16 @@ def query(req: QueryRequest) -> dict:
if req.path: if req.path:
conds.append(FieldCondition(key="path", match=MatchValue(value=req.path))) conds.append(FieldCondition(key="path", match=MatchValue(value=req.path)))
if req.tags: if req.tags:
# tags as keyword list -> match any of the tags (OR)
for t in req.tags: for t in req.tags:
conds.append(FieldCondition(key="tags", match=MatchValue(value=t))) conds.append(FieldCondition(key="tags", match=MatchValue(value=t)))
if conds: if conds:
flt = Filter(must=conds) flt = Filter(must=conds)
res = cli.search( res = cli.search(collection_name=_col("chunks"), query_vector=vec, limit=req.limit, with_payload=True, with_vectors=False, query_filter=flt)
collection_name=_col("chunks"),
query_vector=vec,
limit=req.limit,
with_payload=True,
with_vectors=False,
query_filter=flt,
)
hits = [] hits = []
for p in res: for p in res:
pl = p.payload or {} pl = p.payload or {}
hits.append( hits.append({
{
"chunk_id": p.id, "chunk_id": p.id,
"score": p.score, "score": p.score,
"note_id": pl.get("note_id"), "note_id": pl.get("note_id"),
@ -194,6 +152,5 @@ def query(req: QueryRequest) -> dict:
"path": pl.get("path"), "path": pl.get("path"),
"preview": pl.get("preview"), "preview": pl.get("preview"),
"tags": pl.get("tags"), "tags": pl.get("tags"),
} })
)
return {"results": hits} return {"results": hits}