嵌入是文本、图像或视频的数值表示,用于捕获输入之间的关系。
嵌入的工作原理是将文本、图像和视频转换为浮点数数组,称为向量。这些向量旨在捕获文本、图像和视频的含义。嵌入数组的长度称为向量的维度。
通过计算两个文本向量表示之间的数值距离,应用程序可以确定用于生成嵌入向量的对象之间的相似性。
EmbeddingModel
接口旨在轻松集成 AI 和机器学习中的嵌入模型。其主要功能是将文本转换为数值向量,通常称为嵌入。这些嵌入对于各种任务至关重要,例如语义分析和文本分类。
Spring AI支持的嵌入模型:
注意:本例使用通过Ollama安装的本地嵌入模型进行测试
通过Ollama安装嵌入模型
关于Ollama的安装,参考之前的博客DeepSeek本地化部署-CSDN博客
本例安装的嵌入模型是dmeta-embedding-zh,安装命令如下:
ollama pull shaw/dmeta-embedding-zh
嵌入模型的使用
导入jar
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
yml中增加嵌入模型的配置
spring:
ai:
ollama:
# ollama的api路径
base-url: http://localhost:11434
embedding:
options:
# 嵌入模型名称
model: shaw/dmeta-embedding-zh:latest
model:
embedding: ollama
嵌入模型支持的方法
EmbeddingResponse call(EmbeddingRequest request);
float[] embed(String text) {
float[] embed(Document document);
List<float[]> embed(List<String> texts)
List<float[]> embed(List<Document> documents, EmbeddingOptions options,
EmbeddingResponse embedForResponse(List<String> texts)
对字符串数据进行嵌入处理
@RestController
@RequestMapping("/embeding")
public class EmbedingController {
@Resource
private EmbeddingModel embeddingModel;
@GetMapping("/embed")
public String embed() {
// 返回响应对象
EmbeddingResponse embeddingResponse = embeddingModel.embedForResponse(List.of("今天天气不错"));
// Map<String, EmbeddingResponse> embedding = Map.of("embedding", embeddingResponse);
System.out.println(Arrays.toString(embeddingResponse.getResult().getOutput()));
// 直接返回向量化后的数据
float[] embed = embeddingModel.embed("挺风和日丽的");
System.out.println(Arrays.toString(embed));
return "success";
}
}
输出:
Document对象进行嵌入处理
DocumentReader
通过DocumentReader实现类对象,可以读取不同来源的文档数据。支持的实现类:
- JsonReader 处理 JSON 文档,将它们转换为 Document 对象列表
- TextReader 处理纯文本文档,将它们转换为 Document 对象列表
- JsoupDocumentReader 使用 JSoup 库处理 HTML 文档,将它们转换为 Document 对象列表。
- MarkdownDocumentReader 处理 Markdown 文档,将它们转换为 Document 对象列表。
- PagePdfDocumentReader 使用 Apache PdfBox 库解析 PDF 文档
- ParagraphPdfDocumentReader 使用 PDF 目录(例如 TOC)信息将输入 PDF 拆分为文本段落,并为每个段落输出一个单独的 Document。注意:并非所有 PDF 文档都包含 PDF 目录
- TikaDocumentReader 使用 Apache Tika 从各种文档格式(例如 PDF、DOC/DOCX、PPT/PPTX 和 HTML)中提取文本
TextReader读取文本文件数据
支持的构造方法
// 参数是文本文件的url路径
public TextReader(String resourceUrl)
// 参数是资源对象
public TextReader(Resource resource)
注入资源文件
// 加载指定的资源文件
@Value("classpath:document/医院.txt")
private org.springframework.core.io.Resource resource;
文本文件向量化处理
@GetMapping("/embed2")
public String embed2() {
// 读取文本文件
TextReader textReader = new TextReader(this.resource);
// 元数据中增加文件名
textReader.getCustomMetadata().put("filename", "医院.txt");
// 获取Document对象
List<Document> docList = textReader.read();
// 向量化处理
float[] embed = embeddingModel.embed(docList.get(0));
// 打印向量化后的数据
System.out.println(Arrays.toString(embed));
// 打印Document中的原始文本数据
System.out.println(docList.get(0).getText());
return "success";
}
DocumentTransformer
对文档进行批量转换处理
TokenTextSplitter
上个例子,我们对文档进行了转换,但是默认一个文档转为一个Document对象,如果文档太长,以后进行检索时,那么聊天上下文占用的token就会很大,为了解决该问题,我们可以对文档进行拆分处理。
TokenTextSplitter 是 TextSplitter 的一个实现,而TextSpliter继承了DocumentTransformer接口,它使用 CL100K_BASE 编码,根据 token 计数将文本分割成块。
构造函数:
public TokenTextSplitter() {
this(800, 350, 5, 10000, true);
}
public TokenTextSplitter(boolean keepSeparator) {
this(800, 350, 5, 10000, keepSeparator);
}
public TokenTextSplitter(int chunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator) {
......
}
参数说明:
- defaultChunkSize: 每个文本块以 token 为单位的目标大小(默认值:800)。
- minChunkSizeChars: 每个文本块以字符为单位的最小大小(默认值:350)。
- minChunkLengthToEmbed: 文本块去除空白字符或者处理分隔符后,用于嵌入处理的文本的最小长度(默认值:5)。
- maxNumChunks: 从文本生成的最大块数(默认值:10000)。
- keepSeparator: 是否在块中保留分隔符(例如换行符)(默认值:true)。
TokenTextSplitter拆分文档的逻辑:
1.使用 CL100K_BASE 编码将输入文本编码为 token
2.根据 defaultChunkSize 对编码后的token进行分块
3.对于分块:
(1)将块再解码为文本字符串
(2)尝试从后向前找到一个合适的截断点(句号、问号、感叹号或换行符)。
(3)如果找到合适的截断点,并且截断点所在的index大于minChunkSizeChars,则将在该点截断该块
(4)对分块去除两边的空白字符,并根据 keepSeparator 设置,可选地移除换行符
(5)如果处理后的分块长度大于 minChunkLengthToEmbed,则将其添加到分块列表中
4.持续执行第2步和第3步,直到所有 token 都被处理完或达到 maxNumChunks
5.如果还有剩余的token没有处理,并且剩余的token进行编码和转换处理后,长度大于 minChunkLengthToEmbed,则将其作为最终块添加
分块逻辑的源码:
protected List<String> doSplit(String text, int chunkSize) {
if (text != null && !text.trim().isEmpty()) {
List<Integer> tokens = this.getEncodedTokens(text);
List<String> chunks = new ArrayList();
int num_chunks = 0;
while(!tokens.isEmpty() && num_chunks < this.maxNumChunks) {
List<Integer> chunk = tokens.subList(0, Math.min(chunkSize, tokens.size()));
String chunkText = this.decodeTokens(chunk);
if (chunkText.trim().isEmpty()) {
tokens = tokens.subList(chunk.size(), tokens.size());
} else {
int lastPunctuation = Math.max(chunkText.lastIndexOf(46), Math.max(chunkText.lastIndexOf(63), Math.max(chunkText.lastIndexOf(33), chunkText.lastIndexOf(10))));
if (lastPunctuation != -1 && lastPunctuation > this.minChunkSizeChars) {
chunkText = chunkText.substring(0, lastPunctuation + 1);
}
String chunkTextToAppend = this.keepSeparator ? chunkText.trim() : chunkText.replace(System.lineSeparator(), " ").trim();
if (chunkTextToAppend.length() > this.minChunkLengthToEmbed) {
chunks.add(chunkTextToAppend);
}
tokens = tokens.subList(this.getEncodedTokens(chunkText).size(), tokens.size());
++num_chunks;
}
}
if (!tokens.isEmpty()) {
String remaining_text = this.decodeTokens(tokens).replace(System.lineSeparator(), " ").trim();
if (remaining_text.length() > this.minChunkLengthToEmbed) {
chunks.add(remaining_text);
}
}
return chunks;
} else {
return new ArrayList();
}
}
测试代码:
@GetMapping("/embed3")
public String embed3() {
// 读取文本文件
TextReader textReader = new TextReader(this.resource);
// 元数据中增加文件名
textReader.getCustomMetadata().put("filename", "医院.txt");
// 获取Document对象
List<Document> docList = textReader.read();
// 文档分割
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> splitDocuments = splitter.apply(docList);
// 向量化处理
float[] embed = embeddingModel.embed(splitDocuments.get(0));
// 打印向量化后的数据
System.out.println(Arrays.toString(embed));
// 打印Document中的原始文本数据
System.out.println(splitDocuments.get(0).getText());
return "success";
}
通过调试,可以看到本例的文本文档被分割为5个Document: