@mastra/rag 0.0.0-commonjs-20250227130920

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.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +23 -0
  2. package/CHANGELOG.md +1151 -0
  3. package/LICENSE +44 -0
  4. package/README.md +26 -0
  5. package/dist/_tsup-dts-rollup.d.cts +620 -0
  6. package/dist/_tsup-dts-rollup.d.ts +620 -0
  7. package/dist/index.cjs +2524 -0
  8. package/dist/index.d.cts +22 -0
  9. package/dist/index.d.ts +22 -0
  10. package/dist/index.js +2505 -0
  11. package/docker-compose.yaml +22 -0
  12. package/eslint.config.js +6 -0
  13. package/package.json +72 -0
  14. package/src/document/document.test.ts +985 -0
  15. package/src/document/document.ts +281 -0
  16. package/src/document/index.ts +2 -0
  17. package/src/document/transformers/character.ts +279 -0
  18. package/src/document/transformers/html.ts +346 -0
  19. package/src/document/transformers/json.ts +265 -0
  20. package/src/document/transformers/latex.ts +19 -0
  21. package/src/document/transformers/markdown.ts +244 -0
  22. package/src/document/transformers/text.ts +134 -0
  23. package/src/document/transformers/token.ts +148 -0
  24. package/src/document/transformers/transformer.ts +5 -0
  25. package/src/document/types.ts +103 -0
  26. package/src/graph-rag/index.test.ts +235 -0
  27. package/src/graph-rag/index.ts +306 -0
  28. package/src/index.ts +6 -0
  29. package/src/rerank/index.test.ts +150 -0
  30. package/src/rerank/index.ts +154 -0
  31. package/src/tools/document-chunker.ts +30 -0
  32. package/src/tools/graph-rag.ts +117 -0
  33. package/src/tools/index.ts +3 -0
  34. package/src/tools/vector-query.ts +90 -0
  35. package/src/utils/default-settings.ts +33 -0
  36. package/src/utils/index.ts +2 -0
  37. package/src/utils/vector-prompts.ts +729 -0
  38. package/src/utils/vector-search.ts +41 -0
  39. package/tsconfig.json +5 -0
  40. package/vitest.config.ts +11 -0
@@ -0,0 +1,235 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ import type { GraphChunk, GraphEdge, GraphEmbedding, GraphNode } from './';
4
+ import { GraphRAG } from './';
5
+
6
+ describe('GraphRAG', () => {
7
+ beforeEach(() => {
8
+ vi.clearAllMocks(); // Clear any mock state before each test
9
+ });
10
+
11
+ describe('addNode', () => {
12
+ it('should throw an error if node does not have an embedding', () => {
13
+ const graph = new GraphRAG();
14
+ const node = {
15
+ id: '1',
16
+ content: 'Node 1',
17
+ };
18
+ expect(() => graph.addNode(node)).toThrow('Node must have an embedding');
19
+ });
20
+
21
+ it('should throw an error if node embedding dimension is not equal to the graph dimension', () => {
22
+ const graph = new GraphRAG(2);
23
+ const node: GraphNode = {
24
+ id: '1',
25
+ content: 'Node 1',
26
+ embedding: [1, 2, 3],
27
+ };
28
+ expect(() => graph.addNode(node)).toThrow('Embedding dimension must be 2');
29
+ });
30
+
31
+ it('should add a node to the graph', () => {
32
+ const graph = new GraphRAG(3);
33
+ const node = {
34
+ id: '1',
35
+ content: 'Node 1',
36
+ embedding: [1, 2, 3],
37
+ };
38
+ graph.addNode(node);
39
+ expect(graph['nodes'].size).toBe(1);
40
+ });
41
+ });
42
+
43
+ describe('addEdge', () => {
44
+ it('should throw an error if either source or target node does not exist', () => {
45
+ const graph = new GraphRAG();
46
+ const edge: GraphEdge = {
47
+ source: '1',
48
+ target: '2',
49
+ weight: 0.5,
50
+ type: 'semantic',
51
+ };
52
+ expect(() => graph.addEdge(edge)).toThrow('Both source and target nodes must exist');
53
+ });
54
+
55
+ it('should add an edge between two nodes', () => {
56
+ const graph = new GraphRAG(3);
57
+ const node1: GraphNode = {
58
+ id: '1',
59
+ content: 'Node 1',
60
+ embedding: [1, 2, 3],
61
+ };
62
+ const node2: GraphNode = {
63
+ id: '2',
64
+ content: 'Node 2',
65
+ embedding: [4, 5, 6],
66
+ };
67
+ graph.addNode(node1);
68
+ graph.addNode(node2);
69
+ const edge: GraphEdge = {
70
+ source: '1',
71
+ target: '2',
72
+ weight: 0.5,
73
+ type: 'semantic',
74
+ };
75
+ graph.addEdge(edge);
76
+ expect(graph['edges'].length).toBe(2);
77
+ });
78
+ });
79
+
80
+ describe('createGraph', () => {
81
+ it("chunks and embeddings can't be empty", () => {
82
+ const graph = new GraphRAG(3);
83
+ const chunks: GraphChunk[] = [];
84
+ const embeddings: GraphEmbedding[] = [];
85
+ expect(() => graph.createGraph(chunks, embeddings)).toThrowError(
86
+ 'Chunks and embeddings arrays must not be empty',
87
+ );
88
+ });
89
+ it('chunks and embeddings must have the same length', () => {
90
+ const graph = new GraphRAG(3);
91
+ const chunks: GraphChunk[] = [
92
+ {
93
+ text: 'Chunk 1',
94
+ metadata: {},
95
+ },
96
+ {
97
+ text: 'Chunk 2',
98
+ metadata: {},
99
+ },
100
+ ];
101
+ const embeddings: GraphEmbedding[] = [
102
+ {
103
+ vector: [1, 2, 3],
104
+ },
105
+ ];
106
+ expect(() => graph.createGraph(chunks, embeddings)).toThrowError(
107
+ 'Chunks and embeddings must have the same length',
108
+ );
109
+ });
110
+ it('should return the top ranked nodes', () => {
111
+ const results = [
112
+ {
113
+ metadata: {
114
+ text: 'Chunk 1',
115
+ },
116
+ vector: [1, 2, 3],
117
+ },
118
+ {
119
+ metadata: {
120
+ text: 'Chunk 2',
121
+ },
122
+ vector: [4, 5, 6],
123
+ },
124
+ {
125
+ metadata: {
126
+ text: 'Chunk 3',
127
+ },
128
+ vector: [7, 8, 9],
129
+ },
130
+ ];
131
+
132
+ const chunks = results.map(result => ({
133
+ text: result?.metadata?.text,
134
+ metadata: result.metadata,
135
+ }));
136
+ const embeddings = results.map(result => ({
137
+ vector: result.vector,
138
+ }));
139
+
140
+ const graph = new GraphRAG(3);
141
+ graph.createGraph(chunks, embeddings);
142
+
143
+ const nodes = graph.getNodes();
144
+ expect(nodes.length).toBe(3);
145
+ expect(nodes[0]?.id).toBe('0');
146
+ expect(nodes[1]?.id).toBe('1');
147
+ expect(nodes[2]?.id).toBe('2');
148
+
149
+ const edges = graph.getEdges();
150
+ expect(edges.length).toBe(6);
151
+ });
152
+ });
153
+
154
+ describe('query', () => {
155
+ it("query embedding can't be empty", () => {
156
+ const graph = new GraphRAG(3);
157
+ const queryEmbedding: number[] = [];
158
+ expect(() => graph.query({ query: queryEmbedding, topK: 2, randomWalkSteps: 3, restartProb: 0.1 })).toThrowError(
159
+ `Query embedding must have dimension ${3}`,
160
+ );
161
+ });
162
+
163
+ it('topK must be greater than 0', () => {
164
+ const graph = new GraphRAG(3);
165
+ const queryEmbedding = [1, 2, 3];
166
+ const topK = 0;
167
+ expect(() => graph.query({ query: queryEmbedding, topK, randomWalkSteps: 3, restartProb: 0.1 })).toThrowError(
168
+ 'TopK must be greater than 0',
169
+ );
170
+ });
171
+
172
+ it('randomWalkSteps must be greater than 0', () => {
173
+ const graph = new GraphRAG(3);
174
+ const queryEmbedding = [1, 2, 3];
175
+ const topK = 2;
176
+ const randomWalkSteps = 0;
177
+ expect(() => graph.query({ query: queryEmbedding, topK, randomWalkSteps, restartProb: 0.1 })).toThrowError(
178
+ 'Random walk steps must be greater than 0',
179
+ );
180
+ });
181
+
182
+ it('restartProb must be between 0 and 1', () => {
183
+ const graph = new GraphRAG(3);
184
+ const queryEmbedding = [1, 2, 3];
185
+ const topK = 2;
186
+ const randomWalkSteps = 3;
187
+ const restartProb = -0.1;
188
+ expect(() => graph.query({ query: queryEmbedding, topK, randomWalkSteps, restartProb })).toThrowError(
189
+ 'Restart probability must be between 0 and 1',
190
+ );
191
+ });
192
+
193
+ it('should return the top ranked nodes', () => {
194
+ const graph = new GraphRAG(3);
195
+ const node1: GraphNode = {
196
+ id: '1',
197
+ content: 'Node 1',
198
+ embedding: [1, 2, 3],
199
+ };
200
+ const node2: GraphNode = {
201
+ id: '2',
202
+ content: 'Node 2',
203
+ embedding: [11, 12, 13],
204
+ };
205
+ const node3: GraphNode = {
206
+ id: '3',
207
+ content: 'Node 3',
208
+ embedding: [21, 22, 23],
209
+ };
210
+ graph.addNode(node1);
211
+ graph.addNode(node2);
212
+ graph.addNode(node3);
213
+ graph.addEdge({
214
+ source: '1',
215
+ target: '2',
216
+ weight: 0.5,
217
+ type: 'semantic',
218
+ });
219
+ graph.addEdge({
220
+ source: '2',
221
+ target: '3',
222
+ weight: 0.7,
223
+ type: 'semantic',
224
+ });
225
+
226
+ const queryEmbedding = [15, 16, 17];
227
+ const topK = 2;
228
+ const randomWalkSteps = 3;
229
+ const restartProb = 0.1;
230
+ const rerankedResults = graph.query({ query: queryEmbedding, topK, randomWalkSteps, restartProb });
231
+
232
+ expect(rerankedResults.length).toBe(2);
233
+ });
234
+ });
235
+ });
@@ -0,0 +1,306 @@
1
+ /**
2
+ * TODO: GraphRAG Enhancements
3
+ * - Add support for more edge types (sequential, hierarchical, citation, etc)
4
+ * - Allow for custom edge types
5
+ * - Utilize metadata for richer connections
6
+ * - Improve graph traversal and querying using types
7
+ */
8
+
9
+ type SupportedEdgeType = 'semantic';
10
+
11
+ // Types for graph nodes and edges
12
+ export interface GraphNode {
13
+ id: string;
14
+ content: string;
15
+ embedding?: number[];
16
+ metadata?: Record<string, any>;
17
+ }
18
+
19
+ interface RankedNode extends GraphNode {
20
+ score: number;
21
+ }
22
+
23
+ export interface GraphEdge {
24
+ source: string;
25
+ target: string;
26
+ weight: number;
27
+ type: SupportedEdgeType;
28
+ }
29
+
30
+ export interface GraphChunk {
31
+ text: string;
32
+ metadata: Record<string, any>;
33
+ }
34
+
35
+ export interface GraphEmbedding {
36
+ vector: number[];
37
+ }
38
+
39
+ export class GraphRAG {
40
+ private nodes: Map<string, GraphNode>;
41
+ private edges: GraphEdge[];
42
+ private dimension: number;
43
+ private threshold: number;
44
+
45
+ constructor(dimension: number = 1536, threshold: number = 0.7) {
46
+ this.nodes = new Map();
47
+ this.edges = [];
48
+ this.dimension = dimension;
49
+ this.threshold = threshold;
50
+ }
51
+
52
+ // Add a node to the graph
53
+ addNode(node: GraphNode): void {
54
+ if (!node.embedding) {
55
+ throw new Error('Node must have an embedding');
56
+ }
57
+ if (node.embedding.length !== this.dimension) {
58
+ throw new Error(`Embedding dimension must be ${this.dimension}`);
59
+ }
60
+ this.nodes.set(node.id, node);
61
+ }
62
+
63
+ // Add an edge between two nodes
64
+ addEdge(edge: GraphEdge): void {
65
+ if (!this.nodes.has(edge.source) || !this.nodes.has(edge.target)) {
66
+ throw new Error('Both source and target nodes must exist');
67
+ }
68
+ this.edges.push(edge);
69
+ // Add reverse edge
70
+ this.edges.push({
71
+ source: edge.target,
72
+ target: edge.source,
73
+ weight: edge.weight,
74
+ type: edge.type,
75
+ });
76
+ }
77
+
78
+ // Helper method to get all nodes
79
+ getNodes(): GraphNode[] {
80
+ return Array.from(this.nodes.values());
81
+ }
82
+
83
+ // Helper method to get all edges
84
+ getEdges(): GraphEdge[] {
85
+ return this.edges;
86
+ }
87
+
88
+ getEdgesByType(type: string): GraphEdge[] {
89
+ return this.edges.filter(edge => edge.type === type);
90
+ }
91
+
92
+ clear(): void {
93
+ this.nodes.clear();
94
+ this.edges = [];
95
+ }
96
+
97
+ updateNodeContent(id: string, newContent: string): void {
98
+ const node = this.nodes.get(id);
99
+ if (!node) {
100
+ throw new Error(`Node ${id} not found`);
101
+ }
102
+ node.content = newContent;
103
+ }
104
+
105
+ // Get neighbors of a node
106
+ private getNeighbors(nodeId: string, edgeType?: string): { id: string; weight: number }[] {
107
+ return this.edges
108
+ .filter(edge => edge.source === nodeId && (!edgeType || edge.type === edgeType))
109
+ .map(edge => ({
110
+ id: edge.target,
111
+ weight: edge.weight,
112
+ }))
113
+ .filter(node => node !== undefined);
114
+ }
115
+
116
+ // Calculate cosine similarity between two vectors
117
+ private cosineSimilarity(vec1: number[], vec2: number[]): number {
118
+ if (!vec1 || !vec2) {
119
+ throw new Error('Vectors must not be null or undefined');
120
+ }
121
+ const vectorLength = vec1.length;
122
+
123
+ if (vectorLength !== vec2.length) {
124
+ throw new Error(`Vector dimensions must match: vec1(${vec1.length}) !== vec2(${vec2.length})`);
125
+ }
126
+
127
+ let dotProduct = 0;
128
+ let normVec1 = 0;
129
+ let normVec2 = 0;
130
+
131
+ for (let i = 0; i < vectorLength; i++) {
132
+ const a = vec1[i]!; // Non-null assertion operator
133
+ const b = vec2[i]!;
134
+
135
+ dotProduct += a * b;
136
+ normVec1 += a * a;
137
+ normVec2 += b * b;
138
+ }
139
+ const magnitudeProduct = Math.sqrt(normVec1 * normVec2);
140
+
141
+ if (magnitudeProduct === 0) {
142
+ return 0;
143
+ }
144
+
145
+ const similarity = dotProduct / magnitudeProduct;
146
+ return Math.max(-1, Math.min(1, similarity));
147
+ }
148
+
149
+ createGraph(chunks: GraphChunk[], embeddings: GraphEmbedding[]) {
150
+ if (!chunks?.length || !embeddings?.length) {
151
+ throw new Error('Chunks and embeddings arrays must not be empty');
152
+ }
153
+ if (chunks.length !== embeddings.length) {
154
+ throw new Error('Chunks and embeddings must have the same length');
155
+ }
156
+ // Create nodes from chunks
157
+ chunks.forEach((chunk, index) => {
158
+ const node: GraphNode = {
159
+ id: index.toString(),
160
+ content: chunk.text,
161
+ embedding: embeddings[index]?.vector,
162
+ metadata: { ...chunk.metadata },
163
+ };
164
+ this.addNode(node);
165
+ this.nodes.set(node.id, node);
166
+ });
167
+
168
+ // Create edges based on cosine similarity
169
+ for (let i = 0; i < chunks.length; i++) {
170
+ const firstEmbedding = embeddings[i]?.vector as number[];
171
+ for (let j = i + 1; j < chunks.length; j++) {
172
+ const secondEmbedding = embeddings[j]?.vector as number[];
173
+ const similarity = this.cosineSimilarity(firstEmbedding, secondEmbedding);
174
+
175
+ // Only create edges if similarity is above threshold
176
+ if (similarity > this.threshold) {
177
+ this.addEdge({
178
+ source: i.toString(),
179
+ target: j.toString(),
180
+ weight: similarity,
181
+ type: 'semantic',
182
+ });
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ private selectWeightedNeighbor(neighbors: Array<{ id: string; weight: number }>): string {
189
+ // Sum all weights to normalize probabilities
190
+ const totalWeight = neighbors.reduce((sum, n) => sum + n.weight, 0);
191
+
192
+ // Pick a random point in the total weight range
193
+ let remainingWeight = Math.random() * totalWeight;
194
+
195
+ // Subtract each weight from our random value until we go below 0
196
+ // Higher weights will make us go below 0 more often, making them more likely to be selected
197
+ for (const neighbor of neighbors) {
198
+ remainingWeight -= neighbor.weight;
199
+ if (remainingWeight <= 0) {
200
+ return neighbor.id;
201
+ }
202
+ }
203
+
204
+ return neighbors[neighbors.length - 1]?.id as string;
205
+ }
206
+
207
+ // Perform random walk with restart
208
+ private randomWalkWithRestart(startNodeId: string, steps: number, restartProb: number): Map<string, number> {
209
+ const visits = new Map<string, number>();
210
+ let currentNodeId = startNodeId;
211
+
212
+ for (let step = 0; step < steps; step++) {
213
+ // Record visit
214
+ visits.set(currentNodeId, (visits.get(currentNodeId) || 0) + 1);
215
+
216
+ // Decide whether to restart
217
+ if (Math.random() < restartProb) {
218
+ currentNodeId = startNodeId;
219
+ continue;
220
+ }
221
+
222
+ // Get neighbors
223
+ const neighbors = this.getNeighbors(currentNodeId);
224
+ if (neighbors.length === 0) {
225
+ currentNodeId = startNodeId;
226
+ continue;
227
+ }
228
+
229
+ // Select random weighted neighbor and set as current node
230
+ currentNodeId = this.selectWeightedNeighbor(neighbors);
231
+ }
232
+
233
+ // Normalize visits
234
+ const totalVisits = Array.from(visits.values()).reduce((a, b) => a + b, 0);
235
+ const normalizedVisits = new Map<string, number>();
236
+ for (const [nodeId, count] of visits) {
237
+ normalizedVisits.set(nodeId, count / totalVisits);
238
+ }
239
+
240
+ return normalizedVisits;
241
+ }
242
+
243
+ // Retrieve relevant nodes using hybrid approach
244
+ query({
245
+ query,
246
+ topK = 10,
247
+ randomWalkSteps = 100,
248
+ restartProb = 0.15,
249
+ }: {
250
+ query: number[];
251
+ topK?: number;
252
+ randomWalkSteps?: number;
253
+ restartProb?: number;
254
+ }): RankedNode[] {
255
+ if (!query || query.length !== this.dimension) {
256
+ throw new Error(`Query embedding must have dimension ${this.dimension}`);
257
+ }
258
+ if (topK < 1) {
259
+ throw new Error('TopK must be greater than 0');
260
+ }
261
+ if (randomWalkSteps < 1) {
262
+ throw new Error('Random walk steps must be greater than 0');
263
+ }
264
+ if (restartProb <= 0 || restartProb >= 1) {
265
+ throw new Error('Restart probability must be between 0 and 1');
266
+ }
267
+ // Retrieve nodes and calculate similarity
268
+ const similarities = Array.from(this.nodes.values()).map(node => ({
269
+ node,
270
+ similarity: this.cosineSimilarity(query, node.embedding!),
271
+ }));
272
+
273
+ // Sort by similarity
274
+ similarities.sort((a, b) => b.similarity - a.similarity);
275
+ const topNodes = similarities.slice(0, topK);
276
+
277
+ // Re-ranks nodes using random walk with restart
278
+ const rerankedNodes = new Map<string, { node: GraphNode; score: number }>();
279
+
280
+ // For each top node, perform random walk
281
+ for (const { node, similarity } of topNodes) {
282
+ const walkScores = this.randomWalkWithRestart(node.id, randomWalkSteps, restartProb);
283
+
284
+ // Combine dense retrieval score with graph score
285
+ for (const [nodeId, walkScore] of walkScores) {
286
+ const node = this.nodes.get(nodeId)!;
287
+ const existingScore = rerankedNodes.get(nodeId)?.score || 0;
288
+ rerankedNodes.set(nodeId, {
289
+ node,
290
+ score: existingScore + similarity * walkScore,
291
+ });
292
+ }
293
+ }
294
+
295
+ // Sort by final score and return top K nodes
296
+ return Array.from(rerankedNodes.values())
297
+ .sort((a, b) => b.score - a.score)
298
+ .slice(0, topK)
299
+ .map(item => ({
300
+ id: item.node.id,
301
+ content: item.node.content,
302
+ metadata: item.node.metadata,
303
+ score: item.score,
304
+ }));
305
+ }
306
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './document/document';
2
+ export * from './rerank';
3
+ export { GraphRAG } from './graph-rag';
4
+ export * from './tools';
5
+ export * from './utils/vector-prompts';
6
+ export * from './utils/default-settings';
@@ -0,0 +1,150 @@
1
+ import { cohere } from '@ai-sdk/cohere';
2
+ import { CohereRelevanceScorer } from '@mastra/core/relevance';
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+
5
+ import { rerank } from '.';
6
+
7
+ vi.spyOn(CohereRelevanceScorer.prototype, 'getRelevanceScore').mockImplementation(async () => {
8
+ return 1;
9
+ });
10
+
11
+ const getScoreSpreads = (results1: any, results2: any) => {
12
+ const scoreSpread1 = Math.max(...results1.map((r: any) => r.score)) - Math.min(...results1.map((r: any) => r.score));
13
+ const scoreSpread2 = Math.max(...results2.map((r: any) => r.score)) - Math.min(...results2.map((r: any) => r.score));
14
+ return { scoreSpread1, scoreSpread2 };
15
+ };
16
+
17
+ describe('rerank', () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ it('should throw an error if weights do not add up to 1', async () => {
23
+ const results = [
24
+ { id: '1', metadata: { text: 'Test result 1' }, score: 0.5 },
25
+ { id: '2', metadata: { text: 'Test result 2' }, score: 0.4 },
26
+ { id: '3', metadata: { text: 'Test result 3' }, score: 0.9 },
27
+ ];
28
+ await expect(
29
+ rerank(results, 'test query', cohere('rerank-v3.5'), { weights: { semantic: 0.5, vector: 0.3, position: 0.5 } }),
30
+ ).rejects.toThrow('Weights must add up to 1');
31
+ });
32
+
33
+ it('should rerank results with default weights', async () => {
34
+ const results = [
35
+ { id: '1', metadata: { text: 'Test result 1' }, score: 0.5 },
36
+ { id: '2', metadata: { text: 'Test result 2' }, score: 0.4 },
37
+ { id: '3', metadata: { text: 'Test result 3' }, score: 0.9 },
38
+ ];
39
+
40
+ const rerankedResults = await rerank(results, 'test query', cohere('rerank-v3.5'), { topK: 2 });
41
+
42
+ expect(rerankedResults).toHaveLength(2);
43
+ expect(rerankedResults[0]).toStrictEqual({
44
+ result: { id: '3', metadata: { text: 'Test result 3' }, score: 0.9 },
45
+ score: 0.8266666666666667,
46
+ details: {
47
+ semantic: 1,
48
+ vector: 0.9,
49
+ position: 0.33333333333333337,
50
+ },
51
+ });
52
+ expect(rerankedResults[1]).toStrictEqual({
53
+ result: { id: '1', metadata: { text: 'Test result 1' }, score: 0.5 },
54
+ score: 0.8,
55
+ details: {
56
+ semantic: 1,
57
+ vector: 0.5,
58
+ position: 1,
59
+ },
60
+ });
61
+
62
+ const { scoreSpread1, scoreSpread2 } = getScoreSpreads(results, rerankedResults);
63
+ expect(scoreSpread1).toBe(0.5);
64
+ expect(scoreSpread2).toBe(0.026666666666666616);
65
+ });
66
+
67
+ it('should rerank results with custom weights', async () => {
68
+ const results = [
69
+ { id: '1', metadata: { text: 'Test result 1' }, score: 0.5 },
70
+ { id: '2', metadata: { text: 'Test result 2' }, score: 0.4 },
71
+ { id: '3', metadata: { text: 'Test result 3' }, score: 0.9 },
72
+ ];
73
+
74
+ const rerankedResults = await rerank(results, 'test query', cohere('rerank-v3.5'), {
75
+ weights: {
76
+ semantic: 0.5,
77
+ vector: 0.4,
78
+ position: 0.1,
79
+ },
80
+ topK: 2,
81
+ });
82
+
83
+ expect(rerankedResults).toHaveLength(2);
84
+ expect(rerankedResults[0]).toStrictEqual({
85
+ result: { id: '3', metadata: { text: 'Test result 3' }, score: 0.9 },
86
+ score: 0.8933333333333334,
87
+ details: {
88
+ semantic: 1,
89
+ vector: 0.9,
90
+ position: 0.33333333333333337,
91
+ },
92
+ });
93
+ expect(rerankedResults[1]).toStrictEqual({
94
+ result: { id: '1', metadata: { text: 'Test result 1' }, score: 0.5 },
95
+ score: 0.7999999999999999,
96
+ details: {
97
+ semantic: 1,
98
+ vector: 0.5,
99
+ position: 1,
100
+ },
101
+ });
102
+ const { scoreSpread1, scoreSpread2 } = getScoreSpreads(results, rerankedResults);
103
+ expect(scoreSpread1).toBe(0.5);
104
+ expect(scoreSpread2).toBe(0.09333333333333349);
105
+ });
106
+
107
+ it('should handle query embedding when provided', async () => {
108
+ const results = [
109
+ { id: '1', metadata: { text: 'Test result 1' }, score: 0.8 },
110
+ { id: '2', metadata: { text: 'Test result 2' }, score: 0.6 },
111
+ ];
112
+
113
+ const rerankedResults = await rerank(results, 'test query', cohere('rerank-v3.5'), {
114
+ queryEmbedding: [0.5, 0.3, -0.2, 0.4],
115
+ topK: 2,
116
+ });
117
+
118
+ // Ensure query embedding analysis is being applied (we don't know exact score without knowing internals)
119
+ expect(rerankedResults).toHaveLength(2);
120
+ expect(rerankedResults[0]).toStrictEqual({
121
+ result: { id: '1', metadata: { text: 'Test result 1' }, score: 0.8 },
122
+ score: 0.9200000000000002,
123
+ details: {
124
+ semantic: 1,
125
+ vector: 0.8,
126
+ position: 1,
127
+ queryAnalysis: {
128
+ magnitude: 0.7348469228349535,
129
+ dominantFeatures: [0, 3, 1, 2],
130
+ },
131
+ },
132
+ });
133
+ expect(rerankedResults[1]).toStrictEqual({
134
+ result: { id: '2', metadata: { text: 'Test result 2' }, score: 0.6 },
135
+ score: 0.74,
136
+ details: {
137
+ semantic: 1,
138
+ vector: 0.6,
139
+ position: 0.5,
140
+ queryAnalysis: {
141
+ magnitude: 0.7348469228349535,
142
+ dominantFeatures: [0, 3, 1, 2],
143
+ },
144
+ },
145
+ });
146
+ const { scoreSpread1, scoreSpread2 } = getScoreSpreads(results, rerankedResults);
147
+ expect(scoreSpread1).toBe(0.20000000000000007);
148
+ expect(scoreSpread2).toBe(0.18000000000000016);
149
+ });
150
+ });