@mastra/rag 0.0.2-alpha.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.
@@ -0,0 +1,119 @@
1
+ // To setup a Qdrant server, run:
2
+ // docker run -p 6333:6333 qdrant/qdrant
3
+ import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
4
+
5
+ import { QdrantVector } from './index';
6
+
7
+ describe('Qdrant Integration Tests', () => {
8
+ let qdrant: QdrantVector;
9
+ const testCollectionName = 'test-collection-' + Date.now();
10
+ const dimension = 3;
11
+
12
+ beforeAll(async () => {
13
+ qdrant = new QdrantVector('http://localhost:6333/');
14
+ await qdrant.createIndex(testCollectionName, dimension);
15
+ });
16
+
17
+ afterAll(async () => {
18
+ await qdrant.deleteIndex(testCollectionName);
19
+ }, 50000);
20
+
21
+ describe('Index Operations', () => {
22
+ it('should list collections including ours', async () => {
23
+ const indexes = await qdrant.listIndexes();
24
+ expect(indexes).toContain(testCollectionName);
25
+ }, 50000);
26
+
27
+ it('should describe index with correct properties', async () => {
28
+ const stats = await qdrant.describeIndex(testCollectionName);
29
+ expect(stats.dimension).toBe(dimension);
30
+ expect(stats.metric).toBe('cosine');
31
+ expect(typeof stats.count).toBe('number');
32
+ }, 50000);
33
+ });
34
+
35
+ describe('Vector Operations', () => {
36
+ const testVectors = [
37
+ [1.0, 0.0, 0.0],
38
+ [0.0, 1.0, 0.0],
39
+ [0.0, 0.0, 1.0],
40
+ ];
41
+ const testMetadata = [{ label: 'x-axis' }, { label: 'y-axis' }, { label: 'z-axis' }];
42
+ let vectorIds: string[];
43
+
44
+ it('should upsert vectors with metadata', async () => {
45
+ vectorIds = await qdrant.upsert(testCollectionName, testVectors, testMetadata);
46
+ expect(vectorIds).toHaveLength(3);
47
+ }, 50000);
48
+
49
+ it('should query vectors and return nearest neighbors', async () => {
50
+ const queryVector = [1.0, 0.1, 0.1];
51
+ const results = await qdrant.query(testCollectionName, queryVector, 3);
52
+
53
+ expect(results).toHaveLength(3);
54
+ expect(results?.[0]?.score).toBeGreaterThan(0);
55
+ expect(results?.[0]?.metadata).toBeDefined();
56
+ }, 50000);
57
+
58
+ it('should query vectors with metadata filter', async () => {
59
+ const queryVector = [0.0, 1.0, 0.0];
60
+ const filter = {
61
+ must: [{ key: 'label', match: { value: 'y-axis' } }],
62
+ };
63
+
64
+ const results = await qdrant.query(testCollectionName, queryVector, 1, filter);
65
+
66
+ expect(results).toHaveLength(1);
67
+ expect(results?.[0]?.metadata?.label).toBe('y-axis');
68
+ }, 50000);
69
+ });
70
+
71
+ describe('Error Handling', () => {
72
+ it('should handle non-existent index query gracefully', async () => {
73
+ const nonExistentIndex = 'non-existent-index';
74
+ await expect(qdrant.query(nonExistentIndex, [1, 0, 0])).rejects.toThrow();
75
+ }, 50000);
76
+
77
+ it('should handle incorrect dimension vectors', async () => {
78
+ const wrongDimVector = [[1, 0]]; // 2D vector for 3D index
79
+ await expect(qdrant.upsert(testCollectionName, wrongDimVector)).rejects.toThrow();
80
+ }, 50000);
81
+ });
82
+
83
+ describe('Performance Tests', () => {
84
+ it('should handle batch upsert of 1000 vectors', async () => {
85
+ const batchSize = 1000;
86
+ const vectors = Array(batchSize)
87
+ .fill(null)
88
+ .map(() =>
89
+ Array(dimension)
90
+ .fill(null)
91
+ .map(() => Math.random()),
92
+ );
93
+ const metadata = vectors.map((_, i) => ({ id: i }));
94
+
95
+ const start = Date.now();
96
+ const ids = await qdrant.upsert(testCollectionName, vectors, metadata);
97
+ const duration = Date.now() - start;
98
+
99
+ expect(ids).toHaveLength(batchSize);
100
+ console.log(`Batch upsert of ${batchSize} vectors took ${duration}ms`);
101
+ }, 300000);
102
+
103
+ it('should perform multiple concurrent queries', async () => {
104
+ const queryVector = [1, 0, 0];
105
+ const numQueries = 10;
106
+
107
+ const start = Date.now();
108
+ const promises = Array(numQueries)
109
+ .fill(null)
110
+ .map(() => qdrant.query(testCollectionName, queryVector));
111
+
112
+ const results = await Promise.all(promises);
113
+ const duration = Date.now() - start;
114
+
115
+ expect(results).toHaveLength(numQueries);
116
+ console.log(`${numQueries} concurrent queries took ${duration}ms`);
117
+ }, 50000);
118
+ });
119
+ });
@@ -0,0 +1,116 @@
1
+ import { MastraVector, QueryResult, IndexStats } from '@mastra/core';
2
+ import { QdrantClient, Schemas } from '@qdrant/js-client-rest';
3
+
4
+ const BATCH_SIZE = 256;
5
+ const DISTANCE_MAPPING: Record<string, Schemas['Distance']> = {
6
+ cosine: 'Cosine',
7
+ euclidean: 'Euclid',
8
+ dotproduct: 'Dot',
9
+ };
10
+
11
+ export class QdrantVector extends MastraVector {
12
+ private client: QdrantClient;
13
+
14
+ constructor(url: string, apiKey?: string, https?: boolean) {
15
+ super();
16
+
17
+ const baseClient = new QdrantClient({
18
+ url,
19
+ apiKey,
20
+ https,
21
+ });
22
+
23
+ const telemetry = this.__getTelemetry();
24
+ this.client =
25
+ telemetry?.traceClass(baseClient, {
26
+ spanNamePrefix: 'qdrant-vector',
27
+ attributes: {
28
+ 'vector.type': 'qdrant',
29
+ },
30
+ }) ?? baseClient;
31
+ }
32
+
33
+ async upsert(
34
+ indexName: string,
35
+ vectors: number[][],
36
+ metadata?: Record<string, any>[],
37
+ ids?: string[],
38
+ ): Promise<string[]> {
39
+ const pointIds = ids || vectors.map(() => crypto.randomUUID());
40
+
41
+ const records = vectors.map((vector, i) => ({
42
+ id: pointIds[i],
43
+ vector: vector,
44
+ payload: metadata?.[i] || {},
45
+ }));
46
+
47
+ for (let i = 0; i < records.length; i += BATCH_SIZE) {
48
+ const batch = records.slice(i, i + BATCH_SIZE);
49
+ await this.client.upsert(indexName, {
50
+ // @ts-expect-error
51
+ points: batch,
52
+ wait: true,
53
+ });
54
+ }
55
+
56
+ return pointIds;
57
+ }
58
+
59
+ async createIndex(
60
+ indexName: string,
61
+ dimension: number,
62
+ metric: 'cosine' | 'euclidean' | 'dotproduct' = 'cosine',
63
+ ): Promise<void> {
64
+ await this.client.createCollection(indexName, {
65
+ vectors: {
66
+ // @ts-expect-error
67
+ size: dimension,
68
+ // @ts-expect-error
69
+ distance: DISTANCE_MAPPING[metric],
70
+ },
71
+ });
72
+ }
73
+
74
+ async query(
75
+ indexName: string,
76
+ queryVector: number[],
77
+ topK: number = 10,
78
+ filter?: Record<string, any>,
79
+ ): Promise<QueryResult[]> {
80
+ const results = (
81
+ await this.client.query(indexName, {
82
+ query: queryVector,
83
+ limit: topK,
84
+ filter: filter,
85
+ with_payload: true,
86
+ })
87
+ ).points;
88
+
89
+ return results.map(match => ({
90
+ id: match.id as string,
91
+ score: match.score || 0,
92
+ metadata: match.payload as Record<string, any>,
93
+ }));
94
+ }
95
+
96
+ async listIndexes(): Promise<string[]> {
97
+ const response = await this.client.getCollections();
98
+ return response.collections.map(collection => collection.name) || [];
99
+ }
100
+
101
+ async describeIndex(indexName: string): Promise<IndexStats> {
102
+ const { config, points_count } = await this.client.getCollection(indexName);
103
+
104
+ const distance = config.params.vectors?.distance as Schemas['Distance'];
105
+ return {
106
+ dimension: config.params.vectors?.size as number,
107
+ count: points_count || 0,
108
+ // @ts-expect-error
109
+ metric: Object.keys(DISTANCE_MAPPING).find(key => DISTANCE_MAPPING[key] === distance),
110
+ };
111
+ }
112
+
113
+ async deleteIndex(indexName: string): Promise<void> {
114
+ await this.client.deleteCollection(indexName);
115
+ }
116
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "moduleResolution": "bundler",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src"
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules", "**/*.test.ts"]
10
+ }