<!-- title: Claude Code语义搜索全链路实战 -->

# Claude Code语义搜索全链路实战：从Embedding到向量检索

> 当代码库突破十万行，传统关键词搜索开始力不从心。本文将带你从零构建一套完整的语义搜索系统，让Claude Code真正"理解"你的代码。

## 引言：为什么Claude Code需要语义搜索？

Claude Code内置了Grep、Glob等文件搜索工具，这些工具基于字符串匹配和文件名模式工作——对于小型项目足够好用，但在大型代码库中存在明显局限：

- **同义词盲区**：搜索"用户认证"找不到`authenticate()`函数
- **语义鸿沟**：搜索"数据库连接"无法定位到`createPool()`或`KnexConfig`
- **上下文缺失**：关键词匹配不知道`userService`和`UserRepository`是关联组件

语义搜索通过将代码转化为向量嵌入（Embedding），在语义空间中计算相似度，从根本上解决了这些问题。

```mermaid
graph LR
    A[自然语言查询] --> B[查询向量化]
    C[代码库] --> D[代码分块 + 向量化]
    D --> E[(向量数据库)]
    B --> F[相似度检索]
    E --> F
    F --> G[语义相关代码]

    style A fill:#e1f5fe
    style G fill:#c8e6c9
    style E fill:#fff3e0
```

## 核心概念速览

### 什么是文本嵌入（Embedding）？

文本嵌入是将文本转换为高维数值向量的过程。语义相近的文本在向量空间中距离更近。例如：

| 文本A | 文本B | 语义关系 | 向量距离 |
|-------|-------|---------|---------|
| "用户登录验证" | "authenticate user" | 相似 | 近 |
| "数据库连接池" | "createPool(config)" | 相似 | 近 |
| "发送邮件" | "矩阵运算" | 无关 | 远 |

当前主流的代码嵌入模型包括：

- **Voyage AI Code Embeddings**：Anthropic推荐的嵌入模型提供商，专为代码优化
- **OpenAI text-embedding-3-small/large**：通用性强，代码效果也不错
- **Ollama本地模型**（如`nomic-embed-text`）：完全离线，零成本，隐私友好

### 向量数据库选型

| 数据库 | 类型 | 特点 | 适用场景 |
|--------|------|------|---------|
| ChromaDB | 嵌入式 | Python原生，零配置 | 快速原型、本地开发 |
| Milvus/Zilliz | 分布式 | 高性能，支持混合检索 | 生产级部署 |
| Pinecone | 云托管 | 全托管，免运维 | 快速上线 |
| Qdrant | 独立部署 | Rust实现，性能优秀 | 自托管生产环境 |
| SQLite-VSS | 嵌入式 | SQLite扩展，轻量 | 单机轻量场景 |

```mermaid
graph TD
    subgraph "选型决策树"
        A{项目规模？} -->|个人/小团队| B{是否需要离线？}
        A -->|企业级| C{是否接受云托管？}
        B -->|是| D[ChromaDB / SQLite-VSS]
        B -->|否| E[Pinecone免费层]
        C -->|是| F[Zilliz Cloud / Pinecone]
        C -->|否| G[Milvus自托管 / Qdrant]
    end

    style D fill:#c8e6c9
    style E fill:#c8e6c9
    style F fill:#bbdefb
    style G fill:#bbdefb
```

## 实战一：本地语义代码搜索工具

我们先从最实用的场景开始——为Claude Code构建一个本地语义搜索工具。

### 第一步：安装依赖

```bash
# 创建项目目录
mkdir claude-semantic-search && cd claude-semantic-search

# 初始化项目
npm init -y

# 安装核心依赖
npm install chromadb openai ollama-ai-provider @xenova/transformers
npm install -D typescript @types/node tsx
```

### 第二步：代码解析与分块

代码分块（Chunking）是语义搜索中最关键的预处理步骤。分块策略直接影响检索质量：

```typescript
// src/chunker.ts
import * as fs from 'fs';
import * as path from 'path';

interface CodeChunk {
  filePath: string;
  content: string;
  startLine: number;
  endLine: number;
  type: 'function' | 'class' | 'module' | 'block';
  name?: string;
}

// AST级别的代码分块（以TypeScript/JavaScript为例）
export function chunkFile(filePath: string): CodeChunk[] {
  const content = fs.readFileSync(filePath, 'utf-8');
  const lines = content.split('\n');
  const chunks: CodeChunk[] = [];

  // 函数/类级别分块策略
  const functionRegex = /(?:export\s+)?(?:async\s+)?function\s+(\w+)/g;
  const classRegex = /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/g;
  const arrowRegex = /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\(/g;

  const extractBlock = (regex: RegExp, type: CodeChunk['type']) => {
    let match;
    while ((match = regex.exec(content)) !== null) {
      const startLine = content.substring(0, match.index).split('\n').length;
      const block = extractBlockContent(lines, startLine - 1);
      if (block) {
        chunks.push({
          filePath,
          content: block.content,
          startLine,
          endLine: block.endLine,
          type,
          name: match[1],
        });
      }
    }
  };

  extractBlock(classRegex, 'class');
  extractBlock(functionRegex, 'function');
  extractBlock(arrowRegex, 'function');

  // 如果没有提取到任何块，按固定大小分块
  if (chunks.length === 0) {
    const CHUNK_SIZE = 50; // 行数
    for (let i = 0; i < lines.length; i += CHUNK_SIZE) {
      const endLine = Math.min(i + CHUNK_SIZE, lines.length);
      chunks.push({
        filePath,
        content: lines.slice(i, endLine).join('\n'),
        startLine: i + 1,
        endLine,
        type: 'block',
      });
    }
  }

  return chunks;
}

// 提取花括号匹配的完整块
function extractBlockContent(
  lines: string[],
  startIdx: number
): { content: string; endLine: number } | null {
  let braceCount = 0;
  let foundFirst = false;
  let endIdx = startIdx;

  for (let i = startIdx; i < lines.length; i++) {
    for (const ch of lines[i]) {
      if (ch === '{') {
        braceCount++;
        foundFirst = true;
      } else if (ch === '}') {
        braceCount--;
      }
    }
    endIdx = i;
    if (foundFirst && braceCount === 0) break;
  }

  if (!foundFirst) return null;

  return {
    content: lines.slice(startIdx, endIdx + 1).join('\n'),
    endLine: endIdx + 1,
  };
}
```

### 第三步：生成嵌入向量

我们提供两种方案——使用Ollama本地模型（零成本、离线）或Voyage AI API（高质量）：

```typescript
// src/embedder.ts

// 方案A：使用Ollama本地嵌入（推荐用于开发/测试）
export async function embedWithOllama(texts: string[]): Promise<number[][]> {
  const response = await fetch('http://localhost:11434/api/embed', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      model: 'nomic-embed-text',
      input: texts,
    }),
  });

  const data = await response.json();
  return data.embeddings;
}

// 方案B：使用Voyage AI（Anthropic推荐，高质量代码嵌入）
export async function embedWithVoyage(
  texts: string[],
  apiKey: string
): Promise<number[][]> {
  const response = await fetch('https://api.voyageai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      input: texts,
      model: 'voyage-code-3',
      input_type: 'document',
    }),
  });

  const data = await response.json();
  return data.data.map((item: any) => item.embedding);
}
```

> **关于Anthropic嵌入服务的说明**：Anthropic的嵌入API底层使用Voyage AI作为提供商。如果你已有Anthropic API密钥，可以通过Anthropic的嵌入端点调用，也可以直接使用Voyage AI的API。两者在代码嵌入场景下效果相当。

### 第四步：构建向量索引

```typescript
// src/indexer.ts
import { ChromaClient } from 'chromadb';
import { chunkFile } from './chunker.js';
import { embedWithOllama, embedWithVoyage } from './embedder.js';
import * as fs from 'fs';
import * as path from 'path';

const client = new ChromaClient({ path: 'http://localhost:8000' });

const SUPPORTED_EXTENSIONS = new Set([
  '.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.java',
  '.rb', '.php', '.c', '.cpp', '.h', '.hpp', '.cs', '.swift',
]);

export async function indexCodebase(
  rootDir: string,
  collectionName: string = 'codebase'
) {
  // 获取或创建集合
  const collection = await client.getOrCreateCollection({
    name: collectionName,
    metadata: { 'hnsw:space': 'cosine' },
  });

  // 递归收集所有代码文件
  const files = collectFiles(rootDir);
  console.log(`发现 ${files.length} 个代码文件`);

  let totalChunks = 0;

  for (const file of files) {
    const chunks = chunkFile(file);
    if (chunks.length === 0) continue;

    const contents = chunks.map((c) => c.content);
    const embeddings = await embedWithOllama(contents);

    const ids = chunks.map(
      (_, i) => `${path.relative(rootDir, file)}::${i}`
    );
    const metadatas = chunks.map((c) => ({
      filePath: path.relative(rootDir, c.filePath),
      startLine: c.startLine,
      endLine: c.endLine,
      type: c.type,
      name: c.name || '',
    }));

    await collection.add({
      ids,
      embeddings,
      documents: contents,
      metadatas,
    });

    totalChunks += chunks.length;
    console.log(`  索引: ${path.relative(rootDir, file)} (${chunks.length} 块)`);
  }

  console.log(`索引完成！共 ${totalChunks} 个代码块`);
  return { fileCount: files.length, chunkCount: totalChunks };
}

function collectFiles(rootDir: string): string[] {
  const files: string[] = [];
  const ignoreDirs = new Set([
    'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
    'vendor', '.venv', 'venv',
  ]);

  function walk(dir: string) {
    const entries = fs.readdirSync(dir, { withFileTypes: true });
    for (const entry of entries) {
      const fullPath = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        if (!ignoreDirs.has(entry.name)) walk(fullPath);
      } else if (SUPPORTED_EXTENSIONS.has(path.extname(entry.name))) {
        files.push(fullPath);
      }
    }
  }

  walk(rootDir);
  return files;
}
```

### 第五步：语义检索

```typescript
// src/search.ts
import { ChromaClient } from 'chromadb';
import { embedWithOllama } from './embedder.js';

const client = new ChromaClient({ path: 'http://localhost:8000' });

export interface SearchResult {
  filePath: string;
  startLine: number;
  endLine: number;
  type: string;
  name: string;
  content: string;
  score: number;
}

export async function semanticSearch(
  query: string,
  options: {
    collectionName?: string;
    topK?: number;
    fileFilter?: string;
  } = {}
): Promise<SearchResult[]> {
  const {
    collectionName = 'codebase',
    topK = 10,
    fileFilter,
  } = options;

  const collection = await client.getCollection({ name: collectionName });

  // 查询向量化
  const [queryEmbedding] = await embedWithOllama([query]);

  // 构建过滤条件
  const where = fileFilter
    ? { filePath: { $contains: fileFilter } }
    : undefined;

  // 向量检索
  const results = await collection.query({
    queryEmbeddings: queryEmbedding,
    nResults: topK,
    where,
    include: ['documents', 'metadatas', 'distances'],
  });

  // 格式化结果
  return results.ids[0].map((id, i) => ({
    filePath: (results.metadatas?.[0]?.[i] as any)?.filePath || '',
    startLine: (results.metadatas?.[0]?.[i] as any)?.startLine || 0,
    endLine: (results.metadatas?.[0]?.[i] as any)?.endLine || 0,
    type: (results.metadatas?.[0]?.[i] as any)?.type || '',
    name: (results.metadatas?.[0]?.[i] as any)?.name || '',
    content: results.documents?.[0]?.[i] || '',
    score: 1 - (results.distances?.[0]?.[i] || 0),
  }));
}

// 增量索引更新
export async function updateIndex(
  changedFiles: string[],
  rootDir: string,
  collectionName: string = 'codebase'
) {
  const collection = await client.getCollection({ name: collectionName });

  for (const file of changedFiles) {
    // 删除旧索引
    const prefix = path.relative(rootDir, file);
    await collection.delete({
      where: { filePath: prefix },
    });

    // 重新索引
    const chunks = chunkFile(file);
    if (chunks.length === 0) continue;

    const contents = chunks.map((c) => c.content);
    const embeddings = await embedWithOllama(contents);

    await collection.add({
      ids: chunks.map((_, i) => `${prefix}::${i}`),
      embeddings,
      documents: contents,
      metadatas: chunks.map((c) => ({
        filePath: prefix,
        startLine: c.startLine,
        endLine: c.endLine,
        type: c.type,
        name: c.name || '',
      })),
    });
  }
}
```

### 第六步：封装为MCP Server接入Claude Code

为了将语义搜索能力直接集成到Claude Code中，我们构建一个MCP Server：

```typescript
// src/mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { semanticSearch, indexCodebase, updateIndex } from './index.js';

const server = new Server(
  { name: 'semantic-code-search', version: '1.0.0' },
  { capabilities: { tools: {} } }
);

// 注册工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'semantic_search',
      description:
        '使用自然语言语义搜索代码。比关键词搜索更智能，能理解代码含义。',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: '自然语言查询，描述你想找的代码功能',
          },
          top_k: {
            type: 'number',
            description: '返回结果数量，默认10',
            default: 10,
          },
          file_filter: {
            type: 'string',
            description: '可选，按文件路径过滤',
          },
        },
        required: ['query'],
      },
    },
    {
      name: 'index_codebase',
      description: '为代码库建立语义索引。首次使用或代码大幅变更时调用。',
      inputSchema: {
        type: 'object',
        properties: {
          root_dir: {
            type: 'string',
            description: '代码库根目录路径',
          },
        },
        required: ['root_dir'],
      },
    },
  ],
}));

// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case 'semantic_search': {
      const results = await semanticSearch(args!.query as string, {
        topK: (args!.top_k as number) || 10,
        fileFilter: args!.file_filter as string,
      });

      const formatted = results
        .map(
          (r, i) =>
            `### 结果 ${i + 1} (${(r.score * 100).toFixed(1)}% 匹配)\n` +
            `文件: ${r.filePath}:${r.startLine}-${r.endLine}\n` +
            `${r.type === 'function' ? '函数' : r.type === 'class' ? '类' : '代码块'}${r.name ? `: ${r.name}` : ''}\n` +
            '```\n' + r.content + '\n```'
        )
        .join('\n\n');

      return {
        content: [{ type: 'text', text: formatted || '未找到相关代码' }],
      };
    }

    case 'index_codebase': {
      const stats = await indexCodebase(args!.root_dir as string);
      return {
        content: [
          {
            type: 'text',
            text: `索引完成：${stats.fileCount} 个文件，${stats.chunkCount} 个代码块`,
          },
        ],
      };
    }

    default:
      throw new Error(`未知工具: ${name}`);
  }
});

// 启动服务
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('语义代码搜索 MCP Server 已启动');
}

main().catch(console.error);
```

### 配置Claude Code使用语义搜索

在`.claude/settings.json`中添加MCP Server配置：

```json
{
  "mcpServers": {
    "semantic-search": {
      "command": "node",
      "args": ["--experimental-specifier-resolution=node", "dist/mcp-server.js"],
      "env": {
        "CHROMA_URL": "http://localhost:8000",
        "OLLAMA_URL": "http://localhost:11434"
      }
    }
  }
}
```

配置完成后，在Claude Code中可以直接使用语义搜索：

```
> 帮我找到处理用户支付回调的所有相关代码

# Claude Code 会调用 semantic_search 工具
# 返回：PaymentService.handleWebhook, verifySignature, processRefund 等
# 这些函数可能不包含"回调"关键词，但语义上高度相关
```

## 实战二：混合检索策略——BM25 + 向量搜索

纯向量搜索在某些场景下不如关键词搜索精准（比如搜索特定变量名）。混合检索结合了两者优势：

```mermaid
graph TB
    Q[用户查询] --> V[向量检索]
    Q --> K[BM25关键词检索]
    V --> R1[向量结果 + 相似度分数]
    K --> R2[关键词结果 + BM25分数]
    R1 --> M[RRF互惠排序融合]
    R2 --> M
    M --> F[最终排序结果]

    style Q fill:#e1f5fe
    style M fill:#fff9c4
    style F fill:#c8e6c9
```

```typescript
// src/hybrid-search.ts
import { semanticSearch, SearchResult } from './search.js';

interface BM25Result {
  filePath: string;
  startLine: number;
  content: string;
  score: number;
}

// 简易BM25实现
function simpleBM25(
  query: string,
  chunks: { content: string; filePath: string; startLine: number }[]
): BM25Result[] {
  const terms = query.toLowerCase().split(/\s+/);
  const avgDocLen =
    chunks.reduce((sum, c) => sum + c.content.split(/\s+/).length, 0) /
    chunks.length;
  const k1 = 1.5;
  const b = 0.75;

  return chunks
    .map((chunk) => {
      const docTerms = chunk.content.toLowerCase().split(/\s+/);
      const docLen = docTerms.length;
      const tf = terms.map((t) => docTerms.filter((d) => d === t).length);
      const idf = terms.map(
        (t) =>
          Math.log(
            (chunks.length -
              chunks.filter((c) =>
                c.content.toLowerCase().includes(t)
              ).length +
              0.5) /
              (chunks.filter((c) =>
                c.content.toLowerCase().includes(t)
              ).length +
                0.5)
          )
      );
      const score = tf.reduce(
        (sum, f, i) =>
          sum +
          idf[i] *
            ((f * (k1 + 1)) / (f + k1 * (1 - b + b * (docLen / avgDocLen)))),
        0
      );
      return { ...chunk, score };
    })
    .sort((a, b) => b.score - a.score);
}

// RRF（Reciprocal Rank Fusion）融合算法
export function reciprocalRankFusion(
  vectorResults: SearchResult[],
  bm25Results: BM25Result[],
  k: number = 60
): (SearchResult & { fusedScore: number })[] {
  const scoreMap = new Map<string, { vector?: number; bm25?: number; data: any }>();

  vectorResults.forEach((r, i) => {
    const key = `${r.filePath}:${r.startLine}`;
    scoreMap.set(key, { vector: 1 / (k + i + 1), data: r });
  });

  bm25Results.forEach((r, i) => {
    const key = `${r.filePath}:${r.startLine}`;
    const existing = scoreMap.get(key);
    if (existing) {
      existing.bm25 = 1 / (k + i + 1);
    } else {
      scoreMap.set(key, {
        bm25: 1 / (k + i + 1),
        data: {
          filePath: r.filePath,
          startLine: r.startLine,
          endLine: 0,
          content: r.content,
          type: 'block',
          name: '',
          score: 0,
        },
      });
    }
  });

  return Array.from(scoreMap.values())
    .map(({ vector = 0, bm25 = 0, data }) => ({
      ...data,
      fusedScore: vector + bm25,
    }))
    .sort((a, b) => b.fusedScore - a.fusedScore);
}
```

## 实战三：利用现有开源工具快速集成

如果你不想从零构建，以下是两个可以直接使用的开源工具：

### claude-context（Zilliz出品）

[claude-context](https://github.com/zilliztech/claude-context) 是Zilliz团队开发的MCP插件，开箱即用的语义代码搜索工具：

```bash
# 使用uvx安装并运行
uvx claude-context --project /path/to/your/project
```

它支持混合检索（BM25 + 稠密向量），使用Zilliz Cloud作为向量后端。配置方式：

```json
{
  "mcpServers": {
    "claude-context": {
      "command": "uvx",
      "args": ["claude-context", "--project", "/path/to/your/project"],
      "env": {
        "ZILLIZ_CLOUD_URI": "https://your-instance.zillizcloud.com",
        "ZILLIZ_CLOUD_TOKEN": "your-token"
      }
    }
  }
}
```

### GrepAI——本地语义搜索

[GrepAI](https://github.com/user/grepai) 是一个完全本地的语义代码搜索方案，使用Ollama嵌入模型：

```bash
# 安装
pip install grepai

# 首先确保Ollama运行中
ollama pull nomic-embed-text

# 在项目中使用
grepai search "处理HTTP请求的错误重试逻辑" ./src
```

GrepAI据称可以节省约27%的Claude Code使用成本——因为它让Claude Code更快找到相关代码，减少了来回搜索的Token消耗。

## 架构设计：生产级语义搜索系统

```mermaid
graph TB
    subgraph "索引管道"
        A[Git Hook / CI触发] --> B[变更文件检测]
        B --> C[增量代码分块]
        C --> D[并行嵌入生成]
        D --> E[向量数据库写入]
    end

    subgraph "查询管道"
        F[Claude Code查询] --> G[MCP Server接收]
        G --> H{查询类型判断}
        H -->|语义查询| I[向量检索]
        H -->|精确匹配| J[BM25检索]
        H -->|混合| K[双路检索 + RRF融合]
        I --> L[结果重排序]
        J --> L
        K --> L
        L --> M[上下文窗口优化]
        M --> N[返回Claude Code]
    end

    subgraph "存储层"
        E --> O[(向量数据库<br/>ChromaDB/Milvus)]
        P[(元数据存储<br/>文件路径/行号/类型)]
    end

    style A fill:#e1f5fe
    style F fill:#e1f5fe
    style N fill:#c8e6c9
```

### Git Hook自动增量索引

将索引更新集成到开发工作流中，确保搜索结果始终最新：

```bash
#!/bin/bash
# .git/hooks/post-commit
# 每次提交后自动更新语义索引

CHANGED_FILES=$(git diff --name-only HEAD~1 HEAD | grep -E '\.(ts|tsx|js|jsx|py|go|rs)$')

if [ -n "$CHANGED_FILES" ]; then
  echo "更新语义索引: $CHANGED_FILES"
  node /path/to/claude-semantic-search/dist/update-index.js $CHANGED_FILES
fi
```

### 上下文窗口优化策略

当检索结果过多时，需要智能裁剪以适配Claude的上下文窗口：

```typescript
// src/context-optimizer.ts

const MAX_CONTEXT_TOKENS = 8000; // 预留给代码上下文的Token预算

export function optimizeContext(
  results: SearchResult[],
  maxTokens: number = MAX_CONTEXT_TOKENS
): SearchResult[] {
  let usedTokens = 0;
  const selected: SearchResult[] = [];

  // 按相关度排序后贪心选择
  for (const result of results) {
    const estimatedTokens = Math.ceil(result.content.length / 3.5);
    if (usedTokens + estimatedTokens > maxTokens) {
      // 如果完整块放不下，尝试截断
      const remaining = maxTokens - usedTokens;
      if (remaining > 200) {
        selected.push({
          ...result,
          content: result.content.substring(0, remaining * 3.5) + '\n// ... (截断)',
        });
      }
      break;
    }
    selected.push(result);
    usedTokens += estimatedTokens;
  }

  return selected;
}
```

## 性能对比：语义搜索 vs 关键词搜索

| 场景 | 关键词搜索 (Grep) | 语义搜索 | 混合检索 |
|------|------------------|---------|---------|
| 查找`getUserById` | 精准命中 | 精准命中 | 精准命中 |
| 查找"用户认证相关代码" | 可能遗漏（需匹配关键词） | 命中`authenticate`、`verifyToken`等 | 最全面 |
| 查找"类似Redis的缓存逻辑" | 无法实现 | 找到内存缓存实现 | 找到内存缓存实现 |
| 大型代码库速度 | 快（<100ms） | 中等（200-500ms） | 稍慢（300-800ms） |
| 索引开销 | 无 | 一次性/增量 | 一次性/增量 |
| Token消耗 | 需多次搜索 | 一次精准命中 | 一次精准命中 |

## 完整启动流程

```bash
# 1. 启动ChromaDB（Docker方式）
docker run -d -p 8000:8000 chromadb/chroma:latest

# 2. 启动Ollama（如果使用本地嵌入）
ollama serve
ollama pull nomic-embed-text

# 3. 构建项目
cd claude-semantic-search
npm run build

# 4. 初始化索引
node dist/indexer.js /path/to/your/project

# 5. 启动MCP Server（Claude Code会自动连接）
node dist/mcp-server.js
```

## 训练营适用性评估

| 维度 | 评分 | 说明 |
|------|------|------|
| **难度曲线** | ⭐⭐⭐⭐ | 需要理解嵌入/向量概念，但代码实现不算复杂 |
| **实用性** | ⭐⭐⭐⭐⭐ | 直接提升Claude Code在大型项目中的表现 |
| **动手占比** | ⭐⭐⭐⭐⭐ | 100%实操，从编码到部署全覆盖 |
| **可扩展性** | ⭐⭐⭐⭐⭐ | 学员可替换不同嵌入模型/向量数据库 |
| **时间投入** | ⭐⭐⭐ | 完整实现约需3-4小时，可拆分为多个练习 |
| **前置知识** | ⭐⭐⭐ | 需TypeScript基础、了解向量概念、MCP协议 |

**适合人群**：有一定编程基础、想深入理解AI代码搜索原理的开发者。

**建议课时**：2次课，每次2小时。第一次完成索引和检索核心，第二次完成MCP集成和混合搜索。

## 参考资料

1. [Anthropic Embeddings API文档](https://docs.anthropic.com/en/docs/build-with-claude/embeddings) - Anthropic官方嵌入服务文档，底层使用Voyage AI
2. [claude-context: Semantic Code Search for Claude Code](https://github.com/zilliztech/claude-context) - Zilliz开源的MCP语义搜索插件，支持混合检索
3. [ChromaDB官方文档](https://docs.trychroma.com/) - 开源嵌入式向量数据库，本文核心存储组件
4. [Voyage AI Code Embeddings](https://docs.voyageai.com/docs/code-embeddings) - 专为代码优化的嵌入模型
5. [Model Context Protocol规范](https://modelcontextprotocol.io/) - MCP协议官方规范

---

*本文为Claude Code实战教程系列第三轮，聚焦语义搜索与向量检索在Claude Code中的完整应用链路。*

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
