@dcyfr/ai-rag 0.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/LICENSE +21 -0
- package/README.md +588 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders/html/index.d.ts +26 -0
- package/dist/loaders/html/index.d.ts.map +1 -0
- package/dist/loaders/html/index.js +106 -0
- package/dist/loaders/html/index.js.map +1 -0
- package/dist/loaders/index.d.ts +8 -0
- package/dist/loaders/index.d.ts.map +1 -0
- package/dist/loaders/index.js +7 -0
- package/dist/loaders/index.js.map +1 -0
- package/dist/loaders/markdown/index.d.ts +33 -0
- package/dist/loaders/markdown/index.d.ts.map +1 -0
- package/dist/loaders/markdown/index.js +150 -0
- package/dist/loaders/markdown/index.js.map +1 -0
- package/dist/loaders/text/index.d.ts +21 -0
- package/dist/loaders/text/index.d.ts.map +1 -0
- package/dist/loaders/text/index.js +78 -0
- package/dist/loaders/text/index.js.map +1 -0
- package/dist/pipeline/embedding/generator.d.ts +24 -0
- package/dist/pipeline/embedding/generator.d.ts.map +1 -0
- package/dist/pipeline/embedding/generator.js +42 -0
- package/dist/pipeline/embedding/generator.js.map +1 -0
- package/dist/pipeline/embedding/index.d.ts +8 -0
- package/dist/pipeline/embedding/index.d.ts.map +1 -0
- package/dist/pipeline/embedding/index.js +6 -0
- package/dist/pipeline/embedding/index.js.map +1 -0
- package/dist/pipeline/embedding/pipeline.d.ts +26 -0
- package/dist/pipeline/embedding/pipeline.d.ts.map +1 -0
- package/dist/pipeline/embedding/pipeline.js +59 -0
- package/dist/pipeline/embedding/pipeline.js.map +1 -0
- package/dist/pipeline/index.d.ts +7 -0
- package/dist/pipeline/index.d.ts.map +1 -0
- package/dist/pipeline/index.js +7 -0
- package/dist/pipeline/index.js.map +1 -0
- package/dist/pipeline/ingestion/index.d.ts +5 -0
- package/dist/pipeline/ingestion/index.d.ts.map +1 -0
- package/dist/pipeline/ingestion/index.js +5 -0
- package/dist/pipeline/ingestion/index.js.map +1 -0
- package/dist/pipeline/ingestion/pipeline.d.ts +27 -0
- package/dist/pipeline/ingestion/pipeline.d.ts.map +1 -0
- package/dist/pipeline/ingestion/pipeline.js +118 -0
- package/dist/pipeline/ingestion/pipeline.js.map +1 -0
- package/dist/pipeline/retrieval/index.d.ts +5 -0
- package/dist/pipeline/retrieval/index.d.ts.map +1 -0
- package/dist/pipeline/retrieval/index.js +5 -0
- package/dist/pipeline/retrieval/index.js.map +1 -0
- package/dist/pipeline/retrieval/pipeline.d.ts +29 -0
- package/dist/pipeline/retrieval/pipeline.d.ts.map +1 -0
- package/dist/pipeline/retrieval/pipeline.js +109 -0
- package/dist/pipeline/retrieval/pipeline.js.map +1 -0
- package/dist/stores/index.d.ts +5 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +5 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/stores/vector/in-memory.d.ts +52 -0
- package/dist/stores/vector/in-memory.d.ts.map +1 -0
- package/dist/stores/vector/in-memory.js +172 -0
- package/dist/stores/vector/in-memory.js.map +1 -0
- package/dist/stores/vector/index.d.ts +6 -0
- package/dist/stores/vector/index.d.ts.map +1 -0
- package/dist/stores/vector/index.js +5 -0
- package/dist/stores/vector/index.js.map +1 -0
- package/dist/types/index.d.ts +259 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/docs/DOCUMENT_LOADERS.md +621 -0
- package/docs/EMBEDDINGS.md +733 -0
- package/docs/PIPELINES.md +771 -0
- package/docs/VECTOR_STORES.md +754 -0
- package/package.json +100 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
# Pipelines - End-to-End RAG Workflows
|
|
2
|
+
|
|
3
|
+
**Target Audience:** Developers building complete RAG systems
|
|
4
|
+
**Prerequisites:** Understanding of loaders, embeddings, vector stores
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Pipelines orchestrate the complete RAG workflow by connecting document loaders, embedding generators, and vector stores into seamless ingestion and retrieval flows.
|
|
11
|
+
|
|
12
|
+
**Key Components:**
|
|
13
|
+
- **Ingestion Pipeline** - Load documents → chunk → embed → store
|
|
14
|
+
- **Retrieval Pipeline** - Query → embed → search → assemble context
|
|
15
|
+
- Progress tracking and error handling
|
|
16
|
+
- Batch processing for efficiency
|
|
17
|
+
- Metadata filtering and ranking
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Ingestion Pipeline
|
|
22
|
+
|
|
23
|
+
### Basic Setup
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import {
|
|
27
|
+
TextLoader,
|
|
28
|
+
OpenAIEmbeddingGenerator,
|
|
29
|
+
Ingest InMemoryVectorStore,
|
|
30
|
+
IngestionPipeline,
|
|
31
|
+
} from '@dcyfr/ai-rag';
|
|
32
|
+
|
|
33
|
+
// 1. Setup components
|
|
34
|
+
const loader = new TextLoader();
|
|
35
|
+
const embedder = new OpenAIEmbeddingGenerator({
|
|
36
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
37
|
+
});
|
|
38
|
+
const store = new InMemoryVectorStore({
|
|
39
|
+
collectionName: 'my-docs',
|
|
40
|
+
embeddingDimensions: 1536,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 2. Create pipeline
|
|
44
|
+
const pipeline = new IngestionPipeline(loader, embedder, store);
|
|
45
|
+
|
|
46
|
+
// 3. Ingest documents
|
|
47
|
+
const result = await pipeline.ingest(['./docs/file1.txt', './docs/file2.txt']);
|
|
48
|
+
|
|
49
|
+
console.log(`Documents processed: ${result.documentsProcessed}`);
|
|
50
|
+
console.log(`Chunks generated: ${result.chunksGenerated}`);
|
|
51
|
+
console.log(`Embeddings created: ${result.embeddingsCreated}`);
|
|
52
|
+
console.log(`Duration: ${result.durationMs}ms`);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Single File Ingestion
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const result = await pipeline.ingestFile('./document.txt', {
|
|
59
|
+
chunkSize: 1000,
|
|
60
|
+
chunkOverlap: 200,
|
|
61
|
+
metadata: {
|
|
62
|
+
category: 'technical',
|
|
63
|
+
priority: 'high',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Batch Ingestion
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { glob } from 'glob';
|
|
72
|
+
|
|
73
|
+
// Find all markdown files
|
|
74
|
+
const files = await glob('./docs/**/*.md');
|
|
75
|
+
|
|
76
|
+
const result = await pipeline.ingest(files, {
|
|
77
|
+
batchSize: 32, // Process 32 documents at a time
|
|
78
|
+
chunkSize: 800,
|
|
79
|
+
chunkOverlap: 150,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log(`Processed ${result.documentsProcessed} files`);
|
|
83
|
+
console.log(`Generated ${result.chunksGenerated} chunks`);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Progress Tracking
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
const result = await pipeline.ingest(files, {
|
|
90
|
+
batchSize: 50,
|
|
91
|
+
onProgress: (current, total, details) => {
|
|
92
|
+
const percent = ((current / total) * 100).toFixed(1);
|
|
93
|
+
console.log(`Progress: ${current}/${total} (${percent}%)`);
|
|
94
|
+
console.log(`Current file: ${details.currentFile}`);
|
|
95
|
+
console.log(`Chunks so far: ${details.chunksProcessed}`);
|
|
96
|
+
console.log(`---`);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Output:**
|
|
102
|
+
```
|
|
103
|
+
Progress: 10/100 (10.0%)
|
|
104
|
+
Current file: ./docs/guide-10.md
|
|
105
|
+
Chunks so far: 147
|
|
106
|
+
---
|
|
107
|
+
Progress: 20/100 (20.0%)
|
|
108
|
+
Current file: ./docs/guide-20.md
|
|
109
|
+
Chunks so far: 289
|
|
110
|
+
---
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Error Handling
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const result = await pipeline.ingest(files, {
|
|
117
|
+
batchSize: 32,
|
|
118
|
+
continueOnError: true, // Don't stop on individual file errors
|
|
119
|
+
onError: (error, file) => {
|
|
120
|
+
console.error(`Failed to process ${file}:`, error.message);
|
|
121
|
+
// Log to monitoring service
|
|
122
|
+
logger.error({ error, file }, 'Ingestion error');
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log(`Successful: ${result.documentsProcessed}`);
|
|
127
|
+
console.log(`Failed: ${result.errors.length}`);
|
|
128
|
+
|
|
129
|
+
// Review errors
|
|
130
|
+
result.errors.forEach(err => {
|
|
131
|
+
console.log(`${err.file}: ${err.error.message}`);
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Custom Metadata
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
await pipeline.ingest(files, {
|
|
139
|
+
metadata: (filePath) => ({
|
|
140
|
+
source: filePath,
|
|
141
|
+
category: filePath.includes('/api/') ? 'api-docs' : 'user-guide',
|
|
142
|
+
ingestedAt: new Date().toISOString(),
|
|
143
|
+
version: '2.0',
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Metadata function receives file path and returns custom metadata
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Retrieval Pipeline
|
|
153
|
+
|
|
154
|
+
### Basic Setup
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import {
|
|
158
|
+
InMemoryVectorStore,
|
|
159
|
+
OpenAIEmbeddingGenerator,
|
|
160
|
+
RetrievalPipeline,
|
|
161
|
+
} from '@dcyfr/ai-rag';
|
|
162
|
+
|
|
163
|
+
const store = new InMemoryVectorStore({
|
|
164
|
+
collectionName: 'my-docs',
|
|
165
|
+
embeddingDimensions: 1536,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const embedder = new OpenAIEmbeddingGenerator({
|
|
169
|
+
apiKey: process.env.OPENAI_API_KEY!,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const pipeline = new RetrievalPipeline(store, embedder);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Semantic Search
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const result = await pipeline.query('What is machine learning?', {
|
|
179
|
+
limit: 5, // Return top 5 results
|
|
180
|
+
threshold: 0.7, // Minimum similarity score
|
|
181
|
+
includeMetadata: true,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Assembled context from top results
|
|
185
|
+
console.log(result.context);
|
|
186
|
+
|
|
187
|
+
// Individual results with scores
|
|
188
|
+
result.results.forEach(r => {
|
|
189
|
+
console.log(`Score: ${r.score.toFixed(3)}`);
|
|
190
|
+
console.log(`Content: ${r.document.content.substring(0, 100)}...`);
|
|
191
|
+
console.log('---');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Query metadata
|
|
195
|
+
console.log(`Query time: ${result.metadata.queryTimeMs}ms`);
|
|
196
|
+
console.log(`Total matches: ${result.metadata.totalMatches}`);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Context Assembly
|
|
200
|
+
|
|
201
|
+
**Strategies:**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// 1. Simple concatenation (default)
|
|
205
|
+
const result = await pipeline.query('search query', {
|
|
206
|
+
contextAssembly: 'concatenate',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Context: "chunk1\n\nchunk2\n\nchunk3..."
|
|
210
|
+
|
|
211
|
+
// 2. Ranked by relevance
|
|
212
|
+
const result = await pipeline.query('search query', {
|
|
213
|
+
contextAssembly: 'ranked',
|
|
214
|
+
includeScores: true,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Context: "[Score: 0.95] chunk1\n\n[Score: 0.87] chunk2..."
|
|
218
|
+
|
|
219
|
+
// 3. Deduplicated
|
|
220
|
+
const result = await pipeline.query('search query', {
|
|
221
|
+
contextAssembly: 'deduplicated',
|
|
222
|
+
similarityThreshold: 0.95, // Merge very similar chunks
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// 4. Custom assembly
|
|
226
|
+
const result = await pipeline.query('search query', {
|
|
227
|
+
contextAssembly: 'custom',
|
|
228
|
+
assembler: (results) => {
|
|
229
|
+
return results
|
|
230
|
+
.map(r => `[Source: ${r.document.metadata.source}]\n${r.document.content}`)
|
|
231
|
+
.join('\n\n---\n\n');
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Metadata Filtering
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
// Filter by category
|
|
240
|
+
const result = await pipeline.query('search query', {
|
|
241
|
+
limit: 10,
|
|
242
|
+
filter: {
|
|
243
|
+
field: 'category',
|
|
244
|
+
operator: 'eq',
|
|
245
|
+
value: 'api-docs',
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Only searches within API documentation
|
|
250
|
+
|
|
251
|
+
// Complex filters
|
|
252
|
+
const result = await pipeline.query('search query', {
|
|
253
|
+
limit: 10,
|
|
254
|
+
filter: {
|
|
255
|
+
operator: 'and',
|
|
256
|
+
filters: [
|
|
257
|
+
{ field: 'category', operator: 'eq', value: 'technical' },
|
|
258
|
+
{ field: 'priority', operator: 'gte', value: 7 },
|
|
259
|
+
{ field: 'tags', operator: 'contains', value: 'ai' },
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Re-ranking
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
// Re-rank results using different embedding model
|
|
269
|
+
const result = await pipeline.query('search query', {
|
|
270
|
+
limit: 10,
|
|
271
|
+
rerank: true,
|
|
272
|
+
rerankModel: 'cohere-rerank-v2',
|
|
273
|
+
rerankTopK: 5, // Re-rank top 5 from initial 10
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Results are semantically re-ordered for better relevance
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Hybrid Search
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// Combine semantic search with keyword search
|
|
283
|
+
const result = await pipeline.query('search query', {
|
|
284
|
+
limit: 10,
|
|
285
|
+
hybrid: true,
|
|
286
|
+
keywordWeight: 0.3, // 30% keyword, 70% semantic
|
|
287
|
+
bm25Params: {
|
|
288
|
+
k1: 1.5,
|
|
289
|
+
b: 0.75,
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Captures both semantic meaning and keyword matches
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Advanced Pipelines
|
|
299
|
+
|
|
300
|
+
### Multi-Source Ingestion
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { IngestionPipeline, TextLoader, MarkdownLoader, HTMLLoader } from '@dcyfr/ai-rag';
|
|
304
|
+
|
|
305
|
+
class MultiSourcePipeline {
|
|
306
|
+
private pipelines: Map<string, IngestionPipeline>;
|
|
307
|
+
|
|
308
|
+
constructor(embedder, store) {
|
|
309
|
+
this.pipelines = new Map([
|
|
310
|
+
['txt', new IngestionPipeline(new TextLoader(), embedder, store)],
|
|
311
|
+
['md', new IngestionPipeline(new MarkdownLoader(), embedder, store)],
|
|
312
|
+
['html', new IngestionPipeline(new HTMLLoader(), embedder, store)],
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async ingest(files: string[]) {
|
|
317
|
+
const byExtension = new Map<string, string[]>();
|
|
318
|
+
|
|
319
|
+
// Group files by extension
|
|
320
|
+
files.forEach(file => {
|
|
321
|
+
const ext = file.split('.').pop() || '';
|
|
322
|
+
const group = byExtension.get(ext) || [];
|
|
323
|
+
group.push(file);
|
|
324
|
+
byExtension.set(ext, group);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Process each group with appropriate pipeline
|
|
328
|
+
const results = [];
|
|
329
|
+
for (const [ext, fileList] of byExtension) {
|
|
330
|
+
const pipeline = this.pipelines.get(ext);
|
|
331
|
+
if (pipeline) {
|
|
332
|
+
const result = await pipeline.ingest(fileList);
|
|
333
|
+
results.push(result);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return results;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Usage
|
|
342
|
+
const multiPipeline = new MultiSourcePipeline(embedder, store);
|
|
343
|
+
await multiPipeline.ingest([
|
|
344
|
+
'./docs/guide.txt',
|
|
345
|
+
'./docs/README.md',
|
|
346
|
+
'./docs/page.html',
|
|
347
|
+
]);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Incremental Updates
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
class IncrementalPipeline {
|
|
354
|
+
private pipeline: IngestionPipeline;
|
|
355
|
+
private processedFiles: Map<string, string>; // file -> hash
|
|
356
|
+
|
|
357
|
+
async ingest(files: string[]) {
|
|
358
|
+
const toProcess = [];
|
|
359
|
+
|
|
360
|
+
for (const file of files) {
|
|
361
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
362
|
+
const hash = this.hash(content);
|
|
363
|
+
const previousHash = this.processedFiles.get(file);
|
|
364
|
+
|
|
365
|
+
if (hash !== previousHash) {
|
|
366
|
+
// File changed or new
|
|
367
|
+
toProcess.push(file);
|
|
368
|
+
|
|
369
|
+
// Remove old version from store
|
|
370
|
+
if (previousHash) {
|
|
371
|
+
await this.removeFileDocuments(file);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
this.processedFiles.set(file, hash);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (toProcess.length > 0) {
|
|
379
|
+
console.log(`Processing ${toProcess.length} changed/new files`);
|
|
380
|
+
await this.pipeline.ingest(toProcess);
|
|
381
|
+
} else {
|
|
382
|
+
console.log('No changes detected');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private hash(content: string): string {
|
|
387
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async removeFileDocuments(file: string) {
|
|
391
|
+
// Remove documents with matching source metadata
|
|
392
|
+
const allDocs = await this.store.getAll();
|
|
393
|
+
const toDelete = allDocs
|
|
394
|
+
.filter(doc => doc.metadata.source === file)
|
|
395
|
+
.map(doc => doc.id);
|
|
396
|
+
await this.store.deleteDocuments(toDelete);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Streaming Ingestion
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import { Readable } from 'stream';
|
|
405
|
+
|
|
406
|
+
class StreamingPipeline {
|
|
407
|
+
async ingestStream(stream: Readable, options: IngestOptions) {
|
|
408
|
+
const chunks: Document[] = [];
|
|
409
|
+
let buffer = '';
|
|
410
|
+
|
|
411
|
+
stream.on('data', async (chunk) => {
|
|
412
|
+
buffer += chunk.toString();
|
|
413
|
+
|
|
414
|
+
// Split into documents when buffer exceeds threshold
|
|
415
|
+
if (buffer.length >= options.chunkSize) {
|
|
416
|
+
const docs = await this.processBuffer(buffer, options);
|
|
417
|
+
chunks.push(...docs);
|
|
418
|
+
buffer = '';
|
|
419
|
+
|
|
420
|
+
// Process batch
|
|
421
|
+
if (chunks.length >= options.batchSize) {
|
|
422
|
+
await this.processBatch(chunks);
|
|
423
|
+
chunks.length = 0;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
stream.on('end', async () => {
|
|
429
|
+
// Process remaining buffer
|
|
430
|
+
if (buffer) {
|
|
431
|
+
const docs = await this.processBuffer(buffer, options);
|
|
432
|
+
chunks.push(...docs);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Process final batch
|
|
436
|
+
if (chunks.length > 0) {
|
|
437
|
+
await this.processBatch(chunks);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private async processBuffer(text: string, options): Promise<Document[]> {
|
|
443
|
+
const [embedding] = await this.embedder.embed([text]);
|
|
444
|
+
return [{
|
|
445
|
+
id: crypto.randomUUID(),
|
|
446
|
+
content: text,
|
|
447
|
+
embedding,
|
|
448
|
+
metadata: options.metadata || {},
|
|
449
|
+
}];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private async processBatch(docs: Document[]) {
|
|
453
|
+
await this.store.addDocuments(docs);
|
|
454
|
+
console.log(`Processed batch of ${docs.length} documents`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## RAG Patterns
|
|
462
|
+
|
|
463
|
+
### Question Answering
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
import OpenAI from 'openai';
|
|
467
|
+
|
|
468
|
+
async function answerQuestion(question: string): Promise<string> {
|
|
469
|
+
// 1. Retrieve relevant context
|
|
470
|
+
const retrieval = await pipeline.query(question, {
|
|
471
|
+
limit: 5,
|
|
472
|
+
threshold: 0.7,
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// 2. Generate answer using context
|
|
476
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
|
|
477
|
+
|
|
478
|
+
const response = await openai.chat.completions.create({
|
|
479
|
+
model: 'gpt-4',
|
|
480
|
+
messages: [
|
|
481
|
+
{
|
|
482
|
+
role: 'system',
|
|
483
|
+
content: 'Answer the question based only on the provided context. If the context doesn\'t contain enough information, say "I don\'t have enough information to answer that."',
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
role: 'user',
|
|
487
|
+
content: `Context:\n${retrieval.context}\n\nQuestion: ${question}`,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
return response.choices[0].message.content || '';
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Usage
|
|
496
|
+
const answer = await answerQuestion('What is machine learning?');
|
|
497
|
+
console.log(answer);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Conversational RAG
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
class ConversationalRAG {
|
|
504
|
+
private pipeline: RetrievalPipeline;
|
|
505
|
+
private openai: OpenAI;
|
|
506
|
+
private history: Array<{ role: string; content: string }> = [];
|
|
507
|
+
|
|
508
|
+
async chat(message: string): Promise<string> {
|
|
509
|
+
// 1. Generate search query from conversation history
|
|
510
|
+
const searchQuery = await this.generateSearchQuery(message);
|
|
511
|
+
|
|
512
|
+
// 2. Retrieve relevant context
|
|
513
|
+
const retrieval = await this.pipeline.query(searchQuery, {
|
|
514
|
+
limit: 5,
|
|
515
|
+
threshold: 0.7,
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// 3. Generate response with context + history
|
|
519
|
+
this.history.push({ role: 'user', content: message });
|
|
520
|
+
|
|
521
|
+
const response = await this.openai.chat.completions.create({
|
|
522
|
+
model: 'gpt-4',
|
|
523
|
+
messages: [
|
|
524
|
+
{
|
|
525
|
+
role: 'system',
|
|
526
|
+
content: `You are a helpful assistant. Use the provided context to answer questions. Context:\n${retrieval.context}`,
|
|
527
|
+
},
|
|
528
|
+
...this.history,
|
|
529
|
+
],
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const assistantMessage = response.choices[0].message.content || '';
|
|
533
|
+
this.history.push({ role: 'assistant', content: assistantMessage });
|
|
534
|
+
|
|
535
|
+
return assistantMessage;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private async generateSearchQuery(message: string): Promise<string> {
|
|
539
|
+
// Use conversation history to generate better search query
|
|
540
|
+
const response = await this.openai.chat.completions.create({
|
|
541
|
+
model: 'gpt-3.5-turbo',
|
|
542
|
+
messages: [
|
|
543
|
+
{ role: 'system', content: 'Generate a semantic search query from the conversation.' },
|
|
544
|
+
...this.history.slice(-4), // Last 2 turns
|
|
545
|
+
{ role: 'user', content: message },
|
|
546
|
+
],
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
return response.choices[0].message.content || message;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Usage
|
|
554
|
+
const rag = new ConversationalRAG(pipeline, openai);
|
|
555
|
+
|
|
556
|
+
console.log(await rag.chat('What is RAG?'));
|
|
557
|
+
console.log(await rag.chat('How does it work?')); // Uses conversation context
|
|
558
|
+
console.log(await rag.chat('What are the benefits?'));
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### Summarization with RAG
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
async function summarizeDocuments(topic: string): Promise<string> {
|
|
565
|
+
// 1. Retrieve all relevant documents
|
|
566
|
+
const retrieval = await pipeline.query(topic, {
|
|
567
|
+
limit: 20, // Get more documents for comprehensive summary
|
|
568
|
+
threshold: 0.6,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// 2. Generate summary
|
|
572
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
|
|
573
|
+
|
|
574
|
+
const response = await openai.chat.completions.create({
|
|
575
|
+
model: 'gpt-4',
|
|
576
|
+
messages: [
|
|
577
|
+
{
|
|
578
|
+
role: 'system',
|
|
579
|
+
content: 'Summarize the key points from the provided documents.',
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
role: 'user',
|
|
583
|
+
content: `Topic: ${topic}\n\nDocuments:\n${retrieval.context}`,
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
return response.choices[0].message.content || '';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Usage
|
|
592
|
+
const summary = await summarizeDocuments('machine learning best practices');
|
|
593
|
+
console.log(summary);
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Best Practices
|
|
599
|
+
|
|
600
|
+
### 1. Batch Processing for Efficiency
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
// ✅ GOOD - Process in batches
|
|
604
|
+
await pipeline.ingest(files, {
|
|
605
|
+
batchSize: 50, // Balance throughput and memory
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ❌ BAD - Process one by one
|
|
609
|
+
for (const file of files) {
|
|
610
|
+
await pipeline.ingestFile(file);
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### 2. Add Progress Tracking
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
// ✅ GOOD - Monitor progress
|
|
618
|
+
await pipeline.ingest(files, {
|
|
619
|
+
onProgress: (current, total, details) => {
|
|
620
|
+
metrics.gauge('ingestion.progress', current / total);
|
|
621
|
+
logger.info({ current, total }, 'Ingestion progress');
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### 3. Handle Errors Gracefully
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// ✅ GOOD - Continue on errors, track failures
|
|
630
|
+
const result = await pipeline.ingest(files, {
|
|
631
|
+
continueOnError: true,
|
|
632
|
+
onError: (error, file) => {
|
|
633
|
+
logger.error({ error, file }, 'Ingestion error');
|
|
634
|
+
metrics.increment('ingestion.errors');
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
console.log(`Success: ${result.documentsProcessed}`);
|
|
639
|
+
console.log(`Failures: ${result.errors.length}`);
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### 4. Use Appropriate Context Size
|
|
643
|
+
|
|
644
|
+
```typescript
|
|
645
|
+
// ✅ GOOD - Balance context quality and cost
|
|
646
|
+
const result = await pipeline.query(question, {
|
|
647
|
+
limit: 5, // Enough for good context, not too much
|
|
648
|
+
threshold: 0.7, // Filter low-quality matches
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
const context = result.context.slice(0, 4000); // Limit tokens
|
|
652
|
+
|
|
653
|
+
// ❌ BAD - Too much context
|
|
654
|
+
const result = await pipeline.query(question, {
|
|
655
|
+
limit: 100, // Excessive, expensive, dilutes signal
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### 5. Monitor Performance
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
class MonitoredPipeline {
|
|
663
|
+
private pipeline: RetrievalPipeline;
|
|
664
|
+
|
|
665
|
+
async query(query: string, options?: QueryOptions) {
|
|
666
|
+
const start = Date.now();
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const result = await this.pipeline.query(query, options);
|
|
670
|
+
const duration = Date.now() - start;
|
|
671
|
+
|
|
672
|
+
// Log metrics
|
|
673
|
+
metrics.histogram('query.duration', duration);
|
|
674
|
+
metrics.gauge('query.results', result.results.length);
|
|
675
|
+
|
|
676
|
+
logger.info({
|
|
677
|
+
query,
|
|
678
|
+
duration,
|
|
679
|
+
results: result.results.length,
|
|
680
|
+
}, 'Query completed');
|
|
681
|
+
|
|
682
|
+
return result;
|
|
683
|
+
} catch (error) {
|
|
684
|
+
metrics.increment('query.errors');
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Troubleshooting
|
|
694
|
+
|
|
695
|
+
### Issue: Slow Ingestion
|
|
696
|
+
|
|
697
|
+
**Problem:** Document ingestion takes too long
|
|
698
|
+
|
|
699
|
+
**Solutions:**
|
|
700
|
+
```typescript
|
|
701
|
+
// 1. Increase batch size
|
|
702
|
+
await pipeline.ingest(files, {
|
|
703
|
+
batchSize: 100, // Process more at once
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// 2. Parallel processing
|
|
707
|
+
const batches = chunk(files, 100);
|
|
708
|
+
await Promise.all(
|
|
709
|
+
batches.map(batch => pipeline.ingest(batch))
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
// 3. Use faster embedding model
|
|
713
|
+
const embedder = new OpenAIEmbeddingGenerator({
|
|
714
|
+
model: 'text-embedding-3-small', // Faster than -large
|
|
715
|
+
dimensions: 512, // Smaller dimensions
|
|
716
|
+
});
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Issue: Poor Retrieval Quality
|
|
720
|
+
|
|
721
|
+
**Problem:** Retrieved context not relevant
|
|
722
|
+
|
|
723
|
+
**Solutions:**
|
|
724
|
+
```typescript
|
|
725
|
+
// 1. Increase threshold
|
|
726
|
+
const result = await pipeline.query(question, {
|
|
727
|
+
threshold: 0.8, // Higher bar for relevance
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// 2. Get more results, filter manually
|
|
731
|
+
const result = await pipeline.query(question, {
|
|
732
|
+
limit: 20,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const filtered = result.results
|
|
736
|
+
.filter(r => r.score >= 0.75)
|
|
737
|
+
.slice(0, 5);
|
|
738
|
+
|
|
739
|
+
// 3. Add metadata filtering
|
|
740
|
+
const result = await pipeline.query(question, {
|
|
741
|
+
filter: {
|
|
742
|
+
field: 'category',
|
|
743
|
+
operator: 'eq',
|
|
744
|
+
value: 'relevant-category',
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Issue: Out of Memory
|
|
750
|
+
|
|
751
|
+
**Problem:** Application crashes during ingestion
|
|
752
|
+
|
|
753
|
+
**Solution:** Stream processingor smaller batches
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
// Option 1: Smaller batches
|
|
757
|
+
await pipeline.ingest(files, {
|
|
758
|
+
batchSize: 10, // Reduce from 50
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
// Option 2: Process sequentially
|
|
762
|
+
for (const batch of chunks(files, 10)) {
|
|
763
|
+
await pipeline.ingest(batch);
|
|
764
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Brief pause
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
**Last Updated:** February 7, 2026
|
|
771
|
+
**Version:** 1.0.0
|