@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.
Files changed (223) hide show
  1. package/README.md +32 -0
  2. package/dist/ai/ai-module.d.ts +24 -0
  3. package/dist/ai/ai-module.d.ts.map +1 -0
  4. package/dist/ai/decorators.d.ts +25 -0
  5. package/dist/ai/decorators.d.ts.map +1 -0
  6. package/dist/ai/errors.d.ts +39 -0
  7. package/dist/ai/errors.d.ts.map +1 -0
  8. package/dist/ai/index.d.ts +12 -0
  9. package/dist/ai/index.d.ts.map +1 -0
  10. package/dist/ai/providers/anthropic-provider.d.ts +23 -0
  11. package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
  12. package/dist/ai/providers/google-provider.d.ts +20 -0
  13. package/dist/ai/providers/google-provider.d.ts.map +1 -0
  14. package/dist/ai/providers/ollama-provider.d.ts +17 -0
  15. package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
  16. package/dist/ai/providers/openai-provider.d.ts +28 -0
  17. package/dist/ai/providers/openai-provider.d.ts.map +1 -0
  18. package/dist/ai/service.d.ts +40 -0
  19. package/dist/ai/service.d.ts.map +1 -0
  20. package/dist/ai/tools/tool-executor.d.ts +15 -0
  21. package/dist/ai/tools/tool-executor.d.ts.map +1 -0
  22. package/dist/ai/tools/tool-registry.d.ts +39 -0
  23. package/dist/ai/tools/tool-registry.d.ts.map +1 -0
  24. package/dist/ai/types.d.ts +134 -0
  25. package/dist/ai/types.d.ts.map +1 -0
  26. package/dist/ai-guard/ai-guard-module.d.ts +18 -0
  27. package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
  28. package/dist/ai-guard/decorators.d.ts +16 -0
  29. package/dist/ai-guard/decorators.d.ts.map +1 -0
  30. package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
  31. package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
  32. package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
  33. package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
  34. package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
  35. package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
  36. package/dist/ai-guard/index.d.ts +8 -0
  37. package/dist/ai-guard/index.d.ts.map +1 -0
  38. package/dist/ai-guard/service.d.ts +21 -0
  39. package/dist/ai-guard/service.d.ts.map +1 -0
  40. package/dist/ai-guard/types.d.ts +59 -0
  41. package/dist/ai-guard/types.d.ts.map +1 -0
  42. package/dist/conversation/conversation-module.d.ts +25 -0
  43. package/dist/conversation/conversation-module.d.ts.map +1 -0
  44. package/dist/conversation/decorators.d.ts +28 -0
  45. package/dist/conversation/decorators.d.ts.map +1 -0
  46. package/dist/conversation/index.d.ts +8 -0
  47. package/dist/conversation/index.d.ts.map +1 -0
  48. package/dist/conversation/service.d.ts +43 -0
  49. package/dist/conversation/service.d.ts.map +1 -0
  50. package/dist/conversation/stores/database-store.d.ts +46 -0
  51. package/dist/conversation/stores/database-store.d.ts.map +1 -0
  52. package/dist/conversation/stores/memory-store.d.ts +17 -0
  53. package/dist/conversation/stores/memory-store.d.ts.map +1 -0
  54. package/dist/conversation/stores/redis-store.d.ts +39 -0
  55. package/dist/conversation/stores/redis-store.d.ts.map +1 -0
  56. package/dist/conversation/types.d.ts +64 -0
  57. package/dist/conversation/types.d.ts.map +1 -0
  58. package/dist/core/cluster.d.ts +42 -3
  59. package/dist/core/cluster.d.ts.map +1 -1
  60. package/dist/core/index.d.ts +1 -1
  61. package/dist/core/index.d.ts.map +1 -1
  62. package/dist/core/server.d.ts.map +1 -1
  63. package/dist/embedding/embedding-module.d.ts +20 -0
  64. package/dist/embedding/embedding-module.d.ts.map +1 -0
  65. package/dist/embedding/index.d.ts +6 -0
  66. package/dist/embedding/index.d.ts.map +1 -0
  67. package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
  68. package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
  69. package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
  70. package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
  71. package/dist/embedding/service.d.ts +27 -0
  72. package/dist/embedding/service.d.ts.map +1 -0
  73. package/dist/embedding/types.d.ts +25 -0
  74. package/dist/embedding/types.d.ts.map +1 -0
  75. package/dist/index.d.ts +9 -1
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +2870 -88
  78. package/dist/mcp/decorators.d.ts +42 -0
  79. package/dist/mcp/decorators.d.ts.map +1 -0
  80. package/dist/mcp/index.d.ts +6 -0
  81. package/dist/mcp/index.d.ts.map +1 -0
  82. package/dist/mcp/mcp-module.d.ts +22 -0
  83. package/dist/mcp/mcp-module.d.ts.map +1 -0
  84. package/dist/mcp/registry.d.ts +23 -0
  85. package/dist/mcp/registry.d.ts.map +1 -0
  86. package/dist/mcp/server.d.ts +29 -0
  87. package/dist/mcp/server.d.ts.map +1 -0
  88. package/dist/mcp/types.d.ts +60 -0
  89. package/dist/mcp/types.d.ts.map +1 -0
  90. package/dist/prompt/index.d.ts +6 -0
  91. package/dist/prompt/index.d.ts.map +1 -0
  92. package/dist/prompt/prompt-module.d.ts +23 -0
  93. package/dist/prompt/prompt-module.d.ts.map +1 -0
  94. package/dist/prompt/service.d.ts +47 -0
  95. package/dist/prompt/service.d.ts.map +1 -0
  96. package/dist/prompt/stores/file-store.d.ts +36 -0
  97. package/dist/prompt/stores/file-store.d.ts.map +1 -0
  98. package/dist/prompt/stores/memory-store.d.ts +17 -0
  99. package/dist/prompt/stores/memory-store.d.ts.map +1 -0
  100. package/dist/prompt/types.d.ts +68 -0
  101. package/dist/prompt/types.d.ts.map +1 -0
  102. package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
  103. package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
  104. package/dist/rag/chunkers/text-chunker.d.ts +11 -0
  105. package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
  106. package/dist/rag/decorators.d.ts +24 -0
  107. package/dist/rag/decorators.d.ts.map +1 -0
  108. package/dist/rag/index.d.ts +7 -0
  109. package/dist/rag/index.d.ts.map +1 -0
  110. package/dist/rag/rag-module.d.ts +23 -0
  111. package/dist/rag/rag-module.d.ts.map +1 -0
  112. package/dist/rag/service.d.ts +36 -0
  113. package/dist/rag/service.d.ts.map +1 -0
  114. package/dist/rag/types.d.ts +56 -0
  115. package/dist/rag/types.d.ts.map +1 -0
  116. package/dist/vector-store/index.d.ts +6 -0
  117. package/dist/vector-store/index.d.ts.map +1 -0
  118. package/dist/vector-store/stores/memory-store.d.ts +17 -0
  119. package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
  120. package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
  121. package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
  122. package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
  123. package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
  124. package/dist/vector-store/types.d.ts +60 -0
  125. package/dist/vector-store/types.d.ts.map +1 -0
  126. package/dist/vector-store/vector-store-module.d.ts +20 -0
  127. package/dist/vector-store/vector-store-module.d.ts.map +1 -0
  128. package/docs/ai.md +500 -0
  129. package/docs/best-practices.md +83 -8
  130. package/docs/database.md +23 -0
  131. package/docs/guide.md +90 -27
  132. package/docs/migration.md +81 -7
  133. package/docs/security.md +23 -0
  134. package/docs/zh/ai.md +441 -0
  135. package/docs/zh/best-practices.md +43 -0
  136. package/docs/zh/database.md +23 -0
  137. package/docs/zh/guide.md +40 -1
  138. package/docs/zh/migration.md +39 -0
  139. package/docs/zh/security.md +23 -0
  140. package/package.json +2 -2
  141. package/src/ai/ai-module.ts +62 -0
  142. package/src/ai/decorators.ts +30 -0
  143. package/src/ai/errors.ts +71 -0
  144. package/src/ai/index.ts +11 -0
  145. package/src/ai/providers/anthropic-provider.ts +190 -0
  146. package/src/ai/providers/google-provider.ts +179 -0
  147. package/src/ai/providers/ollama-provider.ts +126 -0
  148. package/src/ai/providers/openai-provider.ts +242 -0
  149. package/src/ai/service.ts +155 -0
  150. package/src/ai/tools/tool-executor.ts +38 -0
  151. package/src/ai/tools/tool-registry.ts +91 -0
  152. package/src/ai/types.ts +145 -0
  153. package/src/ai-guard/ai-guard-module.ts +50 -0
  154. package/src/ai-guard/decorators.ts +21 -0
  155. package/src/ai-guard/detectors/content-moderator.ts +80 -0
  156. package/src/ai-guard/detectors/injection-detector.ts +48 -0
  157. package/src/ai-guard/detectors/pii-detector.ts +64 -0
  158. package/src/ai-guard/index.ts +7 -0
  159. package/src/ai-guard/service.ts +100 -0
  160. package/src/ai-guard/types.ts +61 -0
  161. package/src/conversation/conversation-module.ts +63 -0
  162. package/src/conversation/decorators.ts +47 -0
  163. package/src/conversation/index.ts +7 -0
  164. package/src/conversation/service.ts +133 -0
  165. package/src/conversation/stores/database-store.ts +125 -0
  166. package/src/conversation/stores/memory-store.ts +57 -0
  167. package/src/conversation/stores/redis-store.ts +101 -0
  168. package/src/conversation/types.ts +68 -0
  169. package/src/core/cluster.ts +239 -46
  170. package/src/core/index.ts +1 -1
  171. package/src/core/server.ts +91 -78
  172. package/src/embedding/embedding-module.ts +52 -0
  173. package/src/embedding/index.ts +5 -0
  174. package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
  175. package/src/embedding/providers/openai-embedding-provider.ts +47 -0
  176. package/src/embedding/service.ts +55 -0
  177. package/src/embedding/types.ts +27 -0
  178. package/src/index.ts +11 -1
  179. package/src/mcp/decorators.ts +60 -0
  180. package/src/mcp/index.ts +5 -0
  181. package/src/mcp/mcp-module.ts +58 -0
  182. package/src/mcp/registry.ts +72 -0
  183. package/src/mcp/server.ts +164 -0
  184. package/src/mcp/types.ts +63 -0
  185. package/src/prompt/index.ts +5 -0
  186. package/src/prompt/prompt-module.ts +61 -0
  187. package/src/prompt/service.ts +93 -0
  188. package/src/prompt/stores/file-store.ts +135 -0
  189. package/src/prompt/stores/memory-store.ts +82 -0
  190. package/src/prompt/types.ts +84 -0
  191. package/src/rag/chunkers/markdown-chunker.ts +40 -0
  192. package/src/rag/chunkers/text-chunker.ts +30 -0
  193. package/src/rag/decorators.ts +26 -0
  194. package/src/rag/index.ts +6 -0
  195. package/src/rag/rag-module.ts +78 -0
  196. package/src/rag/service.ts +134 -0
  197. package/src/rag/types.ts +47 -0
  198. package/src/vector-store/index.ts +5 -0
  199. package/src/vector-store/stores/memory-store.ts +69 -0
  200. package/src/vector-store/stores/pinecone-store.ts +123 -0
  201. package/src/vector-store/stores/qdrant-store.ts +147 -0
  202. package/src/vector-store/types.ts +77 -0
  203. package/src/vector-store/vector-store-module.ts +50 -0
  204. package/tests/ai/ai-module.test.ts +46 -0
  205. package/tests/ai/ai-service.test.ts +91 -0
  206. package/tests/ai/tool-registry.test.ts +57 -0
  207. package/tests/ai-guard/ai-guard-module.test.ts +23 -0
  208. package/tests/ai-guard/content-moderator.test.ts +65 -0
  209. package/tests/ai-guard/pii-detector.test.ts +41 -0
  210. package/tests/conversation/conversation-module.test.ts +26 -0
  211. package/tests/conversation/conversation-service.test.ts +64 -0
  212. package/tests/conversation/memory-store.test.ts +68 -0
  213. package/tests/core/cluster.test.ts +45 -1
  214. package/tests/embedding/embedding-service.test.ts +55 -0
  215. package/tests/mcp/mcp-server.test.ts +85 -0
  216. package/tests/prompt/prompt-module.test.ts +30 -0
  217. package/tests/prompt/prompt-service.test.ts +74 -0
  218. package/tests/rag/chunkers.test.ts +58 -0
  219. package/tests/rag/rag-service.test.ts +66 -0
  220. package/tests/vector-store/memory-vector-store.test.ts +84 -0
  221. package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
  222. package/tests/perf/optimization.test.ts +0 -182
  223. 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
+ }
@@ -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,5 @@
1
+ export * from './types';
2
+ export * from './vector-store-module';
3
+ export * from './stores/memory-store';
4
+ export * from './stores/pinecone-store';
5
+ export * from './stores/qdrant-store';
@@ -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
+ }