簡介
對文件進行分塊後,需要將文字轉換為向量(嵌入)並將其儲存到向量儲存中以進行相似性搜尋。本文實現了嵌入管道和向量存儲抽象。
1. 嵌入接口
// packages/core/src/rag/embedding.ts
export interface EmbeddingProvider {
embed(texts: string[]): Promise<number[][]>;
readonly dimensions: number;
readonly model: string;
}
export class OpenAIEmbedding implements EmbeddingProvider {
readonly dimensions = 1536;
readonly model = 'text-embedding-3-small';
private client: OpenAI;
constructor(apiKey: string) {
this.client = new OpenAI({ apiKey });
}
async embed(texts: string[]): Promise<number[][]> {
// Batch processing — max 2048 texts per request
const batches: string[][] = [];
for (let i = 0; i < texts.length; i += 2048) {
batches.push(texts.slice(i, i + 2048));
}
const allEmbeddings: number[][] = [];
for (const batch of batches) {
const response = await this.client.embeddings.create({
model: this.model,
input: batch,
});
allEmbeddings.push(...response.data.map(d => d.embedding));
}
return allEmbeddings;
}
}
2.向量儲存抽象
// packages/core/src/rag/vector-store.ts
export interface VectorStore {
upsert(documents: VectorDocument[]): Promise<void>;
search(query: number[], options: SearchOptions): Promise<SearchResult[]>;
delete(ids: string[]): Promise<void>;
createCollection(name: string): Promise<void>;
}
export interface VectorDocument {
id: string;
vector: number[];
content: string;
metadata: Record<string, unknown>;
}
export interface SearchResult {
id: string;
content: string;
score: number;
metadata: Record<string, unknown>;
}
export interface SearchOptions {
collection: string;
topK: number;
filter?: Record<string, unknown>;
minScore?: number;
}
3. MongoDB Atlas向量搜尋
// packages/core/src/rag/stores/mongodb-vector-store.ts
export class MongoDBVectorStore implements VectorStore {
private db: Db;
constructor(db: Db) {
this.db = db;
}
async createCollection(name: string): Promise<void> {
const collection = this.db.collection(name);
// Create vector search index
await collection.createSearchIndex({
name: 'vector_index',
definition: {
mappings: {
dynamic: true,
fields: {
vector: {
type: 'knnVector',
dimensions: 1536,
similarity: 'cosine',
},
},
},
},
});
}
async upsert(documents: VectorDocument[]): Promise<void> {
const ops = documents.map(doc => ({
updateOne: {
filter: { _id: doc.id },
update: {
$set: {
vector: doc.vector,
content: doc.content,
metadata: doc.metadata,
updatedAt: new Date(),
},
},
upsert: true,
},
}));
await this.db.collection('vectors').bulkWrite(ops);
}
async search(query: number[], options: SearchOptions): Promise<SearchResult[]> {
const results = await this.db.collection(options.collection).aggregate([
{
$vectorSearch: {
index: 'vector_index',
path: 'vector',
queryVector: query,
numCandidates: options.topK * 10,
limit: options.topK,
filter: options.filter,
},
},
{
$project: {
content: 1,
metadata: 1,
score: { $meta: 'vectorSearchScore' },
},
},
]).toArray();
return results
.filter(r => !options.minScore || r.score >= options.minScore)
.map(r => ({
id: r._id.toString(),
content: r.content,
score: r.score,
metadata: r.metadata,
}));
}
async delete(ids: string[]): Promise<void> {
await this.db.collection('vectors').deleteMany({
_id: { $in: ids },
});
}
}
4. 批次嵌入管道
export async function embedAndStore(
chunks: DocumentChunk[],
embedding: EmbeddingProvider,
vectorStore: VectorStore,
collection: string,
): Promise<void> {
// Embed in batches of 100
const BATCH_SIZE = 100;
for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
const batch = chunks.slice(i, i + BATCH_SIZE);
const texts = batch.map(c => c.content);
console.log(`Embedding batch ${i / BATCH_SIZE + 1}/${Math.ceil(chunks.length / BATCH_SIZE)}`);
const vectors = await embedding.embed(texts);
const documents: VectorDocument[] = batch.map((chunk, j) => ({
id: chunk.id,
vector: vectors[j],
content: chunk.content,
metadata: chunk.metadata,
}));
await vectorStore.upsert(documents);
}
}
5. 總結
- Embedding Provider — OpenAI、Cohere、本地模型的抽象
- 向量儲存 — MongoDB Atlas 搜尋或 ChromaDB
- 批次 — 批次處理區塊以避免速率限制
- 餘弦相似度 - 文字嵌入最常見的距離測量
下一篇文章: RAG 引擎 — 將檢索整合到代理流程中。