@rws-framework/ai-tools 2.2.1 → 3.1.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/docs/tutorial-style-rag.md +124 -0
- package/examples/test-recursive-chunker.ts +167 -0
- package/examples/tutorial-style-rag.ts +153 -0
- package/package.json +6 -4
- package/src/index.ts +25 -4
- package/src/models/convo/EmbedLoader.ts +111 -29
- package/src/models/convo/VectorStore.ts +82 -4
- package/src/models/prompts/_prompt.ts +2 -2
- package/src/models/prompts/inc/execution-methods-handler.ts +2 -2
- package/src/models/prompts/inc/input-output-manager.ts +9 -7
- package/src/services/LangChainEmbeddingService.ts +222 -0
- package/src/services/LangChainRAGService.ts +395 -0
- package/src/services/LangChainVectorStoreService.ts +378 -0
- package/src/services/OptimizedVectorSearchService.ts +324 -0
- package/src/services/TextChunker.ts +319 -0
- package/src/types/IPrompt.ts +3 -1
- package/src/types/embedding.types.ts +15 -0
- package/src/types/index.ts +5 -0
- package/src/types/rag.types.ts +44 -0
- package/src/types/search.types.ts +56 -0
- package/src/types/vectorstore.types.ts +23 -0
- package/src/services/VectorStoreService.ts +0 -15
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { Injectable } from '@rws-framework/server/nest';
|
|
2
|
+
import { Document } from '@langchain/core/documents';
|
|
3
|
+
import { LangChainEmbeddingService } from './LangChainEmbeddingService';
|
|
4
|
+
import RWSVectorStore, { VectorDocType, IVectorStoreConfig as RWSVectorStoreConfig } from '../models/convo/VectorStore';
|
|
5
|
+
|
|
6
|
+
export interface ISearchResult {
|
|
7
|
+
content: string;
|
|
8
|
+
score: number;
|
|
9
|
+
metadata: any;
|
|
10
|
+
chunkId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IVectorStoreConfig {
|
|
14
|
+
type: 'memory' | 'faiss';
|
|
15
|
+
maxResults?: number;
|
|
16
|
+
autoSave?: boolean;
|
|
17
|
+
similarityThreshold?: number;
|
|
18
|
+
persistPath?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IDocumentChunk {
|
|
22
|
+
id: string;
|
|
23
|
+
content: string;
|
|
24
|
+
embedding?: number[];
|
|
25
|
+
metadata?: {
|
|
26
|
+
documentId: string;
|
|
27
|
+
chunkIndex: number;
|
|
28
|
+
source?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
knowledgeId?: string;
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IVectorSearchRequest {
|
|
36
|
+
query: string;
|
|
37
|
+
maxResults?: number;
|
|
38
|
+
similarityThreshold?: number;
|
|
39
|
+
filter?: {
|
|
40
|
+
knowledgeIds?: string[];
|
|
41
|
+
documentIds?: string[];
|
|
42
|
+
[key: string]: any;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface IVectorSearchResponse {
|
|
47
|
+
results: ISearchResult[];
|
|
48
|
+
totalResults: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* LangChain-based vector store service that provides document storage and similarity search
|
|
53
|
+
* Now uses RWSVectorStore for unified vector storage like the LangChain tutorial
|
|
54
|
+
*/
|
|
55
|
+
@Injectable()
|
|
56
|
+
export class LangChainVectorStoreService {
|
|
57
|
+
private vectorStore: RWSVectorStore;
|
|
58
|
+
private documents: Map<string, Document> = new Map();
|
|
59
|
+
private config: IVectorStoreConfig;
|
|
60
|
+
private isInitialized = false;
|
|
61
|
+
private documentCount = 0;
|
|
62
|
+
private embeddingService: LangChainEmbeddingService;
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
// Empty constructor for NestJS dependency injection
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Initialize the service with configuration
|
|
70
|
+
*/
|
|
71
|
+
async initialize(embeddingService: LangChainEmbeddingService, config: IVectorStoreConfig): Promise<void> {
|
|
72
|
+
if (this.isInitialized) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.embeddingService = embeddingService;
|
|
77
|
+
this.config = {
|
|
78
|
+
type: 'memory',
|
|
79
|
+
maxResults: 10,
|
|
80
|
+
similarityThreshold: 0.1, // Use lower threshold like we configured
|
|
81
|
+
...config,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const embeddings = this.embeddingService.getEmbeddingsInstance();
|
|
85
|
+
|
|
86
|
+
// Initialize with empty documents first, then create RWSVectorStore
|
|
87
|
+
const initialDocs: VectorDocType = [];
|
|
88
|
+
this.vectorStore = await new RWSVectorStore(
|
|
89
|
+
initialDocs,
|
|
90
|
+
embeddings,
|
|
91
|
+
{
|
|
92
|
+
type: this.config.type,
|
|
93
|
+
persistPath: this.config.persistPath
|
|
94
|
+
}
|
|
95
|
+
).init();
|
|
96
|
+
|
|
97
|
+
console.log(`Created new ${this.config.type} vector store using RWSVectorStore`);
|
|
98
|
+
this.isInitialized = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Add documents to the vector store
|
|
103
|
+
*/
|
|
104
|
+
async addDocuments(documents: Document[]): Promise<string[]> {
|
|
105
|
+
this.ensureInitialized();
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const ids = await this.vectorStore.addDocuments(documents);
|
|
109
|
+
const docIds: string[] = [];
|
|
110
|
+
|
|
111
|
+
// Store documents in our map for retrieval
|
|
112
|
+
documents.forEach((doc, index) => {
|
|
113
|
+
const id = `doc_${Date.now()}_${index}`;
|
|
114
|
+
this.documents.set(id, doc);
|
|
115
|
+
docIds.push(id);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.documentCount += documents.length;
|
|
119
|
+
return docIds;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('Failed to add documents to vector store:', error);
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Index a single document (split into chunks if needed)
|
|
128
|
+
*/
|
|
129
|
+
async indexDocument(
|
|
130
|
+
content: string,
|
|
131
|
+
documentId: string,
|
|
132
|
+
metadata: Record<string, any> = {}
|
|
133
|
+
): Promise<{ success: boolean; chunkIds: string[]; error?: string }> {
|
|
134
|
+
try {
|
|
135
|
+
this.ensureInitialized();
|
|
136
|
+
|
|
137
|
+
// Remove existing chunks for this document
|
|
138
|
+
await this.deleteDocument(documentId);
|
|
139
|
+
|
|
140
|
+
// Create document chunks
|
|
141
|
+
const documents = await this.embeddingService.createDocuments(content, {
|
|
142
|
+
documentId,
|
|
143
|
+
...metadata,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Add to vector store
|
|
147
|
+
const chunkIds = await this.addDocuments(documents);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
chunkIds,
|
|
152
|
+
};
|
|
153
|
+
} catch (error: any) {
|
|
154
|
+
console.error(`Failed to index document ${documentId}:`, error);
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
chunkIds: [],
|
|
158
|
+
error: error?.message || 'Unknown error',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Search for similar documents using RWSVectorStore like the LangChain tutorial
|
|
165
|
+
*/
|
|
166
|
+
async searchSimilar(request: IVectorSearchRequest): Promise<IVectorSearchResponse> {
|
|
167
|
+
this.ensureInitialized();
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const {
|
|
171
|
+
query,
|
|
172
|
+
maxResults = this.config.maxResults,
|
|
173
|
+
similarityThreshold = this.config.similarityThreshold,
|
|
174
|
+
filter,
|
|
175
|
+
} = request;
|
|
176
|
+
|
|
177
|
+
// Use RWSVectorStore's similaritySearchWithScore method like the tutorial
|
|
178
|
+
const searchResults = await this.vectorStore.similaritySearchWithScore(
|
|
179
|
+
query,
|
|
180
|
+
maxResults || 10
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Filter by similarity threshold and metadata filters
|
|
184
|
+
let filteredResults = searchResults
|
|
185
|
+
.filter(([_doc, score]: [any, any]) => score >= (similarityThreshold || 0.1));
|
|
186
|
+
|
|
187
|
+
// Apply knowledge/document ID filters if provided
|
|
188
|
+
if (filter) {
|
|
189
|
+
filteredResults = filteredResults.filter(([doc, _score]: [any, any]) => {
|
|
190
|
+
// Check knowledge IDs
|
|
191
|
+
if (filter.knowledgeIds && filter.knowledgeIds.length > 0) {
|
|
192
|
+
const docKnowledgeId = doc.metadata?.knowledgeId;
|
|
193
|
+
if (!docKnowledgeId || !filter.knowledgeIds.includes(docKnowledgeId)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check document IDs
|
|
199
|
+
if (filter.documentIds && filter.documentIds.length > 0) {
|
|
200
|
+
const docId = doc.metadata?.documentId;
|
|
201
|
+
if (!docId || !filter.documentIds.includes(docId)) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return true;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Format results to match our interface
|
|
211
|
+
const results: ISearchResult[] = filteredResults
|
|
212
|
+
.map(([doc, score]: [any, any], index: number) => ({
|
|
213
|
+
content: doc.pageContent,
|
|
214
|
+
score,
|
|
215
|
+
metadata: doc.metadata,
|
|
216
|
+
chunkId: doc.metadata?.id || `chunk_${index}`,
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
results,
|
|
221
|
+
totalResults: results.length,
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('Failed to search similar documents:', error);
|
|
225
|
+
return {
|
|
226
|
+
results: [],
|
|
227
|
+
totalResults: 0,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Delete a document and all its chunks
|
|
234
|
+
*/
|
|
235
|
+
async deleteDocument(documentId: string): Promise<boolean> {
|
|
236
|
+
try {
|
|
237
|
+
await this.ensureInitialized();
|
|
238
|
+
|
|
239
|
+
// Find all chunks for this document
|
|
240
|
+
const docsToDelete: string[] = [];
|
|
241
|
+
for (const [id, doc] of this.documents.entries()) {
|
|
242
|
+
if (doc.metadata?.documentId === documentId) {
|
|
243
|
+
docsToDelete.push(id);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (docsToDelete.length > 0) {
|
|
248
|
+
// Use RWSVectorStore's deleteDocuments method
|
|
249
|
+
await this.vectorStore.deleteDocuments(docsToDelete);
|
|
250
|
+
|
|
251
|
+
// Remove from our document map
|
|
252
|
+
docsToDelete.forEach(id => this.documents.delete(id));
|
|
253
|
+
|
|
254
|
+
if (this.config.autoSave) {
|
|
255
|
+
await this.save();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return true;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error(`Failed to delete document ${documentId}:`, error);
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get documents by filter
|
|
268
|
+
*/
|
|
269
|
+
async getDocuments(filter?: Record<string, any>): Promise<Document[]> {
|
|
270
|
+
const docs: Document[] = [];
|
|
271
|
+
|
|
272
|
+
for (const doc of this.documents.values()) {
|
|
273
|
+
if (!filter) {
|
|
274
|
+
docs.push(doc);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let matches = true;
|
|
279
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
280
|
+
if (doc.metadata?.[key] !== value) {
|
|
281
|
+
matches = false;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (matches) {
|
|
287
|
+
docs.push(doc);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return docs;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Save the vector store to disk (not needed for memory store)
|
|
296
|
+
*/
|
|
297
|
+
async save(): Promise<boolean> {
|
|
298
|
+
return true; // Memory store doesn't need saving
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get statistics about the vector store
|
|
303
|
+
*/
|
|
304
|
+
getStats(): { totalChunks: number; totalDocuments: number } {
|
|
305
|
+
const documentIds = new Set<string>();
|
|
306
|
+
|
|
307
|
+
for (const doc of this.documents.values()) {
|
|
308
|
+
if (doc.metadata?.documentId) {
|
|
309
|
+
documentIds.add(doc.metadata.documentId);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
totalChunks: this.documentCount,
|
|
315
|
+
totalDocuments: documentIds.size,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Clear all documents from the vector store
|
|
321
|
+
*/
|
|
322
|
+
async clear(): Promise<boolean> {
|
|
323
|
+
try {
|
|
324
|
+
await this.ensureInitialized();
|
|
325
|
+
|
|
326
|
+
// Recreate RWSVectorStore with empty documents
|
|
327
|
+
const embeddings = this.embeddingService.getEmbeddingsInstance();
|
|
328
|
+
const initialDocs: VectorDocType = [];
|
|
329
|
+
this.vectorStore = await new RWSVectorStore(
|
|
330
|
+
initialDocs,
|
|
331
|
+
embeddings,
|
|
332
|
+
{
|
|
333
|
+
type: this.config.type,
|
|
334
|
+
persistPath: this.config.persistPath
|
|
335
|
+
}
|
|
336
|
+
).init();
|
|
337
|
+
|
|
338
|
+
// Clear our document map
|
|
339
|
+
this.documents.clear();
|
|
340
|
+
this.documentCount = 0;
|
|
341
|
+
|
|
342
|
+
return true;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
console.error('Failed to clear vector store:', error);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Ensure the vector store is initialized
|
|
351
|
+
*/
|
|
352
|
+
private ensureInitialized(): void {
|
|
353
|
+
if (!this.isInitialized) {
|
|
354
|
+
throw new Error('LangChainVectorStoreService not initialized. Call initialize() first.');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get the underlying RWSVectorStore instance
|
|
360
|
+
*/
|
|
361
|
+
getVectorStore(): RWSVectorStore {
|
|
362
|
+
return this.vectorStore;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get the underlying LangChain vector store (memory or faiss)
|
|
367
|
+
*/
|
|
368
|
+
getLangChainVectorStore() {
|
|
369
|
+
return this.vectorStore.getVectorStore();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Update configuration
|
|
374
|
+
*/
|
|
375
|
+
updateConfig(newConfig: Partial<IVectorStoreConfig>): void {
|
|
376
|
+
this.config = { ...this.config, ...newConfig };
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { LangChainEmbeddingService } from './LangChainEmbeddingService';
|
|
3
|
+
import {
|
|
4
|
+
IOptimizedSearchRequest,
|
|
5
|
+
IOptimizedSearchResult,
|
|
6
|
+
IOptimizedSearchResponse,
|
|
7
|
+
IVectorSearchRequest,
|
|
8
|
+
IVectorSearchResponse,
|
|
9
|
+
ISearchResult
|
|
10
|
+
} from '../types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Optimized vector search service for lightning-fast similarity searches
|
|
14
|
+
* Uses pre-computed embeddings and direct cosine similarity calculations
|
|
15
|
+
*/
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class OptimizedVectorSearchService {
|
|
18
|
+
private queryEmbeddingCache = new Map<string, number[]>();
|
|
19
|
+
private maxCacheSize = 100;
|
|
20
|
+
|
|
21
|
+
constructor(private embeddingService: LangChainEmbeddingService) {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Perform optimized similarity search across pre-computed vectors
|
|
25
|
+
*/
|
|
26
|
+
async searchSimilar(request: IOptimizedSearchRequest): Promise<IOptimizedSearchResponse> {
|
|
27
|
+
const startTime = Date.now();
|
|
28
|
+
const { query, knowledgeVectors, maxResults = 5, threshold = 0.1 } = request;
|
|
29
|
+
|
|
30
|
+
// Get or compute query embedding
|
|
31
|
+
const queryEmbedding = await this.getQueryEmbedding(query);
|
|
32
|
+
|
|
33
|
+
// Collect all candidates with parallel processing
|
|
34
|
+
const allCandidates: IOptimizedSearchResult[] = [];
|
|
35
|
+
let totalCandidates = 0;
|
|
36
|
+
|
|
37
|
+
// Process all knowledge vectors in parallel
|
|
38
|
+
const searchPromises = knowledgeVectors.map(async (knowledgeVector) => {
|
|
39
|
+
const candidates: IOptimizedSearchResult[] = [];
|
|
40
|
+
const similarities: number[] = []; // Track all similarities for debugging
|
|
41
|
+
|
|
42
|
+
for (const chunk of knowledgeVector.chunks) {
|
|
43
|
+
totalCandidates++;
|
|
44
|
+
|
|
45
|
+
if (!chunk.embedding || !Array.isArray(chunk.embedding)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Compute cosine similarity
|
|
50
|
+
const similarity = this.embeddingService.cosineSimilarity(queryEmbedding, chunk.embedding);
|
|
51
|
+
similarities.push(similarity);
|
|
52
|
+
|
|
53
|
+
if (similarity >= threshold) {
|
|
54
|
+
candidates.push({
|
|
55
|
+
content: chunk.content,
|
|
56
|
+
score: similarity,
|
|
57
|
+
metadata: chunk.metadata,
|
|
58
|
+
knowledgeId: knowledgeVector.knowledgeId,
|
|
59
|
+
chunkId: chunk.metadata?.id || `${knowledgeVector.knowledgeId}_chunk_${Date.now()}`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Log similarity statistics for debugging
|
|
65
|
+
if (similarities.length > 0) {
|
|
66
|
+
const maxSim = Math.max(...similarities);
|
|
67
|
+
const avgSim = similarities.reduce((a, b) => a + b, 0) / similarities.length;
|
|
68
|
+
console.log(`[VECTOR SEARCH] Knowledge ${knowledgeVector.knowledgeId}: Max similarity: ${maxSim.toFixed(4)}, Avg: ${avgSim.toFixed(4)}, Candidates above ${threshold}: ${candidates.length}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return candidates;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Wait for all searches to complete
|
|
75
|
+
const allCandidateArrays = await Promise.all(searchPromises);
|
|
76
|
+
|
|
77
|
+
// Flatten results
|
|
78
|
+
for (const candidates of allCandidateArrays) {
|
|
79
|
+
allCandidates.push(...candidates);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Sort by similarity score and take top results
|
|
83
|
+
const results = allCandidates
|
|
84
|
+
.sort((a, b) => b.score - a.score)
|
|
85
|
+
.slice(0, maxResults);
|
|
86
|
+
|
|
87
|
+
const searchTime = Date.now() - startTime;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
results,
|
|
91
|
+
searchTime,
|
|
92
|
+
totalCandidates
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get query embedding with caching
|
|
98
|
+
*/
|
|
99
|
+
private async getQueryEmbedding(query: string): Promise<number[]> {
|
|
100
|
+
// Check cache first
|
|
101
|
+
if (this.queryEmbeddingCache.has(query)) {
|
|
102
|
+
return this.queryEmbeddingCache.get(query)!;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Generate embedding
|
|
106
|
+
const embedding = await this.embeddingService.embedText(query);
|
|
107
|
+
|
|
108
|
+
// Cache the embedding (with size limit)
|
|
109
|
+
if (this.queryEmbeddingCache.size >= this.maxCacheSize) {
|
|
110
|
+
// Remove oldest entry
|
|
111
|
+
const firstKey = this.queryEmbeddingCache.keys().next().value;
|
|
112
|
+
this.queryEmbeddingCache.delete(firstKey);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.queryEmbeddingCache.set(query, embedding);
|
|
116
|
+
return embedding;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Batch search multiple queries efficiently
|
|
121
|
+
*/
|
|
122
|
+
async batchSearch(
|
|
123
|
+
queries: string[],
|
|
124
|
+
knowledgeVectors: Array<{
|
|
125
|
+
knowledgeId: string | number;
|
|
126
|
+
chunks: Array<{
|
|
127
|
+
content: string;
|
|
128
|
+
embedding: number[];
|
|
129
|
+
metadata: any;
|
|
130
|
+
}>;
|
|
131
|
+
}>,
|
|
132
|
+
maxResults = 5,
|
|
133
|
+
threshold = 0.1 // Updated to match other defaults
|
|
134
|
+
): Promise<Map<string, IOptimizedSearchResponse>> {
|
|
135
|
+
const results = new Map<string, IOptimizedSearchResponse>();
|
|
136
|
+
|
|
137
|
+
// Generate embeddings for all queries in batch
|
|
138
|
+
const queryEmbeddings = await this.embeddingService.embedTexts(queries);
|
|
139
|
+
|
|
140
|
+
// Process each query
|
|
141
|
+
for (let i = 0; i < queries.length; i++) {
|
|
142
|
+
const query = queries[i];
|
|
143
|
+
const queryEmbedding = queryEmbeddings[i];
|
|
144
|
+
|
|
145
|
+
// Cache the embedding
|
|
146
|
+
this.queryEmbeddingCache.set(query, queryEmbedding);
|
|
147
|
+
|
|
148
|
+
// Perform search with pre-computed embedding
|
|
149
|
+
const response = await this.searchWithEmbedding({
|
|
150
|
+
queryEmbedding,
|
|
151
|
+
knowledgeVectors,
|
|
152
|
+
maxResults,
|
|
153
|
+
threshold
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
results.set(query, response);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Search with pre-computed query embedding
|
|
164
|
+
*/
|
|
165
|
+
private async searchWithEmbedding(request: {
|
|
166
|
+
queryEmbedding: number[];
|
|
167
|
+
knowledgeVectors: Array<{
|
|
168
|
+
knowledgeId: string | number;
|
|
169
|
+
chunks: Array<{
|
|
170
|
+
content: string;
|
|
171
|
+
embedding: number[];
|
|
172
|
+
metadata: any;
|
|
173
|
+
}>;
|
|
174
|
+
}>;
|
|
175
|
+
maxResults: number;
|
|
176
|
+
threshold: number;
|
|
177
|
+
}): Promise<IOptimizedSearchResponse> {
|
|
178
|
+
const startTime = Date.now();
|
|
179
|
+
const { queryEmbedding, knowledgeVectors, maxResults, threshold } = request;
|
|
180
|
+
|
|
181
|
+
const allCandidates: IOptimizedSearchResult[] = [];
|
|
182
|
+
let totalCandidates = 0;
|
|
183
|
+
|
|
184
|
+
// Process all knowledge vectors in parallel
|
|
185
|
+
const searchPromises = knowledgeVectors.map(async (knowledgeVector) => {
|
|
186
|
+
const candidates: IOptimizedSearchResult[] = [];
|
|
187
|
+
|
|
188
|
+
for (const chunk of knowledgeVector.chunks) {
|
|
189
|
+
totalCandidates++;
|
|
190
|
+
|
|
191
|
+
if (!chunk.embedding || !Array.isArray(chunk.embedding)) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Compute cosine similarity
|
|
196
|
+
const similarity = this.embeddingService.cosineSimilarity(queryEmbedding, chunk.embedding);
|
|
197
|
+
|
|
198
|
+
if (similarity >= threshold) {
|
|
199
|
+
candidates.push({
|
|
200
|
+
content: chunk.content,
|
|
201
|
+
score: similarity,
|
|
202
|
+
metadata: chunk.metadata,
|
|
203
|
+
knowledgeId: knowledgeVector.knowledgeId,
|
|
204
|
+
chunkId: chunk.metadata?.id || `${knowledgeVector.knowledgeId}_chunk_${Date.now()}`
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return candidates;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Wait for all searches to complete
|
|
213
|
+
const allCandidateArrays = await Promise.all(searchPromises);
|
|
214
|
+
|
|
215
|
+
// Flatten results
|
|
216
|
+
for (const candidates of allCandidateArrays) {
|
|
217
|
+
allCandidates.push(...candidates);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Sort by similarity score and take top results
|
|
221
|
+
const results = allCandidates
|
|
222
|
+
.sort((a, b) => b.score - a.score)
|
|
223
|
+
.slice(0, maxResults);
|
|
224
|
+
|
|
225
|
+
const searchTime = Date.now() - startTime;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
results,
|
|
229
|
+
searchTime,
|
|
230
|
+
totalCandidates
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Clear query embedding cache
|
|
236
|
+
*/
|
|
237
|
+
clearCache(): void {
|
|
238
|
+
this.queryEmbeddingCache.clear();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get cache statistics
|
|
243
|
+
*/
|
|
244
|
+
getCacheStats(): { size: number; maxSize: number } {
|
|
245
|
+
return {
|
|
246
|
+
size: this.queryEmbeddingCache.size,
|
|
247
|
+
maxSize: this.maxCacheSize
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Search similar documents (compatibility method from LangChainVectorStoreService)
|
|
253
|
+
*/
|
|
254
|
+
async searchSimilarCompat(request: IVectorSearchRequest, knowledgeVectors: Array<{
|
|
255
|
+
knowledgeId: string | number;
|
|
256
|
+
chunks: Array<{
|
|
257
|
+
content: string;
|
|
258
|
+
embedding: number[];
|
|
259
|
+
metadata: any;
|
|
260
|
+
}>;
|
|
261
|
+
}>): Promise<IVectorSearchResponse> {
|
|
262
|
+
try {
|
|
263
|
+
const {
|
|
264
|
+
query,
|
|
265
|
+
maxResults = 10,
|
|
266
|
+
similarityThreshold = 0.1, // Updated to match other defaults
|
|
267
|
+
filter,
|
|
268
|
+
} = request;
|
|
269
|
+
|
|
270
|
+
// Filter knowledge vectors if needed
|
|
271
|
+
let filteredVectors = knowledgeVectors;
|
|
272
|
+
if (filter) {
|
|
273
|
+
filteredVectors = knowledgeVectors.filter(vector => {
|
|
274
|
+
// Check knowledge IDs
|
|
275
|
+
if (filter.knowledgeIds && filter.knowledgeIds.length > 0) {
|
|
276
|
+
return filter.knowledgeIds.includes(String(vector.knowledgeId));
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Use our optimized search
|
|
283
|
+
const searchResponse = await this.searchSimilar({
|
|
284
|
+
query,
|
|
285
|
+
knowledgeVectors: filteredVectors,
|
|
286
|
+
maxResults,
|
|
287
|
+
threshold: similarityThreshold
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Convert to IVectorSearchResponse format
|
|
291
|
+
const results: ISearchResult[] = searchResponse.results.map(result => ({
|
|
292
|
+
content: result.content,
|
|
293
|
+
score: result.score,
|
|
294
|
+
metadata: result.metadata,
|
|
295
|
+
chunkId: result.chunkId
|
|
296
|
+
}));
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
results,
|
|
300
|
+
totalResults: results.length,
|
|
301
|
+
};
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('Failed to search similar documents:', error);
|
|
304
|
+
return {
|
|
305
|
+
results: [],
|
|
306
|
+
totalResults: 0,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get search statistics
|
|
313
|
+
*/
|
|
314
|
+
getStats(knowledgeVectors: Array<{
|
|
315
|
+
knowledgeId: string | number;
|
|
316
|
+
chunks: Array<{ content: string; embedding: number[]; metadata: any; }>;
|
|
317
|
+
}>): { totalChunks: number; totalKnowledge: number } {
|
|
318
|
+
const totalChunks = knowledgeVectors.reduce((total, vector) => total + vector.chunks.length, 0);
|
|
319
|
+
return {
|
|
320
|
+
totalChunks,
|
|
321
|
+
totalKnowledge: knowledgeVectors.length,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|