@dangao/bun-server 1.12.0 → 2.0.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 +32 -0
- package/dist/ai/ai-module.d.ts +24 -0
- package/dist/ai/ai-module.d.ts.map +1 -0
- package/dist/ai/decorators.d.ts +25 -0
- package/dist/ai/decorators.d.ts.map +1 -0
- package/dist/ai/errors.d.ts +39 -0
- package/dist/ai/errors.d.ts.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/providers/anthropic-provider.d.ts +23 -0
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/ai/providers/google-provider.d.ts +20 -0
- package/dist/ai/providers/google-provider.d.ts.map +1 -0
- package/dist/ai/providers/ollama-provider.d.ts +17 -0
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
- package/dist/ai/providers/openai-provider.d.ts +28 -0
- package/dist/ai/providers/openai-provider.d.ts.map +1 -0
- package/dist/ai/service.d.ts +40 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/tools/tool-executor.d.ts +15 -0
- package/dist/ai/tools/tool-executor.d.ts.map +1 -0
- package/dist/ai/tools/tool-registry.d.ts +39 -0
- package/dist/ai/tools/tool-registry.d.ts.map +1 -0
- package/dist/ai/types.d.ts +134 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai-guard/ai-guard-module.d.ts +18 -0
- package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
- package/dist/ai-guard/decorators.d.ts +16 -0
- package/dist/ai-guard/decorators.d.ts.map +1 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
- package/dist/ai-guard/index.d.ts +8 -0
- package/dist/ai-guard/index.d.ts.map +1 -0
- package/dist/ai-guard/service.d.ts +21 -0
- package/dist/ai-guard/service.d.ts.map +1 -0
- package/dist/ai-guard/types.d.ts +59 -0
- package/dist/ai-guard/types.d.ts.map +1 -0
- package/dist/conversation/conversation-module.d.ts +25 -0
- package/dist/conversation/conversation-module.d.ts.map +1 -0
- package/dist/conversation/decorators.d.ts +28 -0
- package/dist/conversation/decorators.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +8 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/service.d.ts +43 -0
- package/dist/conversation/service.d.ts.map +1 -0
- package/dist/conversation/stores/database-store.d.ts +46 -0
- package/dist/conversation/stores/database-store.d.ts.map +1 -0
- package/dist/conversation/stores/memory-store.d.ts +17 -0
- package/dist/conversation/stores/memory-store.d.ts.map +1 -0
- package/dist/conversation/stores/redis-store.d.ts +39 -0
- package/dist/conversation/stores/redis-store.d.ts.map +1 -0
- package/dist/conversation/types.d.ts +64 -0
- package/dist/conversation/types.d.ts.map +1 -0
- package/dist/core/cluster.d.ts +42 -3
- package/dist/core/cluster.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/embedding/embedding-module.d.ts +20 -0
- package/dist/embedding/embedding-module.d.ts.map +1 -0
- package/dist/embedding/index.d.ts +6 -0
- package/dist/embedding/index.d.ts.map +1 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/service.d.ts +27 -0
- package/dist/embedding/service.d.ts.map +1 -0
- package/dist/embedding/types.d.ts +25 -0
- package/dist/embedding/types.d.ts.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2870 -88
- package/dist/mcp/decorators.d.ts +42 -0
- package/dist/mcp/decorators.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/mcp-module.d.ts +22 -0
- package/dist/mcp/mcp-module.d.ts.map +1 -0
- package/dist/mcp/registry.d.ts +23 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +29 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/types.d.ts +60 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/prompt/index.d.ts +6 -0
- package/dist/prompt/index.d.ts.map +1 -0
- package/dist/prompt/prompt-module.d.ts +23 -0
- package/dist/prompt/prompt-module.d.ts.map +1 -0
- package/dist/prompt/service.d.ts +47 -0
- package/dist/prompt/service.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts +36 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -0
- package/dist/prompt/stores/memory-store.d.ts +17 -0
- package/dist/prompt/stores/memory-store.d.ts.map +1 -0
- package/dist/prompt/types.d.ts +68 -0
- package/dist/prompt/types.d.ts.map +1 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
- package/dist/rag/chunkers/text-chunker.d.ts +11 -0
- package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
- package/dist/rag/decorators.d.ts +24 -0
- package/dist/rag/decorators.d.ts.map +1 -0
- package/dist/rag/index.d.ts +7 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/rag-module.d.ts +23 -0
- package/dist/rag/rag-module.d.ts.map +1 -0
- package/dist/rag/service.d.ts +36 -0
- package/dist/rag/service.d.ts.map +1 -0
- package/dist/rag/types.d.ts +56 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/vector-store/index.d.ts +6 -0
- package/dist/vector-store/index.d.ts.map +1 -0
- package/dist/vector-store/stores/memory-store.d.ts +17 -0
- package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
- package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
- package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
- package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
- package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
- package/dist/vector-store/types.d.ts +60 -0
- package/dist/vector-store/types.d.ts.map +1 -0
- package/dist/vector-store/vector-store-module.d.ts +20 -0
- package/dist/vector-store/vector-store-module.d.ts.map +1 -0
- package/docs/ai.md +500 -0
- package/docs/best-practices.md +83 -8
- package/docs/database.md +23 -0
- package/docs/guide.md +90 -27
- package/docs/migration.md +81 -7
- package/docs/security.md +23 -0
- package/docs/zh/ai.md +441 -0
- package/docs/zh/best-practices.md +43 -0
- package/docs/zh/database.md +23 -0
- package/docs/zh/guide.md +40 -1
- package/docs/zh/migration.md +39 -0
- package/docs/zh/security.md +23 -0
- package/package.json +2 -2
- package/src/ai/ai-module.ts +62 -0
- package/src/ai/decorators.ts +30 -0
- package/src/ai/errors.ts +71 -0
- package/src/ai/index.ts +11 -0
- package/src/ai/providers/anthropic-provider.ts +190 -0
- package/src/ai/providers/google-provider.ts +179 -0
- package/src/ai/providers/ollama-provider.ts +126 -0
- package/src/ai/providers/openai-provider.ts +242 -0
- package/src/ai/service.ts +155 -0
- package/src/ai/tools/tool-executor.ts +38 -0
- package/src/ai/tools/tool-registry.ts +91 -0
- package/src/ai/types.ts +145 -0
- package/src/ai-guard/ai-guard-module.ts +50 -0
- package/src/ai-guard/decorators.ts +21 -0
- package/src/ai-guard/detectors/content-moderator.ts +80 -0
- package/src/ai-guard/detectors/injection-detector.ts +48 -0
- package/src/ai-guard/detectors/pii-detector.ts +64 -0
- package/src/ai-guard/index.ts +7 -0
- package/src/ai-guard/service.ts +100 -0
- package/src/ai-guard/types.ts +61 -0
- package/src/conversation/conversation-module.ts +63 -0
- package/src/conversation/decorators.ts +47 -0
- package/src/conversation/index.ts +7 -0
- package/src/conversation/service.ts +133 -0
- package/src/conversation/stores/database-store.ts +125 -0
- package/src/conversation/stores/memory-store.ts +57 -0
- package/src/conversation/stores/redis-store.ts +101 -0
- package/src/conversation/types.ts +68 -0
- package/src/core/cluster.ts +239 -46
- package/src/core/index.ts +1 -1
- package/src/core/server.ts +91 -78
- package/src/embedding/embedding-module.ts +52 -0
- package/src/embedding/index.ts +5 -0
- package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
- package/src/embedding/providers/openai-embedding-provider.ts +47 -0
- package/src/embedding/service.ts +55 -0
- package/src/embedding/types.ts +27 -0
- package/src/index.ts +11 -1
- package/src/mcp/decorators.ts +60 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/mcp-module.ts +58 -0
- package/src/mcp/registry.ts +72 -0
- package/src/mcp/server.ts +164 -0
- package/src/mcp/types.ts +63 -0
- package/src/prompt/index.ts +5 -0
- package/src/prompt/prompt-module.ts +61 -0
- package/src/prompt/service.ts +93 -0
- package/src/prompt/stores/file-store.ts +135 -0
- package/src/prompt/stores/memory-store.ts +82 -0
- package/src/prompt/types.ts +84 -0
- package/src/rag/chunkers/markdown-chunker.ts +40 -0
- package/src/rag/chunkers/text-chunker.ts +30 -0
- package/src/rag/decorators.ts +26 -0
- package/src/rag/index.ts +6 -0
- package/src/rag/rag-module.ts +78 -0
- package/src/rag/service.ts +134 -0
- package/src/rag/types.ts +47 -0
- package/src/vector-store/index.ts +5 -0
- package/src/vector-store/stores/memory-store.ts +69 -0
- package/src/vector-store/stores/pinecone-store.ts +123 -0
- package/src/vector-store/stores/qdrant-store.ts +147 -0
- package/src/vector-store/types.ts +77 -0
- package/src/vector-store/vector-store-module.ts +50 -0
- package/tests/ai/ai-module.test.ts +46 -0
- package/tests/ai/ai-service.test.ts +91 -0
- package/tests/ai/tool-registry.test.ts +57 -0
- package/tests/ai-guard/ai-guard-module.test.ts +23 -0
- package/tests/ai-guard/content-moderator.test.ts +65 -0
- package/tests/ai-guard/pii-detector.test.ts +41 -0
- package/tests/conversation/conversation-module.test.ts +26 -0
- package/tests/conversation/conversation-service.test.ts +64 -0
- package/tests/conversation/memory-store.test.ts +68 -0
- package/tests/core/cluster.test.ts +45 -1
- package/tests/embedding/embedding-service.test.ts +55 -0
- package/tests/mcp/mcp-server.test.ts +85 -0
- package/tests/prompt/prompt-module.test.ts +30 -0
- package/tests/prompt/prompt-service.test.ts +74 -0
- package/tests/rag/chunkers.test.ts +58 -0
- package/tests/rag/rag-service.test.ts +66 -0
- package/tests/vector-store/memory-vector-store.test.ts +84 -0
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
- package/tests/perf/optimization.test.ts +0 -182
- package/tests/perf/regression.test.ts +0 -120
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { Injectable } from '../di/decorators';
|
|
2
|
+
import { Inject } from '../di/decorators';
|
|
3
|
+
import type { EmbeddingService } from '../embedding/service';
|
|
4
|
+
import { EMBEDDING_SERVICE_TOKEN } from '../embedding/types';
|
|
5
|
+
import type { VectorStore } from '../vector-store/types';
|
|
6
|
+
import { VECTOR_STORE_TOKEN } from '../vector-store/types';
|
|
7
|
+
import type { IngestSource, RagContext, RagModuleOptions, DocumentChunker } from './types';
|
|
8
|
+
import { RAG_OPTIONS_TOKEN } from './types';
|
|
9
|
+
import { TextChunker } from './chunkers/text-chunker';
|
|
10
|
+
import { MarkdownChunker } from './chunkers/markdown-chunker';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RAG service — document ingestion pipeline and context retrieval.
|
|
14
|
+
*
|
|
15
|
+
* Ingestion: source → load text → chunk → embed → store in VectorStore
|
|
16
|
+
* Retrieval: query → embed → search VectorStore → format context
|
|
17
|
+
*/
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class RagService {
|
|
20
|
+
private readonly options: RagModuleOptions;
|
|
21
|
+
private readonly embeddingService: EmbeddingService;
|
|
22
|
+
private readonly vectorStore: VectorStore;
|
|
23
|
+
private readonly textChunker: DocumentChunker;
|
|
24
|
+
private readonly markdownChunker: DocumentChunker;
|
|
25
|
+
|
|
26
|
+
public constructor(
|
|
27
|
+
@Inject(RAG_OPTIONS_TOKEN) options: RagModuleOptions,
|
|
28
|
+
@Inject(EMBEDDING_SERVICE_TOKEN) embeddingService: EmbeddingService,
|
|
29
|
+
@Inject(VECTOR_STORE_TOKEN) vectorStore: VectorStore,
|
|
30
|
+
) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.embeddingService = embeddingService;
|
|
33
|
+
this.vectorStore = vectorStore;
|
|
34
|
+
this.textChunker = new TextChunker(options.chunkSize, options.chunkOverlap);
|
|
35
|
+
this.markdownChunker = new MarkdownChunker(options.chunkSize);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ingest a document source into the vector store
|
|
40
|
+
* @param source - text, file path, or URL
|
|
41
|
+
* @param collection - override default collection
|
|
42
|
+
* @returns Number of chunks stored
|
|
43
|
+
*/
|
|
44
|
+
public async ingest(source: IngestSource, collection?: string): Promise<number> {
|
|
45
|
+
const targetCollection = collection ?? this.options.collection ?? 'rag';
|
|
46
|
+
const text = await this.loadText(source);
|
|
47
|
+
|
|
48
|
+
if (!text.trim()) return 0;
|
|
49
|
+
|
|
50
|
+
// Choose chunker based on content type
|
|
51
|
+
const isMarkdown = source.type === 'file' && (source.path.endsWith('.md') || source.path.endsWith('.mdx'));
|
|
52
|
+
const chunker = isMarkdown ? this.markdownChunker : this.textChunker;
|
|
53
|
+
const chunks = chunker.chunk(text);
|
|
54
|
+
|
|
55
|
+
// Batch embed all chunks
|
|
56
|
+
const vectors = await this.embeddingService.embedBatch(chunks.map((c) => c.content));
|
|
57
|
+
|
|
58
|
+
// Store in vector store
|
|
59
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
60
|
+
const chunk = chunks[i]!;
|
|
61
|
+
const vector = vectors[i]!;
|
|
62
|
+
await this.vectorStore.upsert({
|
|
63
|
+
id: crypto.randomUUID(),
|
|
64
|
+
vector,
|
|
65
|
+
content: chunk.content,
|
|
66
|
+
collection: targetCollection,
|
|
67
|
+
metadata: {
|
|
68
|
+
...(chunk.metadata ?? {}),
|
|
69
|
+
...(source.metadata ?? {}),
|
|
70
|
+
sourceType: source.type,
|
|
71
|
+
...(source.type === 'file' ? { sourcePath: source.path } : {}),
|
|
72
|
+
...(source.type === 'url' ? { sourceUrl: source.url } : {}),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return chunks.length;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retrieve relevant context for a query
|
|
82
|
+
* @param query - User query text
|
|
83
|
+
* @param collection - override default collection
|
|
84
|
+
*/
|
|
85
|
+
public async retrieve(query: string, collection?: string): Promise<RagContext> {
|
|
86
|
+
const targetCollection = collection ?? this.options.collection ?? 'rag';
|
|
87
|
+
const queryVector = await this.embeddingService.embed(query);
|
|
88
|
+
|
|
89
|
+
const results = await this.vectorStore.search(queryVector, {
|
|
90
|
+
topK: this.options.topK ?? 5,
|
|
91
|
+
minScore: this.options.minScore ?? 0.5,
|
|
92
|
+
collection: targetCollection,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const chunks = results.map((r) => ({
|
|
96
|
+
content: r.document.content,
|
|
97
|
+
score: r.score,
|
|
98
|
+
metadata: r.document.metadata,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
const formatted = chunks.length > 0
|
|
102
|
+
? chunks.map((c, i) => `[${i + 1}] ${c.content}`).join('\n\n')
|
|
103
|
+
: '';
|
|
104
|
+
|
|
105
|
+
return { chunks, formatted };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build a system prompt with retrieved context
|
|
110
|
+
*/
|
|
111
|
+
public async buildContextPrompt(query: string, collection?: string): Promise<string> {
|
|
112
|
+
const context = await this.retrieve(query, collection);
|
|
113
|
+
if (!context.formatted) return '';
|
|
114
|
+
return `Use the following context to answer the question:\n\n${context.formatted}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async loadText(source: IngestSource): Promise<string> {
|
|
118
|
+
switch (source.type) {
|
|
119
|
+
case 'text':
|
|
120
|
+
return source.content;
|
|
121
|
+
|
|
122
|
+
case 'file': {
|
|
123
|
+
const file = Bun.file(source.path);
|
|
124
|
+
return file.text();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case 'url': {
|
|
128
|
+
const res = await fetch(source.url);
|
|
129
|
+
if (!res.ok) throw new Error(`Failed to fetch ${source.url}: ${res.status}`);
|
|
130
|
+
return res.text();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/rag/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chunk of text produced by a document chunker
|
|
3
|
+
*/
|
|
4
|
+
export interface DocumentChunk {
|
|
5
|
+
content: string;
|
|
6
|
+
metadata?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Source for document ingestion
|
|
11
|
+
*/
|
|
12
|
+
export type IngestSource =
|
|
13
|
+
| { type: 'text'; content: string; metadata?: Record<string, unknown> }
|
|
14
|
+
| { type: 'file'; path: string; metadata?: Record<string, unknown> }
|
|
15
|
+
| { type: 'url'; url: string; metadata?: Record<string, unknown> };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Abstract document chunker
|
|
19
|
+
*/
|
|
20
|
+
export interface DocumentChunker {
|
|
21
|
+
chunk(text: string): DocumentChunk[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Retrieved context for RAG
|
|
26
|
+
*/
|
|
27
|
+
export interface RagContext {
|
|
28
|
+
chunks: Array<{ content: string; score: number; metadata?: Record<string, unknown> }>;
|
|
29
|
+
/** Formatted context string for inclusion in prompts */
|
|
30
|
+
formatted: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RagModuleOptions {
|
|
34
|
+
/** Collection name in VectorStore (default: 'rag') */
|
|
35
|
+
collection?: string;
|
|
36
|
+
/** Number of chunks per ingested document (default: 512) */
|
|
37
|
+
chunkSize?: number;
|
|
38
|
+
/** Overlap between consecutive chunks (default: 50) */
|
|
39
|
+
chunkOverlap?: number;
|
|
40
|
+
/** Number of top results to retrieve (default: 5) */
|
|
41
|
+
topK?: number;
|
|
42
|
+
/** Minimum similarity score to include (default: 0.5) */
|
|
43
|
+
minScore?: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const RAG_SERVICE_TOKEN = Symbol('@dangao/bun-server:rag:service');
|
|
47
|
+
export const RAG_OPTIONS_TOKEN = Symbol('@dangao/bun-server:rag:options');
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { VectorStore, VectorDocument, VectorSearchResult, VectorSearchOptions } from '../types';
|
|
2
|
+
import { cosineSimilarity } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* High-performance in-memory vector store using cosine similarity.
|
|
6
|
+
* Suitable for development and small to medium datasets (up to ~100K documents).
|
|
7
|
+
*/
|
|
8
|
+
export class MemoryVectorStore implements VectorStore {
|
|
9
|
+
private readonly documents = new Map<string, VectorDocument>();
|
|
10
|
+
|
|
11
|
+
public async upsert(document: VectorDocument): Promise<void> {
|
|
12
|
+
this.documents.set(this.key(document.id, document.collection), document);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async upsertBatch(documents: VectorDocument[]): Promise<void> {
|
|
16
|
+
for (const doc of documents) {
|
|
17
|
+
this.documents.set(this.key(doc.id, doc.collection), doc);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async get(id: string, collection?: string): Promise<VectorDocument | null> {
|
|
22
|
+
return this.documents.get(this.key(id, collection)) ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public async search(query: number[], options: VectorSearchOptions = {}): Promise<VectorSearchResult[]> {
|
|
26
|
+
const { topK = 5, minScore = 0, collection, filter } = options;
|
|
27
|
+
|
|
28
|
+
const results: VectorSearchResult[] = [];
|
|
29
|
+
|
|
30
|
+
for (const doc of this.documents.values()) {
|
|
31
|
+
if (collection && doc.collection !== collection) continue;
|
|
32
|
+
if (filter && !filter(doc)) continue;
|
|
33
|
+
|
|
34
|
+
const score = cosineSimilarity(query, doc.vector);
|
|
35
|
+
if (score >= minScore) {
|
|
36
|
+
results.push({ document: doc, score });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return results
|
|
41
|
+
.sort((a, b) => b.score - a.score)
|
|
42
|
+
.slice(0, topK);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async delete(id: string, collection?: string): Promise<boolean> {
|
|
46
|
+
return this.documents.delete(this.key(id, collection));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async deleteCollection(collection: string): Promise<void> {
|
|
50
|
+
for (const [key, doc] of this.documents.entries()) {
|
|
51
|
+
if (doc.collection === collection) {
|
|
52
|
+
this.documents.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async count(collection?: string): Promise<number> {
|
|
58
|
+
if (!collection) return this.documents.size;
|
|
59
|
+
let count = 0;
|
|
60
|
+
for (const doc of this.documents.values()) {
|
|
61
|
+
if (doc.collection === collection) count++;
|
|
62
|
+
}
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private key(id: string, collection?: string): string {
|
|
67
|
+
return collection ? `${collection}:${id}` : id;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { VectorStore, VectorDocument, VectorSearchResult, VectorSearchOptions } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface PineconeStoreConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
/** Pinecone host URL (e.g., https://my-index-xxxx.svc.aped-xxxx.pinecone.io) */
|
|
6
|
+
host: string;
|
|
7
|
+
/** Namespace (optional) */
|
|
8
|
+
namespace?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Pinecone vector store adapter.
|
|
13
|
+
* Uses the Pinecone REST API directly — no SDK dependency.
|
|
14
|
+
*/
|
|
15
|
+
export class PineconeVectorStore implements VectorStore {
|
|
16
|
+
private readonly apiKey: string;
|
|
17
|
+
private readonly host: string;
|
|
18
|
+
private readonly namespace: string;
|
|
19
|
+
|
|
20
|
+
public constructor(config: PineconeStoreConfig) {
|
|
21
|
+
this.apiKey = config.apiKey;
|
|
22
|
+
this.host = config.host.replace(/\/$/, '');
|
|
23
|
+
this.namespace = config.namespace ?? '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async upsert(document: VectorDocument): Promise<void> {
|
|
27
|
+
await this.upsertBatch([document]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async upsertBatch(documents: VectorDocument[]): Promise<void> {
|
|
31
|
+
const vectors = documents.map((d) => ({
|
|
32
|
+
id: d.id,
|
|
33
|
+
values: d.vector,
|
|
34
|
+
metadata: { content: d.content, collection: d.collection ?? '', ...d.metadata },
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
await this.request('/vectors/upsert', 'POST', { vectors, namespace: this.namespace });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public async get(id: string): Promise<VectorDocument | null> {
|
|
41
|
+
const res = await this.request<{ vectors: Record<string, unknown> }>(
|
|
42
|
+
`/vectors/fetch?ids=${encodeURIComponent(id)}&namespace=${this.namespace}`,
|
|
43
|
+
'GET',
|
|
44
|
+
);
|
|
45
|
+
const vector = res?.['vectors']?.[id] as Record<string, unknown> | undefined;
|
|
46
|
+
if (!vector) return null;
|
|
47
|
+
const metadata = (vector['metadata'] as Record<string, unknown>) ?? {};
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
vector: vector['values'] as number[],
|
|
51
|
+
content: (metadata['content'] as string) ?? '',
|
|
52
|
+
collection: (metadata['collection'] as string) || undefined,
|
|
53
|
+
metadata,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async search(query: number[], options: VectorSearchOptions = {}): Promise<VectorSearchResult[]> {
|
|
58
|
+
const { topK = 5, collection } = options;
|
|
59
|
+
const filter = collection ? { collection: { $eq: collection } } : undefined;
|
|
60
|
+
|
|
61
|
+
const res = await this.request<{ matches: Array<Record<string, unknown>> }>(
|
|
62
|
+
'/query',
|
|
63
|
+
'POST',
|
|
64
|
+
{
|
|
65
|
+
vector: query,
|
|
66
|
+
topK,
|
|
67
|
+
includeMetadata: true,
|
|
68
|
+
namespace: this.namespace,
|
|
69
|
+
filter,
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (res?.['matches'] ?? []).map((match) => {
|
|
74
|
+
const metadata = (match['metadata'] as Record<string, unknown>) ?? {};
|
|
75
|
+
return {
|
|
76
|
+
document: {
|
|
77
|
+
id: match['id'] as string,
|
|
78
|
+
vector: [],
|
|
79
|
+
content: (metadata['content'] as string) ?? '',
|
|
80
|
+
collection: (metadata['collection'] as string) || undefined,
|
|
81
|
+
metadata,
|
|
82
|
+
},
|
|
83
|
+
score: match['score'] as number,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async delete(id: string): Promise<boolean> {
|
|
89
|
+
await this.request('/vectors/delete', 'POST', { ids: [id], namespace: this.namespace });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public async deleteCollection(collection: string): Promise<void> {
|
|
94
|
+
await this.request('/vectors/delete', 'POST', {
|
|
95
|
+
deleteAll: false,
|
|
96
|
+
namespace: this.namespace,
|
|
97
|
+
filter: { collection: { $eq: collection } },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public async count(): Promise<number> {
|
|
102
|
+
const res = await this.request<{ namespaces: Record<string, { vectorCount: number }> }>(
|
|
103
|
+
'/describe_index_stats',
|
|
104
|
+
'POST',
|
|
105
|
+
{},
|
|
106
|
+
);
|
|
107
|
+
const ns = res?.['namespaces']?.[this.namespace];
|
|
108
|
+
return ns?.vectorCount ?? 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async request<T = unknown>(path: string, method: string, body?: unknown): Promise<T | null> {
|
|
112
|
+
const res = await fetch(`${this.host}${path}`, {
|
|
113
|
+
method,
|
|
114
|
+
headers: {
|
|
115
|
+
'Api-Key': this.apiKey,
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
},
|
|
118
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
119
|
+
});
|
|
120
|
+
if (!res.ok) return null;
|
|
121
|
+
return res.json() as Promise<T>;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { VectorStore, VectorDocument, VectorSearchResult, VectorSearchOptions } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface QdrantStoreConfig {
|
|
4
|
+
/** Qdrant server URL (default: http://localhost:6333) */
|
|
5
|
+
url?: string;
|
|
6
|
+
/** Collection name in Qdrant */
|
|
7
|
+
collectionName: string;
|
|
8
|
+
/** API key (optional, for Qdrant Cloud) */
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Qdrant vector store adapter.
|
|
14
|
+
* Uses the Qdrant REST API directly — no SDK dependency.
|
|
15
|
+
*/
|
|
16
|
+
export class QdrantVectorStore implements VectorStore {
|
|
17
|
+
private readonly url: string;
|
|
18
|
+
private readonly collectionName: string;
|
|
19
|
+
private readonly apiKey: string | undefined;
|
|
20
|
+
|
|
21
|
+
public constructor(config: QdrantStoreConfig) {
|
|
22
|
+
this.url = (config.url ?? 'http://localhost:6333').replace(/\/$/, '');
|
|
23
|
+
this.collectionName = config.collectionName;
|
|
24
|
+
this.apiKey = config.apiKey;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async upsert(document: VectorDocument): Promise<void> {
|
|
28
|
+
await this.upsertBatch([document]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async upsertBatch(documents: VectorDocument[]): Promise<void> {
|
|
32
|
+
const points = documents.map((d, idx) => ({
|
|
33
|
+
id: this.toNumericId(d.id) ?? idx,
|
|
34
|
+
vector: d.vector,
|
|
35
|
+
payload: {
|
|
36
|
+
original_id: d.id,
|
|
37
|
+
content: d.content,
|
|
38
|
+
collection: d.collection ?? '',
|
|
39
|
+
...(d.metadata ?? {}),
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
await this.request(`/collections/${this.collectionName}/points`, 'PUT', { points });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async get(id: string): Promise<VectorDocument | null> {
|
|
47
|
+
const numId = this.toNumericId(id);
|
|
48
|
+
if (!numId) return null;
|
|
49
|
+
|
|
50
|
+
const res = await this.request<{ result: Record<string, unknown> | null }>(
|
|
51
|
+
`/collections/${this.collectionName}/points/${numId}`,
|
|
52
|
+
'GET',
|
|
53
|
+
);
|
|
54
|
+
const point = res?.['result'];
|
|
55
|
+
if (!point) return null;
|
|
56
|
+
|
|
57
|
+
const payload = (point['payload'] as Record<string, unknown>) ?? {};
|
|
58
|
+
return {
|
|
59
|
+
id: (payload['original_id'] as string) ?? id,
|
|
60
|
+
vector: (point['vector'] as number[]) ?? [],
|
|
61
|
+
content: (payload['content'] as string) ?? '',
|
|
62
|
+
collection: (payload['collection'] as string) || undefined,
|
|
63
|
+
metadata: payload,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public async search(query: number[], options: VectorSearchOptions = {}): Promise<VectorSearchResult[]> {
|
|
68
|
+
const { topK = 5, minScore = 0, collection } = options;
|
|
69
|
+
|
|
70
|
+
const body: Record<string, unknown> = {
|
|
71
|
+
vector: query,
|
|
72
|
+
limit: topK,
|
|
73
|
+
with_payload: true,
|
|
74
|
+
score_threshold: minScore,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (collection) {
|
|
78
|
+
body['filter'] = { must: [{ key: 'collection', match: { value: collection } }] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const res = await this.request<{ result: Array<Record<string, unknown>> }>(
|
|
82
|
+
`/collections/${this.collectionName}/points/search`,
|
|
83
|
+
'POST',
|
|
84
|
+
body,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return (res?.['result'] ?? []).map((hit) => {
|
|
88
|
+
const payload = (hit['payload'] as Record<string, unknown>) ?? {};
|
|
89
|
+
return {
|
|
90
|
+
document: {
|
|
91
|
+
id: (payload['original_id'] as string) ?? String(hit['id']),
|
|
92
|
+
vector: [],
|
|
93
|
+
content: (payload['content'] as string) ?? '',
|
|
94
|
+
collection: (payload['collection'] as string) || undefined,
|
|
95
|
+
metadata: payload,
|
|
96
|
+
},
|
|
97
|
+
score: hit['score'] as number,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public async delete(id: string): Promise<boolean> {
|
|
103
|
+
const numId = this.toNumericId(id);
|
|
104
|
+
if (!numId) return false;
|
|
105
|
+
await this.request(
|
|
106
|
+
`/collections/${this.collectionName}/points/delete`,
|
|
107
|
+
'POST',
|
|
108
|
+
{ points: [numId] },
|
|
109
|
+
);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async deleteCollection(collection: string): Promise<void> {
|
|
114
|
+
await this.request(
|
|
115
|
+
`/collections/${this.collectionName}/points/delete`,
|
|
116
|
+
'POST',
|
|
117
|
+
{ filter: { must: [{ key: 'collection', match: { value: collection } }] } },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public async count(): Promise<number> {
|
|
122
|
+
const res = await this.request<{ result: { count: number } }>(
|
|
123
|
+
`/collections/${this.collectionName}/points/count`,
|
|
124
|
+
'POST',
|
|
125
|
+
{},
|
|
126
|
+
);
|
|
127
|
+
return res?.['result']?.count ?? 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private toNumericId(id: string): number | null {
|
|
131
|
+
const n = parseInt(id, 10);
|
|
132
|
+
return isNaN(n) ? null : n;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async request<T = unknown>(path: string, method: string, body?: unknown): Promise<T | null> {
|
|
136
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
|
137
|
+
if (this.apiKey) headers['api-key'] = this.apiKey;
|
|
138
|
+
|
|
139
|
+
const res = await fetch(`${this.url}${path}`, {
|
|
140
|
+
method,
|
|
141
|
+
headers,
|
|
142
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
143
|
+
});
|
|
144
|
+
if (!res.ok) return null;
|
|
145
|
+
return res.json() as Promise<T>;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A document stored in the vector store
|
|
3
|
+
*/
|
|
4
|
+
export interface VectorDocument {
|
|
5
|
+
id: string;
|
|
6
|
+
/** The embedding vector */
|
|
7
|
+
vector: number[];
|
|
8
|
+
/** Original text content */
|
|
9
|
+
content: string;
|
|
10
|
+
/** Arbitrary metadata */
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
/** Collection / namespace */
|
|
13
|
+
collection?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Search result with similarity score
|
|
18
|
+
*/
|
|
19
|
+
export interface VectorSearchResult {
|
|
20
|
+
document: VectorDocument;
|
|
21
|
+
/** Cosine similarity score [0, 1] */
|
|
22
|
+
score: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Search options
|
|
27
|
+
*/
|
|
28
|
+
export interface VectorSearchOptions {
|
|
29
|
+
topK?: number;
|
|
30
|
+
minScore?: number;
|
|
31
|
+
collection?: string;
|
|
32
|
+
filter?: (doc: VectorDocument) => boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Abstract vector store interface
|
|
37
|
+
*/
|
|
38
|
+
export interface VectorStore {
|
|
39
|
+
/** Insert or update a document */
|
|
40
|
+
upsert(document: VectorDocument): Promise<void>;
|
|
41
|
+
/** Insert or update multiple documents */
|
|
42
|
+
upsertBatch(documents: VectorDocument[]): Promise<void>;
|
|
43
|
+
/** Retrieve a document by ID */
|
|
44
|
+
get(id: string, collection?: string): Promise<VectorDocument | null>;
|
|
45
|
+
/** Search for similar documents using cosine similarity */
|
|
46
|
+
search(query: number[], options?: VectorSearchOptions): Promise<VectorSearchResult[]>;
|
|
47
|
+
/** Delete a document by ID */
|
|
48
|
+
delete(id: string, collection?: string): Promise<boolean>;
|
|
49
|
+
/** Delete all documents in a collection */
|
|
50
|
+
deleteCollection(collection: string): Promise<void>;
|
|
51
|
+
/** Count documents */
|
|
52
|
+
count(collection?: string): Promise<number>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface VectorStoreModuleOptions {
|
|
56
|
+
store?: VectorStore;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const VECTOR_STORE_TOKEN = Symbol('@dangao/bun-server:vector-store:store');
|
|
60
|
+
export const VECTOR_STORE_OPTIONS_TOKEN = Symbol('@dangao/bun-server:vector-store:options');
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Compute cosine similarity between two vectors
|
|
64
|
+
*/
|
|
65
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
66
|
+
if (a.length !== b.length) return 0;
|
|
67
|
+
let dot = 0;
|
|
68
|
+
let normA = 0;
|
|
69
|
+
let normB = 0;
|
|
70
|
+
for (let i = 0; i < a.length; i++) {
|
|
71
|
+
dot += a[i]! * b[i]!;
|
|
72
|
+
normA += a[i]! * a[i]!;
|
|
73
|
+
normB += b[i]! * b[i]!;
|
|
74
|
+
}
|
|
75
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
76
|
+
return denom === 0 ? 0 : dot / denom;
|
|
77
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import type { ModuleProvider } from '../di/module';
|
|
3
|
+
import {
|
|
4
|
+
VECTOR_STORE_TOKEN,
|
|
5
|
+
VECTOR_STORE_OPTIONS_TOKEN,
|
|
6
|
+
type VectorStoreModuleOptions,
|
|
7
|
+
} from './types';
|
|
8
|
+
import { MemoryVectorStore } from './stores/memory-store';
|
|
9
|
+
|
|
10
|
+
@Module({ providers: [] })
|
|
11
|
+
export class VectorStoreModule {
|
|
12
|
+
/**
|
|
13
|
+
* Configure the vector store module.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // Built-in memory store (default):
|
|
18
|
+
* VectorStoreModule.forRoot({});
|
|
19
|
+
*
|
|
20
|
+
* // Pinecone:
|
|
21
|
+
* VectorStoreModule.forRoot({
|
|
22
|
+
* store: new PineconeVectorStore({ apiKey: '...', host: '...' }),
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
public static forRoot(options: VectorStoreModuleOptions = {}): typeof VectorStoreModule {
|
|
27
|
+
const store = options.store ?? new MemoryVectorStore();
|
|
28
|
+
|
|
29
|
+
const providers: ModuleProvider[] = [
|
|
30
|
+
{ provide: VECTOR_STORE_OPTIONS_TOKEN, useValue: options },
|
|
31
|
+
{ provide: VECTOR_STORE_TOKEN, useValue: store },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const existing = Reflect.getMetadata(MODULE_METADATA_KEY, VectorStoreModule) || {};
|
|
35
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, {
|
|
36
|
+
...existing,
|
|
37
|
+
providers: [...(existing.providers || []), ...providers],
|
|
38
|
+
exports: [
|
|
39
|
+
...(existing.exports || []),
|
|
40
|
+
VECTOR_STORE_TOKEN,
|
|
41
|
+
],
|
|
42
|
+
}, VectorStoreModule);
|
|
43
|
+
|
|
44
|
+
return VectorStoreModule;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static reset(): void {
|
|
48
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, VectorStoreModule);
|
|
49
|
+
}
|
|
50
|
+
}
|