《60天AI学习计划启动 | Day 08: 向量数据库基础 - RAG 的核心技术》

Day 08: 向量数据库基础 - RAG 的核心技术

学习目标


核心学习内容

1. 向量和 Embedding

什么是向量?

  • 数值数组,表示文本的数学表示
  • 相似文本 → 相似向量
  • 高维空间中的点(通常 1536 维)

什么是 Embedding?

  • 将文本转换为向量的过程
  • 保留语义信息
  • 相似文本在向量空间中距离近

示例:

"前端开发" → [0.1, 0.2, -0.3, ..., 0.5]  (1536维向量)
"JavaScript" → [0.12, 0.18, -0.28, ..., 0.48]  (相似向量)
"后端开发" → [0.3, 0.1, -0.1, ..., 0.2]  (不同向量)

2. 向量数据库

作用:

  • 存储向量数据
  • 快速相似度搜索
  • 支持大规模数据

常见方案:

  • Pinecone(云服务)
  • Weaviate(开源)
  • Chroma(轻量级)
  • 自建(内存/文件)

3. 相似度计算

方法:

  • 余弦相似度(常用)
  • 欧氏距离
  • 点积

余弦相似度公式:

similarity = (A · B) / (||A|| * ||B||)
范围:-1 到 1,1 表示完全相同

实践作业

作业1:实现 Embedding 生成

src/services/embedding.js:

import OpenAI from 'openai';
import { logger } from '../utils/logger.js';

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY
});

/**
 * 生成文本的 Embedding
 */
export async function generateEmbedding(text) {
  try {
    const response = await openai.embeddings.create({
      model: 'text-embedding-ada-002',
      input: text
    });

    return response.data[0].embedding;
  } catch (error) {
    logger.error('生成 Embedding 失败:', error);
    throw error;
  }
}

/**
 * 批量生成 Embedding
 */
export async function generateEmbeddings(texts) {
  try {
    const response = await openai.embeddings.create({
      model: 'text-embedding-ada-002',
      input: texts
    });

    return response.data.map(item => item.embedding);
  } catch (error) {
    logger.error('批量生成 Embedding 失败:', error);
    throw error;
  }
}

/**
 * 文本分块(为长文本生成多个 Embedding)
 */
export function chunkText(text, chunkSize = 500, overlap = 50) {
  const chunks = [];
  let start = 0;

  while (start < text.length) {
    const end = Math.min(start + chunkSize, text.length);
    const chunk = text.substring(start, end);
    chunks.push({
      text: chunk,
      start: start,
      end: end
    });
    start = end - overlap; // 重叠部分
  }

  return chunks;
}

作业2:实现向量相似度计算

src/utils/vector-math.js:

/**
 * 计算两个向量的余弦相似度
 */
export function cosineSimilarity(vecA, vecB) {
  if (vecA.length !== vecB.length) {
    throw new Error('向量维度不匹配');
  }

  let dotProduct = 0;
  let normA = 0;
  let normB = 0;

  for (let i = 0; i < vecA.length; i++) {
    dotProduct += vecA[i] * vecB[i];
    normA += vecA[i] * vecA[i];
    normB += vecB[i] * vecB[i];
  }

  const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
  if (magnitude === 0) {
    return 0;
  }

  return dotProduct / magnitude;
}

/**
 * 计算欧氏距离
 */
export function euclideanDistance(vecA, vecB) {
  if (vecA.length !== vecB.length) {
    throw new Error('向量维度不匹配');
  }

  let sum = 0;
  for (let i = 0; i < vecA.length; i++) {
    const diff = vecA[i] - vecB[i];
    sum += diff * diff;
  }

  return Math.sqrt(sum);
}

/**
 * 计算点积
 */
export function dotProduct(vecA, vecB) {
  if (vecA.length !== vecB.length) {
    throw new Error('向量维度不匹配');
  }

  let sum = 0;
  for (let i = 0; i < vecA.length; i++) {
    sum += vecA[i] * vecB[i];
  }

  return sum;
}

作业3:实现简单向量数据库

src/services/vector-db.js:

import fs from 'fs/promises';
import path from 'path';
import { generateEmbedding } from './embedding.js';
import { cosineSimilarity } from '../utils/vector-math.js';
import { logger } from '../utils/logger.js';

/**
 * 简单的内存向量数据库
 */
export class VectorDB {
  constructor() {
    this.vectors = []; // [{ id, text, embedding, metadata }]
    this.dimension = 1536; // OpenAI embedding 维度
  }

  /**
   * 添加文档
   */
  async addDocument(text, metadata = {}) {
    try {
      // 生成 Embedding
      const embedding = await generateEmbedding(text);

      const document = {
        id: this.generateId(),
        text: text,
        embedding: embedding,
        metadata: {
          ...metadata,
          createdAt: new Date().toISOString()
        }
      };

      this.vectors.push(document);
      logger.info(`添加文档: ${document.id}`);

      return document.id;
    } catch (error) {
      logger.error('添加文档失败:', error);
      throw error;
    }
  }

  /**
   * 批量添加文档
   */
  async addDocuments(texts, metadatas = []) {
    const ids = [];
    for (let i = 0; i < texts.length; i++) {
      const id = await this.addDocument(texts[i], metadatas[i] || {});
      ids.push(id);
    }
    return ids;
  }

  /**
   * 相似度搜索
   */
  async search(queryText, topK = 5) {
    try {
      // 生成查询向量
      const queryEmbedding = await generateEmbedding(queryText);

      // 计算相似度
      const results = this.vectors.map(doc => ({
        id: doc.id,
        text: doc.text,
        metadata: doc.metadata,
        similarity: cosineSimilarity(queryEmbedding, doc.embedding)
      }));

      // 按相似度排序
      results.sort((a, b) => b.similarity - a.similarity);

      // 返回 topK
      return results.slice(0, topK);
    } catch (error) {
      logger.error('搜索失败:', error);
      throw error;
    }
  }

  /**
   * 根据 ID 获取文档
   */
  getDocument(id) {
    return this.vectors.find(doc => doc.id === id);
  }

  /**
   * 删除文档
   */
  deleteDocument(id) {
    const index = this.vectors.findIndex(doc => doc.id === id);
    if (index !== -1) {
      this.vectors.splice(index, 1);
      logger.info(`删除文档: ${id}`);
      return true;
    }
    return false;
  }

  /**
   * 获取所有文档
   */
  getAllDocuments() {
    return this.vectors.map(doc => ({
      id: doc.id,
      text: doc.text,
      metadata: doc.metadata
    }));
  }

  /**
   * 清空数据库
   */
  clear() {
    this.vectors = [];
    logger.info('数据库已清空');
  }

  /**
   * 保存到文件
   */
  async saveToFile(filePath) {
    try {
      const data = {
        vectors: this.vectors.map(doc => ({
          id: doc.id,
          text: doc.text,
          embedding: doc.embedding,
          metadata: doc.metadata
        }))
      };

      await fs.writeFile(filePath, JSON.stringify(data, null, 2));
      logger.info(`数据已保存到: ${filePath}`);
    } catch (error) {
      logger.error('保存失败:', error);
      throw error;
    }
  }

  /**
   * 从文件加载
   */
  async loadFromFile(filePath) {
    try {
      const data = await fs.readFile(filePath, 'utf-8');
      const parsed = JSON.parse(data);

      this.vectors = parsed.vectors || [];
      logger.info(`从文件加载了 ${this.vectors.length} 个文档`);
    } catch (error) {
      if (error.code === 'ENOENT') {
        logger.warn('文件不存在,创建新数据库');
        this.vectors = [];
      } else {
        logger.error('加载失败:', error);
        throw error;
      }
    }
  }

  /**
   * 生成唯一 ID
   */
  generateId() {
    return `doc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  /**
   * 获取统计信息
   */
  getStats() {
    return {
      totalDocuments: this.vectors.length,
      dimension: this.dimension
    };
  }
}

// 单例
export const vectorDB = new VectorDB();

作业4:实现文档处理服务

src/services/document-processor.js:

import { chunkText } from './embedding.js';
import { vectorDB } from './vector-db.js';
import { logger } from '../utils/logger.js';

/**
 * 文档处理器
 */
export class DocumentProcessor {
  /**
   * 处理并存储文档
   */
  async processDocument(text, metadata = {}) {
    try {
      // 文本分块
      const chunks = chunkText(text, 500, 50);
      logger.info(`文档分块: ${chunks.length} 个块`);

      // 为每个块生成 Embedding 并存储
      const ids = [];
      for (let i = 0; i < chunks.length; i++) {
        const chunk = chunks[i];
        const id = await vectorDB.addDocument(chunk.text, {
          ...metadata,
          chunkIndex: i,
          totalChunks: chunks.length,
          originalLength: text.length
        });
        ids.push(id);
      }

      return {
        documentId: ids[0], // 返回第一个块的 ID 作为文档 ID
        chunkIds: ids,
        totalChunks: chunks.length
      };
    } catch (error) {
      logger.error('处理文档失败:', error);
      throw error;
    }
  }

  /**
   * 处理文件(支持文本文件)
   */
  async processFile(filePath, metadata = {}) {
    try {
      const fs = await import('fs/promises');
      const text = await fs.readFile(filePath, 'utf-8');
      
      return await this.processDocument(text, {
        ...metadata,
        source: filePath,
        fileName: path.basename(filePath)
      });
    } catch (error) {
      logger.error('处理文件失败:', error);
      throw error;
    }
  }

  /**
   * 搜索相关文档
   */
  async searchRelevantDocuments(query, topK = 5) {
    try {
      const results = await vectorDB.search(query, topK);
      
      // 合并同一文档的多个块
      const documentMap = new Map();
      
      for (const result of results) {
        const docId = result.metadata.source || result.id;
        if (!documentMap.has(docId)) {
          documentMap.set(docId, {
            id: docId,
            text: result.text,
            similarity: result.similarity,
            metadata: result.metadata,
            chunks: [result]
          });
        } else {
          const doc = documentMap.get(docId);
          doc.chunks.push(result);
          // 使用最高相似度
          if (result.similarity > doc.similarity) {
            doc.similarity = result.similarity;
          }
        }
      }

      return Array.from(documentMap.values())
        .sort((a, b) => b.similarity - a.similarity);
    } catch (error) {
      logger.error('搜索文档失败:', error);
      throw error;
    }
  }
}

export const documentProcessor = new DocumentProcessor();

作业5:创建向量数据库 API

src/routes/vector-db.js:

import express from 'express';
import { vectorDB } from '../services/vector-db.js';
import { documentProcessor } from '../services/document-processor.js';
import { logger } from '../utils/logger.js';

export const vectorDBRouter = express.Router();

// POST /api/vector-db/add - 添加文档
vectorDBRouter.post('/add', async (req, res) => {
  try {
    const { text, metadata = {} } = req.body;

    if (!text) {
      return res.status(400).json({
        success: false,
        error: '文本内容不能为空'
      });
    }

    const id = await vectorDB.addDocument(text, metadata);

    res.json({
      success: true,
      data: { id }
    });
  } catch (error) {
    logger.error('添加文档错误:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// POST /api/vector-db/process - 处理文档(分块)
vectorDBRouter.post('/process', async (req, res) => {
  try {
    const { text, metadata = {} } = req.body;

    if (!text) {
      return res.status(400).json({
        success: false,
        error: '文本内容不能为空'
      });
    }

    const result = await documentProcessor.processDocument(text, metadata);

    res.json({
      success: true,
      data: result
    });
  } catch (error) {
    logger.error('处理文档错误:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// POST /api/vector-db/search - 搜索
vectorDBRouter.post('/search', async (req, res) => {
  try {
    const { query, topK = 5 } = req.body;

    if (!query) {
      return res.status(400).json({
        success: false,
        error: '查询内容不能为空'
      });
    }

    const results = await vectorDB.search(query, topK);

    res.json({
      success: true,
      data: results
    });
  } catch (error) {
    logger.error('搜索错误:', error);
    res.status(500).json({
      success: false,
      error: error.message
    });
  }
});

// GET /api/vector-db/stats - 获取统计信息
vectorDBRouter.get('/stats', (req, res) => {
  const stats = vectorDB.getStats();
  res.json({
    success: true,
    data: stats
  });
});

// DELETE /api/vector-db/:id - 删除文档
vectorDBRouter.delete('/:id', (req, res) => {
  const { id } = req.params;
  const deleted = vectorDB.deleteDocument(id);
  
  res.json({
    success: deleted,
    message: deleted ? '文档已删除' : '文档不存在'
  });
});

// POST /api/vector-db/clear - 清空数据库
vectorDBRouter.post('/clear', (req, res) => {
  vectorDB.clear();
  res.json({
    success: true,
    message: '数据库已清空'
  });
});

作业6:测试向量数据库

test-vector-db.js:

import { vectorDB } from './src/services/vector-db.js';
import { documentProcessor } from './src/services/document-processor.js';

async function testVectorDB() {
  console.log('=== 测试向量数据库 ===\n');

  // 1. 添加文档
  console.log('1. 添加文档...');
  const doc1 = await vectorDB.addDocument(
    '前端开发是构建用户界面的技术,主要使用 HTML、CSS 和 JavaScript。',
    { category: 'frontend' }
  );
  console.log('文档1 ID:', doc1);

  const doc2 = await vectorDB.addDocument(
    '后端开发是服务器端编程,处理业务逻辑和数据库操作。',
    { category: 'backend' }
  );
  console.log('文档2 ID:', doc2);

  const doc3 = await vectorDB.addDocument(
    'JavaScript 是一种动态编程语言,用于前端和后端开发。',
    { category: 'javascript' }
  );
  console.log('文档3 ID:', doc3);

  // 2. 搜索
  console.log('\n2. 搜索 "前端技术"...');
  const results1 = await vectorDB.search('前端技术', 3);
  results1.forEach((result, index) => {
    console.log(`${index + 1}. 相似度: ${result.similarity.toFixed(4)}`);
    console.log(`   文本: ${result.text.substring(0, 50)}...`);
  });

  // 3. 处理长文档
  console.log('\n3. 处理长文档...');
  const longText = `
    人工智能(AI)是计算机科学的一个分支,致力于创建能够执行通常需要人类智能的任务的系统。
    机器学习是 AI 的一个子集,它使计算机能够从数据中学习,而无需明确编程。
    深度学习是机器学习的一个子集,使用神经网络来模拟人脑的工作方式。
    自然语言处理(NLP)是 AI 的一个分支,专注于计算机与人类语言之间的交互。
    计算机视觉是 AI 的另一个分支,使计算机能够理解和解释视觉信息。
  `;

  const processed = await documentProcessor.processDocument(
    longText,
    { title: 'AI 基础知识' }
  );
  console.log(`文档ID: ${processed.documentId}`);
  console.log(`分块数: ${processed.totalChunks}`);

  // 4. 搜索长文档
  console.log('\n4. 搜索 "机器学习"...');
  const results2 = await documentProcessor.searchRelevantDocuments('机器学习', 2);
  results2.forEach((result, index) => {
    console.log(`${index + 1}. 相似度: ${result.similarity.toFixed(4)}`);
    console.log(`   文本: ${result.text.substring(0, 100)}...`);
  });

  // 5. 统计信息
  console.log('\n5. 统计信息:');
  const stats = vectorDB.getStats();
  console.log(stats);
}

testVectorDB().catch(console.error);

遇到的问题

问题1:Embedding 生成慢

解决方案:

// 批量生成,减少 API 调用
const embeddings = await generateEmbeddings([text1, text2, text3]);

// 缓存已生成的 Embedding
const embeddingCache = new Map();
if (embeddingCache.has(text)) {
  return embeddingCache.get(text);
}

问题2:向量维度不匹配

解决方案:

// 检查维度
if (vecA.length !== vecB.length) {
  throw new Error(`维度不匹配: ${vecA.length} vs ${vecB.length}`);
}

问题3:内存占用过大

解决方案:

// 使用文件存储
await vectorDB.saveToFile('./data/vectors.json');

// 定期清理旧数据
if (this.vectors.length > 10000) {
  this.vectors = this.vectors.slice(-5000);
}

学习总结

今日收获

  1. ✅ 理解向量和 Embedding 概念
  2. ✅ 掌握文档向量化
  3. ✅ 实现相似度搜索
  4. ✅ 构建简单向量数据库
  5. ✅ 为 RAG 应用打基础

关键知识点

  • Embedding 是文本的数学表示,保留语义信息
  • 相似度搜索,找到最相关的文档
  • 文本分块,处理长文档
  • 向量数据库,存储和检索向量
  • RAG 基础,检索增强生成的核心

技术要点

  • 余弦相似度:常用相似度计算方法
  • 文本分块:长文档需要分块处理
  • 批量处理:提高效率
  • 持久化存储:保存向量数据

明日计划

明天将学习:

期待明天的学习! 🚀


参考资源


代码仓库

项目已更新:

  • ✅ Embedding 生成服务
  • ✅ 向量相似度计算
  • ✅ 简单向量数据库
  • ✅ 文档处理服务
  • ✅ API 接口

GitHub 提交: Day 08 - 向量数据库基础


标签: #AI学习 #向量数据库 #Embedding #RAG #学习笔记


写在最后

今天学习了向量数据库的基础知识,这是 RAG 应用的核心技术。
通过向量化文档和相似度搜索,可以让 AI 基于特定文档回答问题。
明天将学习完整的 RAG 实现,打造智能文档助手!

继续加油! 💪


快速检查清单

完成这些,第八天就达标了!

posted @ 2025-12-16 15:13  XiaoZhengTou  阅读(1)  评论(0)    收藏  举报