"""An in-memory stand-in for Qdrant, used by the pipeline tests. It implements the routes the pipeline actually calls and nothing else, and it is deliberately dumb: brute-force cosine over a dict. If the pipeline passes against this, the pipeline's own logic is right; the real server is exercised separately in `docs/phase-4-notes.md` (rebuild against a live container). """ from __future__ import annotations import math from corpus.qdrant import Qdrant, QdrantError, QdrantNotFound class FakeQdrant(Qdrant): def __init__(self, dim: int): super().__init__(url="fake://qdrant", timeout=1.0, attempts=1) self.dim = dim self.collections = {} # name -> {id: {"vector": [...], "payload": {...}}} self.calls = [] # -- transport -------------------------------------------------------- def _request(self, method: str, path: str, body=None) -> dict: self.calls.append((method, path, body)) query = "" if "?" in path: path, query = path.split("?", 1) parts = [part for part in path.split("/") if part] if not parts: return {"version": "fake-1.0"} if parts[0] != "collections": raise QdrantNotFound(f"no route {path}") if len(parts) == 2: name = parts[1] if method == "PUT": self.collections.setdefault(name, {}) return {"result": True} if method == "DELETE": if name not in self.collections: raise QdrantNotFound("collection not found") del self.collections[name] return {"result": True} if name not in self.collections: raise QdrantNotFound("collection not found") return { "result": { "points_count": len(self.collections[name]), "config": {"params": {"vectors": {"size": self.dim, "distance": "Cosine"}}}, } } name = parts[1] if name not in self.collections: raise QdrantNotFound("collection not found") points = self.collections[name] if parts[2:4] == ["points", "count"]: return {"result": {"count": len(points)}} if len(parts) == 3 and parts[2] == "points" and method == "PUT": for point in (body or {}).get("points", []): if len(point["vector"]) != self.dim: raise QdrantError("wrong vector dimension") points[point["id"]] = {"vector": point["vector"], "payload": point["payload"]} return {"result": {"status": "completed"}} if parts[2:4] == ["points", "delete"]: must = (((body or {}).get("filter") or {}).get("must")) or [] for point_id in list(points): if _matches(points[point_id]["payload"], must): del points[point_id] return {"result": {"status": "completed"}} if parts[2:4] == ["points", "query"]: vector = body["query"] must = (((body or {}).get("filter") or {}).get("must")) or [] scored = [] for point_id, point in points.items(): if not _matches(point["payload"], must): continue scored.append( { "id": point_id, "score": _cosine(vector, point["vector"]), "payload": point["payload"], } ) scored.sort(key=lambda row: row["score"], reverse=True) return {"result": {"points": scored[: int(body.get("limit", 10))]}} if parts[2:4] == ["points", "scroll"]: limit = int((body or {}).get("limit", 100)) rows = [ {"id": point_id, "payload": {"source_path": point["payload"].get("source_path")}} for point_id, point in list(points.items())[:limit] ] return {"result": {"points": rows, "next_page_offset": None}} raise QdrantNotFound(f"no route {path}") def _matches(payload: dict, conditions: list) -> bool: for condition in conditions: key = condition.get("key") expected = (condition.get("match") or {}).get("value") if payload.get(key) != expected: return False return True def _cosine(left: list, right: list) -> float: dot = sum(a * b for a, b in zip(left, right)) norm = math.sqrt(sum(a * a for a in left)) * math.sqrt(sum(b * b for b in right)) return dot / norm if norm else 0.0