app/routers/qdrant_router.py aktualisiert
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 2s
All checks were successful
Deploy mindnet to llm-node / deploy (push) Successful in 2s
This commit is contained in:
parent
3dea8ad999
commit
aeead3746b
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user