@vivantel/virage-store-qdrant 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @vivantel/virage-store-qdrant
2
+
3
+ Qdrant vector store for [`@vivantel/virage-core`](../rag-core/README.md). Works with both local Qdrant instances and Qdrant Cloud (SaaS).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @vivantel/virage-store-qdrant @vivantel/virage-core
9
+ ```
10
+
11
+ ## Quick start (JSON config)
12
+
13
+ ```json
14
+ {
15
+ "vectorStore": {
16
+ "package": "@vivantel/virage-store-qdrant",
17
+ "config": {
18
+ "url": "${QDRANT_URL}",
19
+ "apiKey": "${QDRANT_API_KEY}"
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ For a local instance (no API key required):
26
+
27
+ ```json
28
+ {
29
+ "vectorStore": {
30
+ "package": "@vivantel/virage-store-qdrant",
31
+ "config": {
32
+ "url": "http://localhost:6333"
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ | Option | Type | Default | Description |
41
+ |--------|------|---------|-------------|
42
+ | `url` | `string` | **required** | Qdrant instance URL |
43
+ | `apiKey` | `string` | `undefined` | API key for Qdrant Cloud |
44
+ | `collection` | `string` | `"documents"` | Collection name |
45
+ | `dimensions` | `number` | `1536` | Vector size — must match your embedder |
46
+
47
+ ## TypeScript usage
48
+
49
+ ```typescript
50
+ import { QdrantVectorStore } from "@vivantel/virage-store-qdrant";
51
+
52
+ const store = new QdrantVectorStore({
53
+ url: process.env.QDRANT_URL!,
54
+ apiKey: process.env.QDRANT_API_KEY,
55
+ collection: "my-docs",
56
+ dimensions: 1536,
57
+ });
58
+ ```
59
+
60
+ ## Observability (ROADMAP v2)
61
+
62
+ ### Index stats (`virage store stats`)
63
+
64
+ ```typescript
65
+ const stats = await store.getIndexStats();
66
+ // {
67
+ // totalVectors: 12500,
68
+ // indexType: "hnsw",
69
+ // annRecallAt10: 0.96,
70
+ // indexAgeHours: 0,
71
+ // deadTupleFraction: 0,
72
+ // suggestions: ["Collection looks healthy"]
73
+ // }
74
+ ```
75
+
76
+ ### Query performance (`virage store perf --timeframe 24`)
77
+
78
+ ```typescript
79
+ const perf = await store.getQueryPerfReport(24);
80
+ // {
81
+ // timeframeHours: 24,
82
+ // p50LatencyMs: 3.2,
83
+ // p95LatencyMs: 8.7,
84
+ // p99LatencyMs: 14.1,
85
+ // slowQueryCount: 0,
86
+ // suggestedIndexes: ["Query performance looks healthy."]
87
+ // }
88
+ ```
89
+
90
+ Query performance metrics require Qdrant's `/metrics` endpoint to be reachable. Metrics are returned as `-1` if telemetry is not available.
91
+
92
+ ## Running Qdrant locally
93
+
94
+ ```bash
95
+ docker run -p 6333:6333 qdrant/qdrant
96
+ ```
@@ -0,0 +1,5 @@
1
+ export { QdrantVectorStore, type QdrantVectorStoreOptions } from "./store.js";
2
+ import type { VectorStore } from "@vivantel/virage-core";
3
+ /** Factory used by the JSON config loader. */
4
+ export declare function createVectorStore(config: Record<string, unknown>): VectorStore;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAE9E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,8CAA8C;AAC9C,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,WAAW,CAkBb"}
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ export { QdrantVectorStore } from "./store.js";
2
+ import { QdrantVectorStore } from "./store.js";
3
+ /** Factory used by the JSON config loader. */
4
+ export function createVectorStore(config) {
5
+ const url = typeof config.url === "string" ? config.url : undefined;
6
+ const path = typeof config.path === "string" ? config.path : undefined;
7
+ if (!url && !path) {
8
+ throw new Error("@vivantel/virage-store-qdrant: config.url or config.path is required");
9
+ }
10
+ return new QdrantVectorStore({
11
+ url,
12
+ path,
13
+ port: typeof config.port === "number" ? config.port : undefined,
14
+ apiKey: typeof config.apiKey === "string" ? config.apiKey : undefined,
15
+ collection: typeof config.collection === "string" ? config.collection : undefined,
16
+ dimensions: typeof config.dimensions === "number" ? config.dimensions : undefined,
17
+ });
18
+ }
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAiC,MAAM,YAAY,CAAC;AAG9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C,8CAA8C;AAC9C,MAAM,UAAU,iBAAiB,CAC/B,MAA+B;IAE/B,MAAM,GAAG,GAAG,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IACpE,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACvE,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,sEAAsE,CACvE,CAAC;IACJ,CAAC;IACD,OAAO,IAAI,iBAAiB,CAAC;QAC3B,GAAG;QACH,IAAI;QACJ,IAAI,EAAE,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;QAC/D,MAAM,EAAE,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;QACrE,UAAU,EACR,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QACvE,UAAU,EACR,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;KACxE,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { QueryPerfReport } from "@vivantel/virage-core";
2
+ export declare function getQueryPerfReport(qdrantUrl: string, _collection: string, timeframeHours: number): Promise<QueryPerfReport>;
3
+ //# sourceMappingURL=query-perf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-perf.d.ts","sourceRoot":"","sources":["../src/query-perf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAc7D,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,eAAe,CAAC,CA4C1B"}
@@ -0,0 +1,81 @@
1
+ const UNAVAILABLE = {
2
+ timeframeHours: 0,
3
+ p50LatencyMs: -1,
4
+ p95LatencyMs: -1,
5
+ p99LatencyMs: -1,
6
+ slowQueryCount: -1,
7
+ suggestedIndexes: [
8
+ "Qdrant telemetry not available. Ensure the /metrics endpoint is reachable " +
9
+ "and telemetry is enabled in your Qdrant configuration.",
10
+ ],
11
+ };
12
+ export async function getQueryPerfReport(qdrantUrl, _collection, timeframeHours) {
13
+ const metricsUrl = `${qdrantUrl.replace(/\/$/, "")}/metrics`;
14
+ let text;
15
+ try {
16
+ const res = await fetch(metricsUrl, { signal: AbortSignal.timeout(5000) });
17
+ if (!res.ok)
18
+ return { ...UNAVAILABLE, timeframeHours };
19
+ text = await res.text();
20
+ }
21
+ catch {
22
+ return { ...UNAVAILABLE, timeframeHours };
23
+ }
24
+ const latencies = parsePrometheusHistogram(text);
25
+ if (latencies.length === 0) {
26
+ return { ...UNAVAILABLE, timeframeHours };
27
+ }
28
+ latencies.sort((a, b) => a - b);
29
+ const percentile = (p) => latencies[Math.floor((p / 100) * latencies.length)] ?? 0;
30
+ const p50 = percentile(50) * 1000;
31
+ const p95 = percentile(95) * 1000;
32
+ const p99 = percentile(99) * 1000;
33
+ const slowQueryCount = latencies.filter((ms) => ms * 1000 > 100).length;
34
+ const suggestedIndexes = [];
35
+ if (slowQueryCount > 0) {
36
+ suggestedIndexes.push(`${slowQueryCount} slow queries (>100ms) detected. Consider tuning hnsw_config ef or increasing hardware resources.`);
37
+ }
38
+ else {
39
+ suggestedIndexes.push("Query performance looks healthy.");
40
+ }
41
+ return {
42
+ timeframeHours,
43
+ p50LatencyMs: Math.round(p50 * 10) / 10,
44
+ p95LatencyMs: Math.round(p95 * 10) / 10,
45
+ p99LatencyMs: Math.round(p99 * 10) / 10,
46
+ slowQueryCount,
47
+ suggestedIndexes,
48
+ };
49
+ }
50
+ /**
51
+ * Reconstruct a latency sample list from Prometheus histogram sum/count lines.
52
+ * Returns latency values in seconds.
53
+ */
54
+ function parsePrometheusHistogram(text) {
55
+ const samples = [];
56
+ // Match _sum and _count lines for REST API duration metrics
57
+ const sumPattern = /^rest_api_response_duration_seconds_sum\{[^}]*\}\s+([\d.e+]+)/gm;
58
+ const countPattern = /^rest_api_response_duration_seconds_count\{[^}]*\}\s+([\d.e+]+)/gm;
59
+ const sums = [];
60
+ const counts = [];
61
+ for (const m of text.matchAll(sumPattern)) {
62
+ sums.push(parseFloat(m[1]));
63
+ }
64
+ for (const m of text.matchAll(countPattern)) {
65
+ counts.push(parseFloat(m[1]));
66
+ }
67
+ for (let i = 0; i < Math.min(sums.length, counts.length); i++) {
68
+ const count = counts[i];
69
+ const sum = sums[i];
70
+ if (count > 0 && sum >= 0) {
71
+ const mean = sum / count;
72
+ // Approximate distribution: repeat the mean `min(count, 10)` times
73
+ const weight = Math.min(Math.round(count), 10);
74
+ for (let j = 0; j < weight; j++) {
75
+ samples.push(mean);
76
+ }
77
+ }
78
+ }
79
+ return samples;
80
+ }
81
+ //# sourceMappingURL=query-perf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-perf.js","sourceRoot":"","sources":["../src/query-perf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=query-perf.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-perf.test.d.ts","sourceRoot":"","sources":["../src/query-perf.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { getQueryPerfReport } from "./query-perf.js";
3
+ // Prometheus histogram text helpers
4
+ function prometheusMetrics(entries) {
5
+ return entries
6
+ .map(({ sum, count }, i) => `rest_api_response_duration_seconds_sum{method="POST",endpoint="/search/${i}",status="200"} ${sum}\n` +
7
+ `rest_api_response_duration_seconds_count{method="POST",endpoint="/search/${i}",status="200"} ${count}`)
8
+ .join("\n");
9
+ }
10
+ function mockFetch(status, body) {
11
+ return vi.fn().mockResolvedValue({
12
+ ok: status >= 200 && status < 300,
13
+ text: vi.fn().mockResolvedValue(body),
14
+ });
15
+ }
16
+ afterEach(() => {
17
+ vi.unstubAllGlobals();
18
+ });
19
+ const UNAVAILABLE_SENTINEL = -1;
20
+ describe("getQueryPerfReport (Qdrant)", () => {
21
+ describe("metrics endpoint unavailable", () => {
22
+ it("returns all -1 latencies when fetch throws", async () => {
23
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNREFUSED")));
24
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
25
+ expect(report.p50LatencyMs).toBe(UNAVAILABLE_SENTINEL);
26
+ expect(report.p95LatencyMs).toBe(UNAVAILABLE_SENTINEL);
27
+ expect(report.p99LatencyMs).toBe(UNAVAILABLE_SENTINEL);
28
+ expect(report.slowQueryCount).toBe(UNAVAILABLE_SENTINEL);
29
+ });
30
+ it("returns all -1 latencies when /metrics returns non-OK status", async () => {
31
+ vi.stubGlobal("fetch", mockFetch(503, "Service Unavailable"));
32
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
33
+ expect(report.p50LatencyMs).toBe(UNAVAILABLE_SENTINEL);
34
+ });
35
+ it("returns all -1 latencies when /metrics body has no matching histogram lines", async () => {
36
+ vi.stubGlobal("fetch", mockFetch(200, "# HELP other_metric something\nother_metric 1.0\n"));
37
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
38
+ expect(report.p50LatencyMs).toBe(UNAVAILABLE_SENTINEL);
39
+ });
40
+ it("includes a telemetry-not-available hint in suggestedIndexes", async () => {
41
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("timeout")));
42
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
43
+ expect(report.suggestedIndexes[0]).toMatch(/telemetry|metrics/i);
44
+ });
45
+ it("preserves timeframeHours even when unavailable", async () => {
46
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("timeout")));
47
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 48);
48
+ expect(report.timeframeHours).toBe(48);
49
+ });
50
+ });
51
+ describe("metrics endpoint reachable", () => {
52
+ it("computes valid percentiles from Prometheus histogram (p50 ≤ p95 ≤ p99)", async () => {
53
+ // 5 entries, mean latencies 10ms–50ms (0.01–0.05 s)
54
+ const body = prometheusMetrics([
55
+ { sum: 0.05, count: 5 }, // mean 0.01 s = 10 ms
56
+ { sum: 0.06, count: 3 }, // mean 0.02 s = 20 ms
57
+ { sum: 0.09, count: 3 }, // mean 0.03 s = 30 ms
58
+ { sum: 0.16, count: 4 }, // mean 0.04 s = 40 ms
59
+ { sum: 0.25, count: 5 }, // mean 0.05 s = 50 ms
60
+ ]);
61
+ vi.stubGlobal("fetch", mockFetch(200, body));
62
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
63
+ expect(report.p50LatencyMs).toBeGreaterThanOrEqual(0);
64
+ expect(report.p95LatencyMs).toBeGreaterThanOrEqual(report.p50LatencyMs);
65
+ expect(report.p99LatencyMs).toBeGreaterThanOrEqual(report.p95LatencyMs);
66
+ });
67
+ it("timeframeHours is passed through to the report", async () => {
68
+ const body = prometheusMetrics([{ sum: 0.05, count: 5 }]);
69
+ vi.stubGlobal("fetch", mockFetch(200, body));
70
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 72);
71
+ expect(report.timeframeHours).toBe(72);
72
+ });
73
+ it("counts slow queries when mean latency > 100 ms", async () => {
74
+ // 2 fast entries (10ms, 50ms) + 2 slow (150ms, 200ms)
75
+ const body = prometheusMetrics([
76
+ { sum: 0.05, count: 5 }, // 10 ms
77
+ { sum: 0.15, count: 3 }, // 50 ms
78
+ { sum: 0.45, count: 3 }, // 150 ms — slow
79
+ { sum: 0.4, count: 2 }, // 200 ms — slow
80
+ ]);
81
+ vi.stubGlobal("fetch", mockFetch(200, body));
82
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
83
+ expect(report.slowQueryCount).toBeGreaterThan(0);
84
+ expect(report.suggestedIndexes[0]).toMatch(/slow|hnsw/i);
85
+ });
86
+ it("returns healthy suggestion when no slow queries", async () => {
87
+ const body = prometheusMetrics([
88
+ { sum: 0.02, count: 4 }, // 5 ms
89
+ { sum: 0.06, count: 3 }, // 20 ms
90
+ ]);
91
+ vi.stubGlobal("fetch", mockFetch(200, body));
92
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
93
+ expect(report.slowQueryCount).toBe(0);
94
+ expect(report.suggestedIndexes[0]).toMatch(/healthy/i);
95
+ });
96
+ it("normalises the metrics URL by stripping a trailing slash", async () => {
97
+ const fetchSpy = mockFetch(200, prometheusMetrics([{ sum: 0.01, count: 2 }]));
98
+ vi.stubGlobal("fetch", fetchSpy);
99
+ await getQueryPerfReport("http://localhost:6333/", "docs", 24);
100
+ const calledUrl = fetchSpy.mock.calls[0][0];
101
+ expect(calledUrl).toBe("http://localhost:6333/metrics");
102
+ });
103
+ it("ignores histogram entries where count is 0", async () => {
104
+ const body = 'rest_api_response_duration_seconds_sum{method="POST"} 0\n' +
105
+ 'rest_api_response_duration_seconds_count{method="POST"} 0\n';
106
+ vi.stubGlobal("fetch", mockFetch(200, body));
107
+ const report = await getQueryPerfReport("http://localhost:6333", "docs", 24);
108
+ // zero-count entry produces no samples → falls back to UNAVAILABLE
109
+ expect(report.p50LatencyMs).toBe(UNAVAILABLE_SENTINEL);
110
+ });
111
+ });
112
+ });
113
+ //# sourceMappingURL=query-perf.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-perf.test.js","sourceRoot":"","sources":["../src/query-perf.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { IndexStats } from "@vivantel/virage-core";
2
+ import type { QdrantClient } from "@qdrant/js-client-rest";
3
+ export declare function getIndexStats(client: QdrantClient, collection: string): Promise<IndexStats>;
4
+ //# sourceMappingURL=stats.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.d.ts","sourceRoot":"","sources":["../src/stats.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAI3D,wBAAsB,aAAa,CACjC,MAAM,EAAE,YAAY,EACpB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC,CA8CrB"}
package/dist/stats.js ADDED
@@ -0,0 +1,75 @@
1
+ export async function getIndexStats(client, collection) {
2
+ const info = await client.getCollection(collection);
3
+ const suggestions = [];
4
+ const totalVectors = info.points_count ?? 0;
5
+ const segmentsCount = info.segments_count ?? 0;
6
+ // Qdrant always uses HNSW internally
7
+ const indexType = "hnsw";
8
+ if (info.status === "red") {
9
+ suggestions.push(`Collection status is RED — check Qdrant logs for errors in "${collection}"`);
10
+ }
11
+ else if (info.status === "yellow") {
12
+ suggestions.push(`Collection status is YELLOW — indexing may be in progress for "${collection}"`);
13
+ }
14
+ if (segmentsCount > 20) {
15
+ suggestions.push(`High segment count (${segmentsCount}) — consider running collection optimization to merge segments`);
16
+ }
17
+ const annRecallAt10 = await computeAnnRecall(client, collection, info, totalVectors);
18
+ if (suggestions.length === 0) {
19
+ suggestions.push("Collection looks healthy");
20
+ }
21
+ return {
22
+ totalVectors,
23
+ indexType,
24
+ annRecallAt10,
25
+ // Not applicable for Qdrant (no vacuum/dead-tuple concept)
26
+ indexAgeHours: 0,
27
+ deadTupleFraction: 0,
28
+ suggestions,
29
+ };
30
+ }
31
+ function extractVectorSize(info) {
32
+ const vectors = info.config?.params?.vectors;
33
+ if (!vectors)
34
+ return undefined;
35
+ // Single-vector mode: { size: number, distance: ... }
36
+ if ("size" in vectors &&
37
+ typeof vectors.size === "number") {
38
+ return vectors.size;
39
+ }
40
+ // Named-vector mode: { default: { size: number, ... }, ... }
41
+ const firstNamed = Object.values(vectors)[0];
42
+ return typeof firstNamed?.size === "number" ? firstNamed.size : undefined;
43
+ }
44
+ async function computeAnnRecall(client, collection, info, totalVectors) {
45
+ if (totalVectors < 10)
46
+ return -1;
47
+ const size = extractVectorSize(info);
48
+ if (!size)
49
+ return -1;
50
+ try {
51
+ const zeroVector = Array(size).fill(0);
52
+ const [exactResults, annResults] = await Promise.all([
53
+ client.search(collection, {
54
+ vector: zeroVector,
55
+ limit: 10,
56
+ params: { exact: true },
57
+ with_payload: false,
58
+ }),
59
+ client.search(collection, {
60
+ vector: zeroVector,
61
+ limit: 10,
62
+ params: { exact: false },
63
+ with_payload: false,
64
+ }),
65
+ ]);
66
+ const exactIds = new Set(exactResults.map((r) => String(r.id)));
67
+ const annIds = annResults.map((r) => String(r.id));
68
+ const hits = annIds.filter((id) => exactIds.has(id)).length;
69
+ return hits / Math.max(exactIds.size, 1);
70
+ }
71
+ catch {
72
+ return -1;
73
+ }
74
+ }
75
+ //# sourceMappingURL=stats.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.js","sourceRoot":"","sources":["../src/stats.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stats.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.test.d.ts","sourceRoot":"","sources":["../src/stats.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,151 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { getIndexStats } from "./stats.js";
3
+ function makeCollectionInfo(overrides = {}) {
4
+ return {
5
+ points_count: 100,
6
+ segments_count: 3,
7
+ status: "green",
8
+ config: {
9
+ params: {
10
+ vectors: {
11
+ size: 4,
12
+ distance: "Cosine",
13
+ },
14
+ shard_number: 1,
15
+ replication_factor: 1,
16
+ write_consistency_factor: 1,
17
+ on_disk_payload: false,
18
+ },
19
+ optimizer_config: {},
20
+ wal_config: {},
21
+ quantization_config: null,
22
+ },
23
+ ...overrides,
24
+ };
25
+ }
26
+ function makeClient(info, searchResults = [[], []]) {
27
+ return {
28
+ getCollection: vi.fn().mockResolvedValue(info),
29
+ search: vi
30
+ .fn()
31
+ .mockResolvedValueOnce(searchResults[0] ?? [])
32
+ .mockResolvedValueOnce(searchResults[1] ?? []),
33
+ };
34
+ }
35
+ describe("getIndexStats (Qdrant)", () => {
36
+ describe("basic fields", () => {
37
+ it("always returns hnsw indexType", async () => {
38
+ const client = makeClient(makeCollectionInfo());
39
+ const stats = await getIndexStats(client, "documents");
40
+ expect(stats.indexType).toBe("hnsw");
41
+ });
42
+ it("reflects points_count as totalVectors", async () => {
43
+ const client = makeClient(makeCollectionInfo({ points_count: 4200 }));
44
+ const stats = await getIndexStats(client, "documents");
45
+ expect(stats.totalVectors).toBe(4200);
46
+ });
47
+ it("totalVectors defaults to 0 when points_count is undefined", async () => {
48
+ const client = makeClient(makeCollectionInfo({ points_count: undefined }));
49
+ const stats = await getIndexStats(client, "documents");
50
+ expect(stats.totalVectors).toBe(0);
51
+ });
52
+ it("always returns indexAgeHours = 0 and deadTupleFraction = 0", async () => {
53
+ const client = makeClient(makeCollectionInfo());
54
+ const stats = await getIndexStats(client, "documents");
55
+ expect(stats.indexAgeHours).toBe(0);
56
+ expect(stats.deadTupleFraction).toBe(0);
57
+ });
58
+ });
59
+ describe("status warnings", () => {
60
+ it("includes RED alert in suggestions when status is red", async () => {
61
+ const client = makeClient(makeCollectionInfo({ status: "red" }));
62
+ const stats = await getIndexStats(client, "documents");
63
+ expect(stats.suggestions.some((s) => /RED/i.test(s))).toBe(true);
64
+ });
65
+ it("includes YELLOW warning in suggestions when status is yellow", async () => {
66
+ const client = makeClient(makeCollectionInfo({ status: "yellow" }));
67
+ const stats = await getIndexStats(client, "documents");
68
+ expect(stats.suggestions.some((s) => /YELLOW/i.test(s))).toBe(true);
69
+ });
70
+ it("returns healthy suggestion when status is green and segments < 20", async () => {
71
+ const client = makeClient(makeCollectionInfo({ status: "green", segments_count: 5 }));
72
+ const stats = await getIndexStats(client, "documents");
73
+ expect(stats.suggestions[0]).toMatch(/healthy/i);
74
+ });
75
+ });
76
+ describe("segment count", () => {
77
+ it("suggests optimization when segments_count > 20", async () => {
78
+ const client = makeClient(makeCollectionInfo({ segments_count: 25 }));
79
+ const stats = await getIndexStats(client, "documents");
80
+ expect(stats.suggestions.some((s) => /segment/i.test(s))).toBe(true);
81
+ });
82
+ it("does not warn about segments when count is exactly 20", async () => {
83
+ const client = makeClient(makeCollectionInfo({ segments_count: 20 }));
84
+ const stats = await getIndexStats(client, "documents");
85
+ expect(stats.suggestions.every((s) => !/segment/i.test(s))).toBe(true);
86
+ });
87
+ });
88
+ describe("ANN recall", () => {
89
+ it("is -1 when totalVectors < 10", async () => {
90
+ const client = makeClient(makeCollectionInfo({ points_count: 5 }));
91
+ const stats = await getIndexStats(client, "documents");
92
+ expect(stats.annRecallAt10).toBe(-1);
93
+ });
94
+ it("is -1 when vector size cannot be determined (no config)", async () => {
95
+ const info = makeCollectionInfo({ points_count: 100 });
96
+ info.config = null;
97
+ const client = makeClient(info);
98
+ const stats = await getIndexStats(client, "documents");
99
+ expect(stats.annRecallAt10).toBe(-1);
100
+ });
101
+ it("is 1.0 when exact and ANN results are identical (single-vector mode)", async () => {
102
+ const ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
103
+ const resultSet = ids.map((id) => ({ id, score: 0.9 }));
104
+ const client = makeClient(makeCollectionInfo({ points_count: 100 }), [
105
+ resultSet,
106
+ resultSet,
107
+ ]);
108
+ const stats = await getIndexStats(client, "documents");
109
+ expect(stats.annRecallAt10).toBe(1);
110
+ });
111
+ it("computes partial recall correctly", async () => {
112
+ const exactResults = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((id) => ({
113
+ id,
114
+ score: 0.9,
115
+ }));
116
+ const annResults = [1, 2, 3, 4, 5, 11, 12, 13, 14, 15].map((id) => ({
117
+ id,
118
+ score: 0.85,
119
+ }));
120
+ const client = makeClient(makeCollectionInfo({ points_count: 100 }), [
121
+ exactResults,
122
+ annResults,
123
+ ]);
124
+ const stats = await getIndexStats(client, "documents");
125
+ expect(stats.annRecallAt10).toBeCloseTo(0.5, 5);
126
+ });
127
+ it("supports named-vector mode for vector size extraction", async () => {
128
+ const info = makeCollectionInfo({ points_count: 100 });
129
+ // Override vectors to use named-vector mode
130
+ info.config.params.vectors = {
131
+ default: { size: 4, distance: "Cosine" },
132
+ };
133
+ const ids = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
134
+ const resultSet = ids.map((id) => ({ id, score: 0.9 }));
135
+ const client = makeClient(info, [resultSet, resultSet]);
136
+ const stats = await getIndexStats(client, "documents");
137
+ expect(stats.annRecallAt10).toBe(1);
138
+ });
139
+ it("is -1 when search() throws", async () => {
140
+ const client = {
141
+ getCollection: vi
142
+ .fn()
143
+ .mockResolvedValue(makeCollectionInfo({ points_count: 100 })),
144
+ search: vi.fn().mockRejectedValue(new Error("collection not found")),
145
+ };
146
+ const stats = await getIndexStats(client, "documents");
147
+ expect(stats.annRecallAt10).toBe(-1);
148
+ });
149
+ });
150
+ });
151
+ //# sourceMappingURL=stats.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stats.test.js","sourceRoot":"","sources":["../src/stats.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import type { VectorStore, VectorDocument, VectorSearchResult, IndexStats, QueryPerfReport, Logger } from "@vivantel/virage-core";
2
+ export interface QdrantVectorStoreOptions {
3
+ /**
4
+ * Qdrant instance URL. e.g. "http://localhost:6333" or "https://xyz.qdrant.io".
5
+ * Required unless `path` is provided.
6
+ */
7
+ url?: string;
8
+ /**
9
+ * Local storage directory for Qdrant data (file mode).
10
+ * When provided, connects to `http://localhost:<port>` — you must start Qdrant
11
+ * pointing at this directory, e.g.:
12
+ * docker run -v <path>:/qdrant/storage -p 6333:6333 qdrant/qdrant
13
+ * Mutually exclusive with `url`.
14
+ */
15
+ path?: string;
16
+ /** Port used when `path` mode is active. Defaults to 6333. */
17
+ port?: number;
18
+ /** API key — required for Qdrant Cloud, omit for local instances. */
19
+ apiKey?: string;
20
+ /** Collection name. Defaults to "documents". */
21
+ collection?: string;
22
+ /** Vector dimensions — must match your embedder. Defaults to 1536. */
23
+ dimensions?: number;
24
+ }
25
+ export declare class QdrantVectorStore implements VectorStore {
26
+ readonly name = "qdrant";
27
+ private readonly client;
28
+ private readonly collection;
29
+ private readonly dimensions;
30
+ private readonly url;
31
+ private logger;
32
+ constructor(options: QdrantVectorStoreOptions);
33
+ setLogger(logger: Logger): void;
34
+ initialize(): Promise<void>;
35
+ upsert(documents: VectorDocument[]): Promise<void>;
36
+ deleteBySourceFile(sourceFiles: string[]): Promise<void>;
37
+ getCurrentState(): Promise<Map<string, string>>;
38
+ search(queryEmbedding: number[], topK: number): Promise<VectorSearchResult[]>;
39
+ getIndexStats(): Promise<IndexStats>;
40
+ getQueryPerfReport(timeframeHours: number): Promise<QueryPerfReport>;
41
+ }
42
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,UAAU,EACV,eAAe,EACf,MAAM,EACP,MAAM,uBAAuB,CAAC;AAK/B,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAKD,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAuB;gBAEzB,OAAO,EAAE,wBAAwB;IAe7C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIzB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB3B,MAAM,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BlD,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAaxD,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAoC/C,MAAM,CACV,cAAc,EAAE,MAAM,EAAE,EACxB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,kBAAkB,EAAE,CAAC;IAwB1B,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC;IAIpC,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAG3E"}
package/dist/store.js ADDED
@@ -0,0 +1,129 @@
1
+ import { QdrantClient } from "@qdrant/js-client-rest";
2
+ import { getIndexStats } from "./stats.js";
3
+ import { getQueryPerfReport } from "./query-perf.js";
4
+ const UPSERT_BATCH_SIZE = 100;
5
+ const SCROLL_PAGE_SIZE = 250;
6
+ export class QdrantVectorStore {
7
+ name = "qdrant";
8
+ client;
9
+ collection;
10
+ dimensions;
11
+ url;
12
+ logger = null;
13
+ constructor(options) {
14
+ if (!options.url && !options.path) {
15
+ throw new Error("QdrantVectorStore: either url or path is required");
16
+ }
17
+ const resolvedUrl = options.url ?? `http://localhost:${options.port ?? 6333}`;
18
+ this.url = resolvedUrl;
19
+ this.collection = options.collection ?? "documents";
20
+ this.dimensions = options.dimensions ?? 1536;
21
+ this.client = new QdrantClient({
22
+ url: resolvedUrl,
23
+ apiKey: options.apiKey,
24
+ });
25
+ }
26
+ setLogger(logger) {
27
+ this.logger = logger.withTag("qdrant");
28
+ }
29
+ async initialize() {
30
+ this.logger?.info(`Connecting to qdrant at ${this.url}, collection: ${this.collection}`);
31
+ const { exists } = await this.client.collectionExists(this.collection);
32
+ if (!exists) {
33
+ await this.client.createCollection(this.collection, {
34
+ vectors: { size: this.dimensions, distance: "Cosine" },
35
+ });
36
+ this.logger?.debug(`Created collection "${this.collection}" (${this.dimensions}d, cosine)`);
37
+ }
38
+ else {
39
+ this.logger?.debug(`Collection "${this.collection}" already exists`);
40
+ }
41
+ }
42
+ async upsert(documents) {
43
+ this.logger?.verbose(`Upserting ${documents.length} docs into "${this.collection}"`);
44
+ for (let i = 0; i < documents.length; i += UPSERT_BATCH_SIZE) {
45
+ const batch = documents.slice(i, i + UPSERT_BATCH_SIZE);
46
+ const points = batch.map((doc) => ({
47
+ id: crypto.randomUUID(),
48
+ vector: doc.embedding,
49
+ payload: {
50
+ content: doc.content,
51
+ metadata: doc.metadata,
52
+ source_file: doc.sourceFile,
53
+ commit_hash: doc.commitHash,
54
+ content_hash: doc.contentHash,
55
+ },
56
+ }));
57
+ await this.client.upsert(this.collection, {
58
+ wait: true,
59
+ points,
60
+ });
61
+ this.logger?.trace(` Upserted IDs: ${points.map((p) => p.id.slice(0, 8)).join(", ")}`);
62
+ }
63
+ }
64
+ async deleteBySourceFile(sourceFiles) {
65
+ if (sourceFiles.length === 0)
66
+ return;
67
+ this.logger?.verbose(`Deleting docs for ${sourceFiles.length} source file(s)`);
68
+ await this.client.delete(this.collection, {
69
+ wait: true,
70
+ filter: {
71
+ must: [{ key: "source_file", match: { any: sourceFiles } }],
72
+ },
73
+ });
74
+ }
75
+ async getCurrentState() {
76
+ const state = new Map();
77
+ let offset;
78
+ do {
79
+ const response = await this.client.scroll(this.collection, {
80
+ with_payload: ["source_file", "commit_hash"],
81
+ with_vector: false,
82
+ limit: SCROLL_PAGE_SIZE,
83
+ offset,
84
+ });
85
+ for (const point of response.points) {
86
+ const payload = point.payload;
87
+ const sourceFile = typeof payload?.source_file === "string" ? payload.source_file : null;
88
+ const commitHash = typeof payload?.commit_hash === "string" ? payload.commit_hash : null;
89
+ if (sourceFile && commitHash) {
90
+ state.set(sourceFile, commitHash);
91
+ }
92
+ }
93
+ const raw = response.next_page_offset;
94
+ // ExtendedPointId (string | number) means more pages; anything else stops iteration
95
+ offset =
96
+ typeof raw === "string" || typeof raw === "number" ? raw : undefined;
97
+ } while (offset !== undefined);
98
+ this.logger?.verbose(`getCurrentState: ${state.size} source version(s)`);
99
+ return state;
100
+ }
101
+ async search(queryEmbedding, topK) {
102
+ this.logger?.debug(`Search: topK=${topK}`);
103
+ const results = await this.client.search(this.collection, {
104
+ vector: queryEmbedding,
105
+ limit: topK,
106
+ with_payload: true,
107
+ });
108
+ return results.map((r) => {
109
+ const payload = r.payload;
110
+ return {
111
+ id: String(r.id),
112
+ content: typeof payload?.content === "string" ? payload.content : "",
113
+ metadata: payload?.metadata &&
114
+ typeof payload.metadata === "object" &&
115
+ !Array.isArray(payload.metadata)
116
+ ? payload.metadata
117
+ : {},
118
+ similarity: r.score,
119
+ };
120
+ });
121
+ }
122
+ async getIndexStats() {
123
+ return getIndexStats(this.client, this.collection);
124
+ }
125
+ async getQueryPerfReport(timeframeHours) {
126
+ return getQueryPerfReport(this.url, this.collection, timeframeHours);
127
+ }
128
+ }
129
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@vivantel/virage-store-qdrant",
3
+ "version": "0.2.0",
4
+ "description": "Qdrant vector store for @vivantel/rag-core (local and cloud)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "sideEffects": false,
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "type-check": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "prepublishOnly": "npm run build",
27
+ "lint": "eslint src/",
28
+ "lint:fix": "eslint src/ --fix",
29
+ "format": "prettier --write \"src/**/*.ts\"",
30
+ "fix": "npm run lint:fix && npm run format"
31
+ },
32
+ "keywords": [
33
+ "rag",
34
+ "vector-store",
35
+ "qdrant",
36
+ "vector-database",
37
+ "semantic-search"
38
+ ],
39
+ "author": "Vivantel",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/vivantel/virage",
44
+ "directory": "packages/virage-store-qdrant"
45
+ },
46
+ "dependencies": {
47
+ "@qdrant/js-client-rest": "^1.18.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@vivantel/virage-core": "*"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^25.9.1",
54
+ "@vitest/coverage-v8": "^4.1.8",
55
+ "@vivantel/virage-core": "0.2.0",
56
+ "typescript": "^6.0.3",
57
+ "vitest": "^4.1.8"
58
+ },
59
+ "engines": {
60
+ "node": ">=18.0.0"
61
+ }
62
+ }