第9天 — Embedding 向量嵌入基础

Spring AI Alibaba 技术博客系列 | 2026-05-03

本文深入讲解 Embedding 向量嵌入的核心概念、原理、以及在 Spring AI Alibaba 中的完整使用方式。这是进入 RAG(检索增强生成)世界的关键前置知识。


目录

  1. 什么是 Embedding?
  2. Embedding 的工作原理
  3. Spring AI Alibaba 中的 EmbeddingModel 体系
  4. 快速入门:第一个 Embedding 应用
  5. 文本相似度计算实战
  6. 批量 Embedding 生成
  7. Embedding 模型选择指南
  8. Embedding 维度与性能调优
  9. 常见陷阱与最佳实践
  10. 完整实战项目:文档语义搜索

1. 什么是 Embedding?

Embedding(向量嵌入)是将文本、图像等高维非结构化数据转换为低维稠密向量(Dense Vector)的技术。转换后的向量保留了原始数据的语义信息——语义相近的文本,其向量在空间中的距离也更近。

举个直观的例子:

"猫是一种可爱的动物"  →  [0.12, -0.34, 0.56, ..., 0.78]  (1536维向量)
"狗狗是人类的好朋友"  →  [0.15, -0.31, 0.52, ..., 0.81]  (1536维向量)
"Python是一种编程语言" →  [-0.45, 0.67, -0.23, ..., 0.11] (1536维向量)

前两个向量很接近(都在讨论宠物),第三个向量则远离它们(编程话题)。这就是 Embedding 的魔力:机器”理解”了语义

为什么需要 Embedding?

传统文本处理Embedding 向量处理
关键词匹配(BM25)语义匹配
“apple” ≠ “水果”向量距离近
无法理解上下文保留语义关系
精确匹配,召回率低模糊匹配,召回率高
不支持多语言对齐多语言共享向量空间

在 AI 应用中,Embedding 是以下场景的基石:

  • 语义搜索:用户搜索”如何做红烧肉”,能匹配到”五花肉烹饪教程”
  • RAG 检索增强:从知识库中找到与问题最相关的段落
  • 文本聚类:自动将相似文档分组
  • 推荐系统:基于内容相似度推荐
  • 去重检测:发现语义重复的内容

2. Embedding 的工作原理

2.1 从 One-Hot 到 Dense Vector

早期的文本表示使用 One-Hot 编码:每个词对应一个巨大的稀疏向量,只有一个位置为1,其余全为0。这种方式存在严重问题:

词典大小: 100,000
"猫" → [0, 0, ..., 1, ..., 0, 0]  (第2345位为1)
"狗" → [0, 0, ..., 1, ..., 0, 0]  (第8912位为1)

这两个向量完全正交(点积为0),无法表达”猫”和”狗”都是宠物这个语义关系。

Embedding 通过神经网络学习,将词汇映射到连续的低维空间:

"猫" → [0.12, -0.34, 0.56, ...]
"狗" → [0.15, -0.31, 0.52, ...]
"汽车" → [-0.45, 0.67, -0.23, ...]

2.2 Transformer 编码器

现代 Embedding 模型(如 OpenAI 的 text-embedding-ada-002、通义千问的 text-embedding-v3)基于 Transformer 架构:

输入文本
    ↓
Tokenization (分词)
    ↓
Token Embedding + Position Embedding
    ↓
Transformer Encoder Layers (多层注意力)
    ↓
[CLS] Token 的输出向量
    ↓
归一化 (L2 Normalization)
    ↓
最终的 Embedding 向量

关键要点:

  • 注意力机制让模型能够捕捉词与词之间的长距离依赖关系
  • 多层编码逐层提取从局部到全局的语义特征
  • [CLS] token 经过所有层后,包含了整个句子的聚合信息
  • L2 归一化确保所有向量长度为1,便于用点积计算余弦相似度

2.3 余弦相似度

两个向量的相似度通常用余弦相似度衡量:

cos(A, B) = (A · B) / (|A| × |B|)

由于 Embedding 向量已经经过 L2 归一化(|A| = |B| = 1),公式简化为:

cos(A, B) = A · B  (直接点积)

相似度范围:-1(完全相反)到 1(完全相同)。通常 0.7 以上表示高度相关,0.5 以上表示有一定相关性。


3. Spring AI Alibaba 中的 EmbeddingModel 体系

3.1 核心接口

Spring AI Alibaba 的 Embedding 抽象基于 Spring AI 标准接口:

// 核心接口
public interface EmbeddingModel {
    
    // 对单个字符串生成 Embedding
    Embedding embed(String text);
    
    // 批量生成 Embedding
    Embeddings call(EmbeddingRequest request);
}

// Embedding 对象
public class Embedding {
    private List<Double> embedding;  // 向量值
    private String content;          // 原始文本
    // ...
}

// 请求对象
public class EmbeddingRequest {
    private List<String> prompts;     // 待嵌入的文本列表
    private EmbeddingOptions options; // 选项配置
    // ...
}

3.2 支持的 Embedding 模型

Spring AI Alibaba 支持多种 Embedding 模型提供商:

提供商模型维度特点
阿里云 DashScopetext-embedding-v31024多语言,支持细粒度维度选择
阿里云 DashScopetext-embedding-v21536经典模型,稳定可靠
OpenAItext-embedding-ada-0021536生态最广
OpenAItext-embedding-3-small1536性价比最优
OpenAItext-embedding-3-large3072效果最好

3.3 依赖配置

<!-- DashScope Embedding(阿里云通义千问) -->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter</artifactId>
    <version>1.0.0-M6.1</version>
</dependency>

<!-- 或者单独的 DashScope 适配器 -->
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    <version>1.0.0-M6.1</version>
</dependency>

4. 快速入门:第一个 Embedding 应用

4.1 项目初始化

# 使用 Spring Initializr 创建项目
curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.3.5 \
  -d baseDir=embedding-demo \
  -d dependencies=web,spring-ai \
  -o embedding-demo.zip

4.2 配置文件

# application.yml
spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}  # 从环境变量读取
      embedding:
        options:
          model: text-embedding-v3
          dimensions: 1024  # 可选,v3支持自定义维度

# 如果你使用 OpenAI:
# spring:
#   ai:
#     openai:
#       api-key: ${OPENAI_API_KEY}
#       embedding:
#         options:
#           model: text-embedding-3-small

4.3 基础使用示例

package com.example.embedding;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.document.MetadataMode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/embedding")
public class EmbeddingController {

    private final EmbeddingModel embeddingModel;

    @Autowired
    public EmbeddingController(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }

    /**
     * 对单个文本生成 Embedding
     */
    @GetMapping("/single")
    public Map<String, Object> singleEmbedding(@RequestParam String text) {
        Embedding embedding = embeddingModel.embed(text);
        
        return Map.of(
            "text", text,
            "dimensions", embedding.getEmbedding().size(),
            "vector_preview", embedding.getEmbedding().subList(0, Math.min(5, embedding.getEmbedding().size()))
        );
    }

    /**
     * 批量生成 Embedding
     */
    @PostMapping("/batch")
    public List<Map<String, Object>> batchEmbedding(@RequestBody List<String> texts) {
        EmbeddingRequest request = new EmbeddingRequest(texts, null);
        var response = embeddingModel.call(request);
        
        return response.getResults().stream()
            .map(result -> Map.of(
                "text", result.getOutput().getContent(),
                "dimensions", result.getOutput().getEmbedding().size(),
                "vector_preview", result.getOutput().getEmbedding().subList(0, 5)
            ))
            .toList();
    }
}

4.4 测试

# 单个文本 Embedding
curl "http://localhost:8080/api/embedding/single?text=人工智能正在改变世界"

# 批量文本 Embedding
curl -X POST http://localhost:8080/api/embedding/batch \
  -H "Content-Type: application/json" \
  -d '["猫是可爱的动物", "Python是编程语言", "红烧肉怎么做"]'

5. 文本相似度计算实战

Embedding 最有价值的应用之一就是计算文本之间的语义相似度。

5.1 相似度计算服务

package com.example.embedding;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Service;

import java.util.*;

@Service
public class SemanticSimilarityService {

    private final EmbeddingModel embeddingModel;

    public SemanticSimilarityService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }

    /**
     * 计算两个文本之间的余弦相似度
     */
    public double cosineSimilarity(String text1, String text2) {
        // 分别获取两个文本的 Embedding
        List<Double> vec1 = embeddingModel.embed(text1).getEmbedding();
        List<Double> vec2 = embeddingModel.embed(text2).getEmbedding();
        
        return cosineSimilarity(vec1, vec2);
    }

    /**
     * 计算两个向量的余弦相似度
     */
    private double cosineSimilarity(List<Double> vec1, List<Double> vec2) {
        if (vec1.size() != vec2.size()) {
            throw new IllegalArgumentException("向量维度不一致");
        }

        double dotProduct = 0.0;
        double norm1 = 0.0;
        double norm2 = 0.0;

        for (int i = 0; i < vec1.size(); i++) {
            double v1 = vec1.get(i);
            double v2 = vec2.get(i);
            dotProduct += v1 * v2;
            norm1 += v1 * v1;
            norm2 += v2 * v2;
        }

        double denominator = Math.sqrt(norm1) * Math.sqrt(norm2);
        if (denominator == 0.0) {
            return 0.0;
        }
        return dotProduct / denominator;
    }

    /**
     * 从候选文本中找出与查询最相似的 Top-K
     */
    public List<SimilarityResult> findTopK(String query, List<String> candidates, int topK) {
        // 获取查询文本的 Embedding
        List<Double> queryVec = embeddingModel.embed(query).getEmbedding();

        // 计算与所有候选文本的相似度
        List<SimilarityResult> results = new ArrayList<>();
        for (String candidate : candidates) {
            List<Double> candidateVec = embeddingModel.embed(candidate).getEmbedding();
            double similarity = cosineSimilarity(queryVec, candidateVec);
            results.add(new SimilarityResult(candidate, similarity));
        }

        // 按相似度降序排序,取 Top-K
        return results.stream()
            .sorted(Comparator.comparingDouble(SimilarityResult::similarity).reversed())
            .limit(topK)
            .toList();
    }

    /**
     * 相似度结果记录
     */
    public record SimilarityResult(String text, double similarity) {}
}

5.2 相似度 API 接口

@RestController
@RequestMapping("/api/similarity")
public class SimilarityController {

    private final SemanticSimilarityService similarityService;

    public SimilarityController(SemanticSimilarityService similarityService) {
        this.similarityService = similarityService;
    }

    @GetMapping("/compare")
    public Map<String, Object> compare(
            @RequestParam String text1,
            @RequestParam String text2) {
        double similarity = similarityService.cosineSimilarity(text1, text2);
        return Map.of(
            "text1", text1,
            "text2", text2,
            "similarity", String.format("%.4f", similarity),
            "interpretation", interpret(similarity)
        );
    }

    @PostMapping("/topk")
    public List<Map<String, Object>> findTopK(
            @RequestParam String query,
            @RequestBody List<String> candidates,
            @RequestParam(defaultValue = "3") int topK) {
        return similarityService.findTopK(query, candidates, topK).stream()
            .map(r -> Map.of(
                "text", r.text(),
                "similarity", String.format("%.4f", r.similarity())
            ))
            .toList();
    }

    private String interpret(double similarity) {
        if (similarity >= 0.85) return "高度相似";
        if (similarity >= 0.70) return "较为相似";
        if (similarity >= 0.50) return "有一定相关性";
        if (similarity >= 0.30) return "弱相关";
        return "几乎无关";
    }
}

5.3 实际测试效果

# 测试1:语义相近
curl "http://localhost:8080/api/similarity/compare?text1=如何做红烧肉&text2=五花肉烹饪方法"
# 预期输出: similarity ≈ 0.85+,interpretation: 高度相似

# 测试2:语义无关
curl "http://localhost:8080/api/similarity/compare?text1=如何做红烧肉&text2=Java并发编程"
# 预期输出: similarity ≈ 0.20-,interpretation: 几乎无关

# 测试3:跨语言语义(v3模型支持)
curl "http://localhost:8080/api/similarity/compare?text1=人工智能&text2=Artificial+Intelligence"
# 预期输出: similarity ≈ 0.90+,interpretation: 高度相似

6. 批量 Embedding 生成

在生产环境中,通常需要批量处理大量文本。Spring AI Alibaba 支持批量请求。

6.1 批量 Embedding 服务

package com.example.embedding;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.embedding.Embedding;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.IntStream;

@Service
public class BatchEmbeddingService {

    private final EmbeddingModel embeddingModel;

    public BatchEmbeddingService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }

    /**
     * 批量生成 Embedding(带分片处理,避免超出 API 限制)
     * 
     * 大多数 Embedding API 对单次请求有数量限制(如 DashScope 最多25条)
     */
    public List<EmbeddingResult> batchEmbed(List<String> texts, int batchSize) {
        List<EmbeddingResult> results = new ArrayList<>();
        
        // 分片处理
        List<List<String>> batches = partition(texts, batchSize);
        
        for (List<String> batch : batches) {
            EmbeddingRequest request = new EmbeddingRequest(batch, null);
            EmbeddingResponse response = embeddingModel.call(request);
            
            // 将结果与原文本关联
            for (int i = 0; i < response.getResults().size(); i++) {
                Embedding embedding = response.getResults().get(i).getOutput();
                results.add(new EmbeddingResult(
                    batch.get(i),
                    embedding.getEmbedding()
                ));
            }
        }
        
        return results;
    }

    /**
     * 分片工具方法
     */
    private <T> List<List<T>> partition(List<T> list, int size) {
        List<List<T>> partitions = new ArrayList<>();
        for (int i = 0; i < list.size(); i += size) {
            partitions.add(list.subList(i, Math.min(i + size, list.size())));
        }
        return partitions;
    }

    /**
     * 将 Embedding 结果转为结构化对象
     */
    public record EmbeddingResult(String text, List<Double> embedding) {}
}

6.2 性能优化:并行处理

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Service
public class ParallelEmbeddingService {

    private final EmbeddingModel embeddingModel;
    private final ExecutorService executor;

    public ParallelEmbeddingService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
        // 根据 API 限流情况调整线程数
        this.executor = Executors.newFixedThreadPool(4);
    }

    /**
     * 并行批量 Embedding
     */
    public List<EmbeddingResult> parallelBatchEmbed(List<String> texts, int batchSize) {
        List<List<String>> batches = partition(texts, batchSize);

        // 并行处理每个批次
        List<CompletableFuture<List<EmbeddingResult>>> futures = batches.stream()
            .map(batch -> CompletableFuture.supplyAsync(
                () -> processBatch(batch),
                executor
            ))
            .toList();

        // 合并结果
        return futures.stream()
            .map(CompletableFuture::join)
            .flatMap(List::stream)
            .toList();
    }

    private List<EmbeddingResult> processBatch(List<String> batch) {
        EmbeddingRequest request = new EmbeddingRequest(batch, null);
        EmbeddingResponse response = embeddingModel.call(request);

        List<EmbeddingResult> results = new ArrayList<>();
        for (int i = 0; i < response.getResults().size(); i++) {
            results.add(new EmbeddingResult(
                batch.get(i),
                response.getResults().get(i).getOutput().getEmbedding()
            ));
        }
        return results;
    }

    private <T> List<List<T>> partition(List<T> list, int size) {
        List<List<T>> partitions = new ArrayList<>();
        for (int i = 0; i < list.size(); i += size) {
            partitions.add(list.subList(i, Math.min(i + size, list.size())));
        }
        return partitions;
    }

    public void shutdown() {
        executor.shutdown();
    }
}

7. Embedding 模型选择指南

7.1 主流模型对比

特性text-embedding-v3text-embedding-v2text-embedding-3-smalltext-embedding-3-large
提供商阿里云 DashScope阿里云 DashScopeOpenAIOpenAI
默认维度1024153615363072
可调维度✅ 256/512/1024✅ 256/1024/1536✅ 256/1024/3072
多语言✅ 有限
中文效果⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
速度中等
成本较高
最大输入8192 tokens2048 tokens8191 tokens8191 tokens

7.2 选择建议

场景:中文为主的业务
推荐:text-embedding-v3
理由:中文语义理解最佳,维度可调,性价比高

场景:多语言/国际化
推荐:text-embedding-v3 或 text-embedding-3-small
理由:多语言支持好,生态广泛

场景:极致精度要求
推荐:text-embedding-3-large
理由:3072维,语义表达能力最强

场景:存储/带宽受限
推荐:text-embedding-v3 (256维) 或 text-embedding-3-small (256维)
理由:可调低维度,节省存储空间

7.3 动态切换模型

package com.example.embedding;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.dashscope.DashScopeEmbeddingModel;
import org.springframework.ai.dashscope.api.DashScopeApi;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class EmbeddingModelConfig {

    /**
     * 默认使用 v3 模型
     */
    @Bean
    @Qualifier("defaultEmbedding")
    public EmbeddingModel defaultEmbedding(DashScopeApi dashScopeApi) {
        return new DashScopeEmbeddingModel(
            dashScopeApi,
            MetadataMode.EMBED,
            "text-embedding-v3"
        );
    }

    /**
     * v2 模型(兼容旧数据)
     */
    @Bean
    @Qualifier("v2Embedding")
    public EmbeddingModel v2Embedding(DashScopeApi dashScopeApi) {
        return new DashScopeEmbeddingModel(
            dashScopeApi,
            MetadataMode.EMBED,
            "text-embedding-v2"
        );
    }
}

// 使用时注入指定的模型
@Service
public class DualModelService {

    private final EmbeddingModel defaultEmbedding;
    private final EmbeddingModel v2Embedding;

    public DualModelService(
            @Qualifier("defaultEmbedding") EmbeddingModel defaultEmbedding,
            @Qualifier("v2Embedding") EmbeddingModel v2Embedding) {
        this.defaultEmbedding = defaultEmbedding;
        this.v2Embedding = v2Embedding;
    }

    public Embedding embedWithV3(String text) {
        return defaultEmbedding.embed(text);
    }

    public Embedding embedWithV2(String text) {
        return v2Embedding.embed(text);
    }
}

8. Embedding 维度与性能调优

8.1 维度选择的影响

维度越高:
✅ 语义表达能力越强
✅ 区分度越高
❌ 存储空间越大
❌ 计算距离越慢
❌ 向量索引构建时间越长

维度越低:
✅ 存储紧凑
✅ 计算快速
❌ 语义表达能力有限
❌ 可能出现语义混淆

8.2 存储大小估算

假设 100 万条文档,每条一个 Embedding 向量:

3072维 (float32): 100万 × 3072 × 4 bytes ≈ 11.7 GB
1536维 (float32): 100万 × 1536 × 4 bytes ≈ 5.9 GB
1024维 (float32): 100万 × 1024 × 4 bytes ≈ 3.9 GB
512维  (float32): 100万 × 512  × 4 bytes ≈ 2.0 GB
256维  (float32): 100万 × 256  × 4 bytes ≈ 1.0 GB

如果使用 float16 或 quantization,可以再减半。

8.3 维度裁剪(Dimensionality Reduction)

如果现有模型不支持维度调节,可以通过矩阵投影来降低维度:

package com.example.embedding;

import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.stereotype.Component;

/**
 * 维度裁剪工具 - 通过 PCA 或随机投影降低向量维度
 * 
 * 注意:这只是近似方案,会损失一定精度。
 * 优先使用模型原生支持的维度调节功能。
 */
@Component
public class DimensionReducer {

    private final EmbeddingModel embeddingModel;

    public DimensionReducer(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }

    /**
     * 简单截断(只适用于向量尾部信息量较少的情况)
     * 注意:这不是推荐做法,仅作参考
     */
    public List<Double> truncate(List<Double> vector, int targetDim) {
        if (vector.size() <= targetDim) {
            return vector;
        }
        return vector.subList(0, targetDim);
    }

    /**
     * 平均池化降维
     * 将向量分段后取平均值
     */
    public List<Double> averagePool(List<Double> vector, int targetDim) {
        int chunkSize = (int) Math.ceil((double) vector.size() / targetDim);
        List<Double> result = new ArrayList<>();

        for (int i = 0; i < targetDim; i++) {
            int start = i * chunkSize;
            int end = Math.min(start + chunkSize, vector.size());
            
            double sum = 0;
            for (int j = start; j < end; j++) {
                sum += vector.get(j);
            }
            result.add(sum / (end - start));
        }

        // L2 归一化
        return normalize(result);
    }

    private List<Double> normalize(List<Double> vector) {
        double norm = Math.sqrt(
            vector.stream().mapToDouble(v -> v * v).sum()
        );
        if (norm == 0) return vector;
        return vector.stream()
            .map(v -> v / norm)
            .toList();
    }
}

9. 常见陷阱与最佳实践

9.1 陷阱1:不同模型的向量不能混用

// ❌ 错误做法
Embedding vec1 = openaiEmbeddingModel.embed("你好");   // OpenAI 的向量空间
Embedding vec2 = dashScopeEmbeddingModel.embed("你好"); // DashScope 的向量空间
double similarity = cosineSimilarity(vec1, vec2);       // 结果无意义!

// ✅ 正确做法:统一使用同一个模型
Embedding vec1 = embeddingModel.embed("你好");
Embedding vec2 = embeddingModel.embed("你好");
double similarity = cosineSimilarity(vec1, vec2);       // 有意义

不同模型训练的向量空间完全不同,即使输入相同文本,生成的向量也完全不同。混用会导致相似度计算完全错误。

9.2 陷阱2:文本预处理不一致

// ❌ 错误:索引时和检索时预处理方式不同
// 索引时:去掉了标点符号
String indexedText = "红烧肉是一道经典的中国菜";
// 检索时:保留了标点符号
String queryText = "红烧肉怎么做?";

// ✅ 正确:保持一致的预处理
public String normalizeText(String text) {
    return text.trim()
               .replaceAll("\\s+", " ")  // 合并多余空格
               .toLowerCase();            // 统一小写(英文场景)
}

String indexedText = normalizeText("红烧肉是一道经典的中国菜");
String queryText = normalizeText("红烧肉怎么做?");

9.3 陷阱3:忽略 Token 长度限制

// ❌ 错误:超长文本直接传入
String longText = readFile("very_long_document.txt");  // 可能超过8192 tokens
Embedding embedding = embeddingModel.embed(longText);   // 可能报错或截断

// ✅ 正确:先分块,再 Embedding
public List<Embedding> embedDocument(String document, int maxTokensPerChunk) {
    List<String> chunks = splitIntoChunks(document, maxTokensPerChunk);
    return chunks.stream()
        .map(embeddingModel::embed)
        .toList();
}

// 粗略估算:1个中文字 ≈ 1.5 tokens
// 8192 tokens ≈ 5500 个中文字
private List<String> splitIntoChunks(String text, int maxTokens) {
    // 实现文本分块逻辑
    // 注意:应该按语义边界分块(句子/段落),而不是简单按字符数
    // 我们将在后续文章中详细讨论文本分块策略
    return List.of(text); // 简化示例
}

9.4 陷阱4:Embedding 不是万能的

Embedding 擅长:
✅ 语义匹配("红烧肉" ≈ "五花肉烹饪")
✅ 主题相关(AI/机器学习/深度学习 相近)
✅ 意图理解("我想退款" ≈ "申请退货")

Embedding 不擅长:
❌ 精确匹配(身份证号、订单号、SKU)
❌ 数值比较(价格高低、日期先后)
❌ 逻辑推理(A>B, B>C => A>C)
❌ 专业术语精确匹配(药物分子式、法律条文编号)

最佳实践:Embedding + 关键词过滤 + 元数据过滤 = 最佳检索效果

9.5 最佳实践清单

1. ✅ 始终对 Embedding 向量做 L2 归一化(模型通常已内置)
2. ✅ 建立统一的文本预处理 pipeline
3. ✅ 同一应用内使用同一个 Embedding 模型
4. ✅ 监控 Embedding API 的延迟和错误率
5. ✅ 缓存常用文本的 Embedding 结果
6. ✅ 选择合适维度,平衡精度与性能
7. ✅ 定期评估 Embedding 质量(人工标注 + 自动评估)
8. ✅ 保留原始文本,方便调试和重新索引
9. ✅ 使用批量 API 而非逐个调用
10. ✅ 设计降级策略(Embedding 服务不可用时)

10. 完整实战项目:文档语义搜索

最后,我们构建一个完整的文档语义搜索服务,这是 RAG 系统的核心组件。

10.1 项目结构

document-search/
├── src/main/java/com/example/search/
│   ├── DocumentSearchApplication.java
│   ├── controller/SearchController.java
│   ├── service/DocumentService.java
│   ├── service/EmbeddingService.java
│   ├── service/SearchService.java
│   ├── model/Document.java
│   ├── model/SearchResult.java
│   └── config/EmbeddingConfig.java
└── src/main/resources/
    └── application.yml

10.2 数据模型

package com.example.search.model;

import java.util.List;

/**
 * 文档模型
 */
public record Document(
    String id,
    String title,
    String content,
    String category,
    List<Double> embedding,  // 文档的 Embedding 向量
    long createdAt
) {}

/**
 * 搜索结果
 */
public record SearchResult(
    String id,
    String title,
    String content,
    String category,
    double score,
    long createdAt
) {}

10.3 Embedding 服务

package com.example.search.service;

import org.springframework.ai.embedding.Embedding;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class EmbeddingService {

    private final EmbeddingModel embeddingModel;

    public EmbeddingService(EmbeddingModel embeddingModel) {
        this.embeddingModel = embeddingModel;
    }

    /**
     * 生成单条文本的 Embedding
     */
    public List<Double> embed(String text) {
        Embedding embedding = embeddingModel.embed(text);
        return embedding.getEmbedding();
    }

    /**
     * 批量生成 Embedding
     */
    public List<List<Double>> embedBatch(List<String> texts) {
        if (texts.isEmpty()) {
            return List.of();
        }

        // DashScope 限制单次最多 25 条
        int batchSize = 25;
        List<List<Double>> allEmbeddings = new java.util.ArrayList<>();

        for (int i = 0; i < texts.size(); i += batchSize) {
            int end = Math.min(i + batchSize, texts.size());
            List<String> batch = texts.subList(i, end);

            EmbeddingRequest request = new EmbeddingRequest(batch, null);
            EmbeddingResponse response = embeddingModel.call(request);

            for (var result : response.getResults()) {
                allEmbeddings.add(result.getOutput().getEmbedding());
            }
        }

        return allEmbeddings;
    }
}

10.4 文档服务(内存存储,实际项目应使用向量数据库)

package com.example.search.service;

import com.example.search.model.Document;
import com.example.search.model.SearchResult;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
public class DocumentService {

    private final EmbeddingService embeddingService;
    private final Map<String, Document> documentStore = new ConcurrentHashMap<>();

    public DocumentService(EmbeddingService embeddingService) {
        this.embeddingService = embeddingService;
    }

    /**
     * 添加文档(自动生成 Embedding)
     */
    public Document addDocument(String id, String title, String content, String category) {
        // 合并标题和内容作为 Embedding 输入
        String embedText = title + " " + content;
        List<Double> embedding = embeddingService.embed(embedText);

        Document doc = new Document(
            id,
            title,
            content,
            category,
            embedding,
            System.currentTimeMillis()
        );

        documentStore.put(id, doc);
        return doc;
    }

    /**
     * 批量添加文档
     */
    public List<Document> addDocuments(List<DocumentInput> inputs) {
        List<String> texts = inputs.stream()
            .map(input -> input.title() + " " + input.content())
            .toList();

        List<List<Double>> embeddings = embeddingService.embedBatch(texts);

        List<Document> docs = new ArrayList<>();
        for (int i = 0; i < inputs.size(); i++) {
            DocumentInput input = inputs.get(i);
            Document doc = new Document(
                input.id(),
                input.title(),
                input.content(),
                input.category(),
                embeddings.get(i),
                System.currentTimeMillis()
            );
            documentStore.put(doc.id(), doc);
            docs.add(doc);
        }

        return docs;
    }

    /**
     * 获取所有文档
     */
    public Collection<Document> getAllDocuments() {
        return documentStore.values();
    }

    /**
     * 删除文档
     */
    public boolean removeDocument(String id) {
        return documentStore.remove(id) != null;
    }

    /**
     * 文档输入
     */
    public record DocumentInput(String id, String title, String content, String category) {}
}

10.5 搜索服务

package com.example.search.service;

import com.example.search.model.Document;
import com.example.search.model.SearchResult;
import org.springframework.stereotype.Service;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class SearchService {

    private final DocumentService documentService;
    private final EmbeddingService embeddingService;

    public SearchService(DocumentService documentService, EmbeddingService embeddingService) {
        this.documentService = documentService;
        this.embeddingService = embeddingService;
    }

    /**
     * 语义搜索
     */
    public List<SearchResult> semanticSearch(String query, int topK) {
        // 1. 对查询生成 Embedding
        List<Double> queryEmbedding = embeddingService.embed(query);

        // 2. 计算与所有文档的相似度
        List<SearchResult> results = documentService.getAllDocuments().stream()
            .map(doc -> {
                double score = cosineSimilarity(queryEmbedding, doc.embedding());
                return new SearchResult(
                    doc.id(),
                    doc.title(),
                    truncateContent(doc.content(), 200),
                    doc.category(),
                    score,
                    doc.createdAt()
                );
            })
            .filter(r -> r.score() > 0.3)  // 过滤低相似度结果
            .sorted(Comparator.comparingDouble(SearchResult::score).reversed())
            .limit(topK)
            .collect(Collectors.toList());

        return results;
    }

    /**
     * 带类别过滤的语义搜索
     */
    public List<SearchResult> semanticSearchWithFilter(
            String query, 
            String category, 
            int topK) {
        List<Double> queryEmbedding = embeddingService.embed(query);

        List<SearchResult> results = documentService.getAllDocuments().stream()
            .filter(doc -> category == null || category.equals(doc.category()))
            .map(doc -> {
                double score = cosineSimilarity(queryEmbedding, doc.embedding());
                return new SearchResult(
                    doc.id(),
                    doc.title(),
                    truncateContent(doc.content(), 200),
                    doc.category(),
                    score,
                    doc.createdAt()
                );
            })
            .filter(r -> r.score() > 0.3)
            .sorted(Comparator.comparingDouble(SearchResult::score).reversed())
            .limit(topK)
            .collect(Collectors.toList());

        return results;
    }

    /**
     * 余弦相似度计算
     */
    private double cosineSimilarity(List<Double> vec1, List<Double> vec2) {
        double dotProduct = 0;
        double norm1 = 0;
        double norm2 = 0;

        for (int i = 0; i < vec1.size(); i++) {
            double v1 = vec1.get(i);
            double v2 = vec2.get(i);
            dotProduct += v1 * v2;
            norm1 += v1 * v1;
            norm2 += v2 * v2;
        }

        double denominator = Math.sqrt(norm1) * Math.sqrt(norm2);
        return denominator == 0 ? 0 : dotProduct / denominator;
    }

    /**
     * 截断内容用于展示
     */
    private String truncateContent(String content, int maxLength) {
        if (content == null || content.length() <= maxLength) {
            return content;
        }
        return content.substring(0, maxLength) + "...";
    }
}

10.6 控制器

package com.example.search.controller;

import com.example.search.model.Document;
import com.example.search.model.SearchResult;
import com.example.search.service.DocumentService;
import com.example.search.service.DocumentService.DocumentInput;
import com.example.search.service.SearchService;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/search")
public class SearchController {

    private final SearchService searchService;
    private final DocumentService documentService;

    public SearchController(SearchService searchService, DocumentService documentService) {
        this.searchService = searchService;
        this.documentService = documentService;
    }

    /**
     * 语义搜索接口
     */
    @GetMapping
    public List<SearchResult> search(
            @RequestParam String q,
            @RequestParam(defaultValue = "5") int topK,
            @RequestParam(required = false) String category) {

        if (category != null) {
            return searchService.semanticSearchWithFilter(q, category, topK);
        }
        return searchService.semanticSearch(q, topK);
    }

    /**
     * 添加单个文档
     */
    @PostMapping("/document")
    public Document addDocument(@RequestBody DocumentInput input) {
        return documentService.addDocument(
            input.id(),
            input.title(),
            input.content(),
            input.category()
        );
    }

    /**
     * 批量添加文档
     */
    @PostMapping("/documents")
    public List<Document> addDocuments(@RequestBody List<DocumentInput> inputs) {
        return documentService.addDocuments(inputs);
    }

    /**
     * 列出所有文档
     */
    @GetMapping("/documents")
    public Map<String, Object> listDocuments() {
        var docs = documentService.getAllDocuments();
        return Map.of(
            "total", docs.size(),
            "documents", docs.stream()
                .map(d -> Map.of(
                    "id", d.id(),
                    "title", d.title(),
                    "category", d.category()
                ))
                .toList()
        );
    }

    /**
     * 删除文档
     */
    @DeleteMapping("/document/{id}")
    public Map<String, Boolean> deleteDocument(@PathVariable String id) {
        boolean deleted = documentService.removeDocument(id);
        return Map.of("deleted", deleted);
    }
}

10.7 初始化测试数据

package com.example.search;

import com.example.search.service.DocumentService;
import com.example.search.service.DocumentService.DocumentInput;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class DataInitializer implements CommandLineRunner {

    private final DocumentService documentService;

    public DataInitializer(DocumentService documentService) {
        this.documentService = documentService;
    }

    @Override
    public void run(String... args) {
        List<DocumentInput> sampleDocs = List.of(
            new DocumentInput("doc1", 
                "Spring Boot 入门教程", 
                "Spring Boot 是一个用于创建独立的、生产级别的 Spring 应用的框架。它简化了 Spring 应用的初始搭建和开发过程。",
                "Java"),
            new DocumentInput("doc2",
                "Python 数据分析指南",
                "Python 是数据科学和机器学习领域最流行的编程语言之一。Pandas、NumPy、Matplotlib 等库提供了强大的数据处理和可视化能力。",
                "Python"),
            new DocumentInput("doc3",
                "机器学习基础概念",
                "机器学习是人工智能的一个重要分支,通过算法让计算机从数据中学习模式并做出预测。常见算法包括线性回归、决策树、神经网络等。",
                "AI"),
            new DocumentInput("doc4",
                "深度学习与神经网络",
                "深度学习是机器学习的子领域,使用多层神经网络来学习数据的层次化表示。CNN 用于图像处理,RNN 用于序列数据,Transformer 是当前最强大的架构。",
                "AI"),
            new DocumentInput("doc5",
                "Java 并发编程实战",
                "Java 提供了丰富的并发编程工具,包括 Thread、ExecutorService、CompletableFuture、synchronized 和 Lock 等。合理使用这些工具可以编写高效的并发程序。",
                "Java"),
            new DocumentInput("doc6",
                "RESTful API 设计规范",
                "RESTful API 设计遵循无状态、资源导向的原则。使用 HTTP 方法(GET/POST/PUT/DELETE)对应 CRUD 操作,URL 表示资源,状态码表示操作结果。",
                "架构"),
            new DocumentInput("doc7",
                "Docker 容器化部署",
                "Docker 是一种容器化技术,将应用及其依赖打包成镜像,实现环境一致性。相比虚拟机,容器更轻量、启动更快、资源利用更高效。",
                "DevOps"),
            new DocumentInput("doc8",
                "MySQL 性能优化技巧",
                "MySQL 性能优化涉及索引设计、查询优化、表结构设计和服务器配置等方面。合理使用 EXPLAIN 分析查询计划是优化的第一步。",
                "数据库")
        );

        documentService.addDocuments(sampleDocs);
        System.out.println("✅ 已初始化 " + sampleDocs.size() + " 条测试文档");
    }
}

10.8 完整测试

# 启动应用
./mvnw spring-boot:run

# 1. 查看文档列表
curl http://localhost:8080/api/search/documents

# 2. 语义搜索:"如何学习编程"
curl "http://localhost:8080/api/search?q=如何学习编程&topK=3"

# 预期结果(按相似度排序):
# 1. Python 数据分析指南 (AI/编程相关)
# 2. Spring Boot 入门教程 (教程类)
# 3. 机器学习基础概念 (学习相关)

# 3. 语义搜索:"神经网络是什么"
curl "http://localhost:8080/api/search?q=神经网络是什么&topK=3"

# 预期结果:
# 1. 深度学习与神经网络 (直接匹配)
# 2. 机器学习基础概念 (相关概念)
# 3. Python 数据分析指南 (工具相关)

# 4. 跨语言搜索:"neural network"
curl "http://localhost:8080/api/search?q=neural+network&topK=3"

# 预期结果(v3模型支持中英文跨语言):
# 1. 深度学习与神经网络
# 2. 机器学习基础概念

# 5. 带类别过滤的搜索
curl "http://localhost:8080/api/search?q=教程&category=Java&topK=3"

# 预期结果:只在 Java 类别中搜索
# 1. Spring Boot 入门教程
# 2. Java 并发编程实战

总结

Embedding 是 AI 应用的基础设施,它将非结构化文本转化为机器可理解的向量表示。本文从原理到实践,完整介绍了:

  1. Embedding 基本概念:什么是向量嵌入、为什么需要它
  2. 工作原理:从 Token 到 Dense Vector 的完整流程
  3. Spring AI Alibaba 集成:DashScope Embedding 的配置和使用
  4. 相似度计算:余弦相似度的实现与应用
  5. 批量处理:分片、并行、性能优化
  6. 模型选择:不同 Embedding 模型的对比与选择策略
  7. 避坑指南:4大常见陷阱和10条最佳实践
  8. 完整实战:文档语义搜索系统的实现

掌握了 Embedding,你就为下一步的 Vector Store 集成RAG 检索增强生成 打下了坚实基础。下一篇我们将深入 Vector Store,学习如何将 Embedding 向量存储到 Milvus、Redis 或 PostgreSQL 中,实现高效的向量检索。


下一篇预告:第10天 — Vector Store 集成(Milvus / Redis / PostgreSQL)

我们将学习多种向量数据库的集成方式,对比各自的优劣势,并构建生产级的向量检索服务。敬请期待!