Spring AI:ETL Pipeline
提取、转换和加载(ETL)框架是检索增强生成(RAG)用例中数据处理的支柱。
ETL管道协调从原始数据源到结构化向量存储的流程,确保数据以最佳格式供AI模型检索。
RAG用例是文本,通过从数据体中检索相关信息来增强生成模型的能力,从而提高生成输出的质量和相关性。
API Overview
ETL管道创建、转换和存储Document实例。
Document类包含文本、元数据以及可选的其他媒体类型,如图像、音频和视频。
ETL管道有三个主要组件,
实施供应商列表的DocumentReader
实现函数<List<Document>、List<Document>>的DocumentTransformer
实现消费者<List<Document>>的DocumentWriter
Document类内容是在DocumentReader的帮助下从PDF、文本文件和其他文档类型创建的。
要构建一个简单的ETL管道,您可以将每种类型的实例链接在一起。
假设我们有这三种ETL类型的以下实例
PagePdfDocumentReader——DocumentReader的一种实现
TokenTextSplitter——DocumentTransformer的一种实现
DocumentWriter的矢量存储实现
要将数据基本加载到矢量数据库中,以便与检索增强生成模式一起使用,请使用以下Java函数样式语法代码。
vectorStore.accept(tokenTextSplitter.apply(pdfReader.get()));
或者,您可以使用更自然地表达域的方法名
vectorStore.write(tokenTextSplitter.split(pdfReader.read()));
ETL Interfaces
ETL管道由以下接口和实现组成。详细的ETL类图显示在ETL类图部分。
DocumentReader
提供来自不同来源的文档源。
public interface DocumentReader extends Supplier<List<Document>> {default List<Document> read() {return get();}
}
DocumentTransformer
将一批文档作为处理工作流的一部分进行转换。
public interface DocumentTransformer extends Function<List<Document>, List<Document>> {default List<Document> transform(List<Document> transform) {return apply(transform);}
}
DocumentWriter
管理ETL过程的最后阶段,准备文档进行存储。
public interface DocumentWriter extends Consumer<List<Document>> {default void write(List<Document> documents) {accept(documents);}
}
ETL Class Diagram
以下类图说明了ETL接口和实现。
DocumentReaders
JSON
JsonReader处理JSON文档,将其转换为Document对象列表。
Example
@Component
class MyJsonReader {private final Resource resource;MyJsonReader(@Value("classpath:bikes.json") Resource resource) {this.resource = resource;}List<Document> loadJsonAsDocuments() {JsonReader jsonReader = new JsonReader(this.resource, "description", "content");return jsonReader.get();}
}
Constructor Options
JsonReader提供了几个构造函数选项:
Parameters
resource:一个指向JSON文件的Spring resource对象。
jsonKeysToUse:JSON中的一组键,应用作生成的Document对象中的文本内容。
jsonMetadataGenerator:一个可选的jsonMetadataGenerator,用于为每个文档创建元数据。
Behavior
JsonReader按如下方式处理JSON内容:
它可以处理JSON数组和单个JSON对象。
对于每个JSON对象(数组或单个对象):
它根据指定的jsonKeysToUse提取内容。
如果没有指定键,它将使用整个JSON对象作为内容。
它使用提供的JsonMetadataGenerator(如果没有提供,则为空)生成元数据。
它使用提取的内容和元数据创建一个Document对象。
Using JSON Pointers
JsonReader现在支持使用JSON指针检索JSON文档的特定部分。此功能允许您轻松地从复杂的JSON结构中提取嵌套数据。
The get(String pointer) method
public List<Document> get(String pointer)
此方法允许您使用JSON指针来检索JSON文档的特定部分。
Parameters
指针:JSON指针字符串(如RFC 6901中定义的),用于在JSON结构中定位所需的元素。
Return Value
返回一个List<Document>,其中包含从指针所在的JSON元素解析的文档。
Behavior
该方法使用提供的JSON指针导航到JSON结构中的特定位置。
如果指针有效并指向现有元素:
对于JSON对象:它返回一个包含单个Document的列表。
对于JSON数组:它返回一个文档列表,数组中的每个元素对应一个文档。
如果指针无效或指向不存在的元素,则会抛出IllegalArgumentException。
Example
JsonReader jsonReader = new JsonReader(resource, "description");
List<Document> documents = this.jsonReader.get("/store/books/0");
Example JSON Structure
[{"id": 1,"brand": "Trek","description": "A high-performance mountain bike for trail riding."},{"id": 2,"brand": "Cannondale","description": "An aerodynamic road bike for racing enthusiasts."}
]
在这个例子中,如果JsonReader配置了“description”作为jsonKeysToUse,它将创建Document对象,其中内容是数组中每个自行车的“descriptions”字段的值。
Notes
JsonReader使用Jackson进行JSON解析。
通过使用数组流,它可以有效地处理大型JSON文件。
如果在jsonKeysToUse中指定了多个键,则内容将是这些键的值的连接。
阅读器很灵活,可以通过定制jsonKeysToUse和JsonMetadataGenerator来适应各种JSON结构。
Text
TextReader处理纯文本文档,将其转换为Document对象列表。
Example
@Component
class MyTextReader {private final Resource resource;MyTextReader(@Value("classpath:text-source.txt") Resource resource) {this.resource = resource;}List<Document> loadText() {TextReader textReader = new TextReader(this.resource);textReader.getCustomMetadata().put("filename", "text-source.txt");return textReader.read();}
}
Constructor Options
TextReader提供了两个构造函数选项:
Parameters
resourceUrl:表示要读取的资源的URL的字符串。
resource:指向文本文件的Spring resource对象。
Configuration
setCharset(字符集字符集):设置用于读取文本文件的字符集。默认值为UTF-8。
getCustomMetadata():返回一个可变映射,您可以在其中为文档添加自定义元数据。
Behavior
TextReader按如下方式处理文本内容:
它将文本文件的全部内容读取到单个Document对象中。
文件的内容成为文档的内容。
元数据会自动添加到文档中:
charset:用于读取文件的字符集(默认值:“UTF-8”)。
source:源文本文件的文件名。
通过getCustomMetadata()添加的任何自定义元数据都包含在文档中。
Notes
TextReader将整个文件内容读入内存,因此它可能不适合非常大的文件。
如果你需要将文本拆分成更小的块,你可以在阅读文档后使用TokenTextSplitter这样的文本拆分器:
List<Document> documents = textReader.get();
List<Document> splitDocuments = new TokenTextSplitter().apply(this.documents);
阅读器使用Spring的资源抽象,允许它从各种源(类路径、文件系统、URL等)读取。
可以使用getCustomMetadata()方法将自定义元数据添加到阅读器创建的所有文档中。
HTML (JSoup)
JsoupDocumentReader处理HTML文档,使用JSoup库将其转换为Document对象列表。
Example
@Component
class MyHtmlReader {private final Resource resource;MyHtmlReader(@Value("classpath:/my-page.html") Resource resource) {this.resource = resource;}List<Document> loadHtml() {JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder().selector("article p") // Extract paragraphs within <article> tags.charset("ISO-8859-1") // Use ISO-8859-1 encoding.includeLinkUrls(true) // Include link URLs in metadata.metadataTags(List.of("author", "date")) // Extract author and date meta tags.additionalMetadata("source", "my-page.html") // Add custom metadata.build();JsoupDocumentReader reader = new JsoupDocumentReader(this.resource, config);return reader.get();}
}
JsoupDocumentReaderConfig允许您自定义JsoudDocumentReader的行为:
charset:指定HTML文档的字符编码(默认为“UTF-8”)。
selector:一个JSoup CSS选择器,用于指定从哪些元素中提取文本(默认为“body”)。
分隔符:用于连接来自多个选定元素的文本的字符串(默认为“\n”)。
allElements:如果为true,则从<body>元素中提取所有文本,忽略选择器(默认为false)。
groupByElement:如果为true,则为选择器匹配的每个元素创建一个单独的Document(默认为false)。
includeLinkUrls:如果为true,则提取绝对链接URL并将其添加到元数据中(默认为false)。
元数据标签:从中提取内容的<meta>标签名称列表(默认为[“description”,“keywords”])。
additionalMetadata:允许您向所有创建的Document对象添加自定义元数据。
Sample Document: my-page.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>My Web Page</title><meta name="description" content="A sample web page for Spring AI"><meta name="keywords" content="spring, ai, html, example"><meta name="author" content="John Doe"><meta name="date" content="2024-01-15"><link rel="stylesheet" href="style.css">
</head>
<body><header><h1>Welcome to My Page</h1></header><nav><ul><li><a href="/">Home</a></li><li><a href="/about">About</a></li></ul></nav><article><h2>Main Content</h2><p>This is the main content of my web page.</p><p>It contains multiple paragraphs.</p><a href="https://www.example.com">External Link</a></article><footer><p>© 2024 John Doe</p></footer>
</body>
</html>
行为:
JsoupDocumentReader处理HTML内容,并根据配置创建Document对象:
选择器确定哪些元素用于文本提取。
如果allElements为true,则<body>中的所有文本都将提取到一个文档中。
如果groupByElement为true,则与选择器匹配的每个元素都会创建一个单独的文档。
如果allElements和groupByElement都不为true,则使用分隔符连接与选择器匹配的所有元素的文本。
文档标题、来自指定<meta>标签的内容和(可选)链接URL将添加到文档元数据中。
用于解析相对链接的基本URI将从URL资源中提取。
阅读器保留所选元素的文本内容,但删除其中的任何HTML标签。
Markdown
MarkdownDocumentReader处理Markdown文档,将其转换为Document对象列表。
Example
@Component
class MyMarkdownReader {private final Resource resource;MyMarkdownReader(@Value("classpath:code.md") Resource resource) {this.resource = resource;}List<Document> loadMarkdown() {MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder().withHorizontalRuleCreateDocument(true).withIncludeCodeBlock(false).withIncludeBlockquote(false).withAdditionalMetadata("filename", "code.md").build();MarkdownDocumentReader reader = new MarkdownDocumentReader(this.resource, config);return reader.get();}
}
MarkdownDocumentReaderConfig允许您自定义MarkdownDocumentReader的行为:
horizontalRuleCreateDocument:当设置为true时,Markdown中的水平规则将创建新的Document对象。
includeCodeBlock:当设置为true时,代码块将与周围的文本包含在同一个文档中。如果为false,代码块将创建单独的Document对象。
includeBlockquote:当设置为true时,blockquotes将与周围的文本包含在同一个文档中。如果为false,blockquotes将创建单独的Document对象。
additionalMetadata:允许您向所有创建的Document对象添加自定义元数据。
Sample Document: code.md
This is a Java sample application:```java
package com.example.demo;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}
}
```Markdown also provides the possibility to `use inline code formatting throughout` the entire sentence.---Another possibility is to set block code without specific highlighting:```
./mvnw spring-javaformat:apply
```
行为:MarkdownDocumentReader处理Markdown内容,并根据配置创建Document对象:
标头将成为Document对象中的元数据。
段落成为Document对象的内容。
代码块可以分离为自己的Document对象,也可以包含在周围的文本中。
块引号可以分隔成自己的Document对象,也可以包含在周围的文本中。
水平规则可用于将内容拆分为单独的文档对象。
阅读器在Document对象的内容中保留了内联代码、列表和文本样式等格式。
PDF Page
PagePdfDocumentReader使用Apache PdfBox库来解析PDF文档
使用Maven或Gradle将依赖项添加到您的项目中。
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
或者保存到您的Gradle build.Gradle构建文件。
dependencies {implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
}
Example
@Component
public class MyPagePdfDocumentReader {List<Document> getDocsFromPdf() {PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/sample1.pdf",PdfDocumentReaderConfig.builder().withPageTopMargin(0).withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withNumberOfTopTextLinesToDelete(0).build()).withPagesPerDocument(1).build());return pdfReader.read();}}
PDF Paragraph
ParagraphPdfDocumentReader使用PDF目录(如TOC)信息将输入的PDF拆分为文本段落,并为每个段落输出一个文档。注意:并非所有PDF文档都包含PDF目录。
Dependencies
使用Maven或Gradle将依赖项添加到您的项目中。
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
或者保存到您的Gradle build.Gradle构建文件。
dependencies {implementation 'org.springframework.ai:spring-ai-pdf-document-reader'
}
Example
@Component
public class MyPagePdfDocumentReader {List<Document> getDocsFromPdfWithCatalog() {ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader("classpath:/sample1.pdf",PdfDocumentReaderConfig.builder().withPageTopMargin(0).withPageExtractedTextFormatter(ExtractedTextFormatter.builder().withNumberOfTopTextLinesToDelete(0).build()).withPagesPerDocument(1).build());return pdfReader.read();}
}
Tika (DOCX, PPTX, HTML…)
TikaDocumentReader使用Apache Tika从各种文档格式中提取文本,如PDF、DOC/DOCX、PPT/PPTX和HTML。有关支持格式的完整列表,请参阅Tika文档。
Dependencies
<dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-tika-document-reader</artifactId>
</dependency>
或者保存到您的Gradle build.Gradle构建文件。
dependencies {implementation 'org.springframework.ai:spring-ai-tika-document-reader'
}
Example
@Component
class MyTikaDocumentReader {private final Resource resource;MyTikaDocumentReader(@Value("classpath:/word-sample.docx")Resource resource) {this.resource = resource;}List<Document> loadText() {TikaDocumentReader tikaDocumentReader = new TikaDocumentReader(this.resource);return tikaDocumentReader.read();}
}
Transformers
TextSplitter
TextSplitter是一个抽象基类,有助于划分文档以适应AI模型的上下文窗口。
TokenTextSplitter
TokenTextSplitter是TextSplitter的一种实现,它使用CL100K_BASE编码,根据令牌计数将文本分割成块。
Usage
@Component
class MyTokenTextSplitter {public List<Document> splitDocuments(List<Document> documents) {TokenTextSplitter splitter = new TokenTextSplitter();return splitter.apply(documents);}public List<Document> splitCustomized(List<Document> documents) {TokenTextSplitter splitter = new TokenTextSplitter(1000, 400, 10, 5000, true);return splitter.apply(documents);}
}
Constructor Options
TokenTextSplitter提供了两个构造函数选项:
Parameters
defaultChunkSize:每个文本块的目标大小(默认值:800)。
minChunkSizeChars:每个文本块的最小字符大小(默认值:350)。
minChunkLengthToEmbed:要包含的块的最小长度(默认值:5)。
maxNumChunks:从文本生成的最大块数(默认值:10000)。
keepSeparator:是否在块中保留分隔符(如换行符)(默认值:true)。
Behavior
TokenTextSplitter按如下方式处理文本内容:
Example
Document doc1 = new Document("This is a long piece of text that needs to be split into smaller chunks for processing.",Map.of("source", "example.txt"));
Document doc2 = new Document("Another document with content that will be split based on token count.",Map.of("source", "example2.txt"));TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> splitDocuments = this.splitter.apply(List.of(this.doc1, this.doc2));for (Document doc : splitDocuments) {System.out.println("Chunk: " + doc.getContent());System.out.println("Metadata: " + doc.getMetadata());
}
Notes
TokenTextSplitter使用jtokkit库中的CL100K_BASE编码,该编码与较新的OpenAI模型兼容。
拆分器试图通过在可能的情况下打破句子边界来创建语义上有意义的块。
原始文档中的元数据被保留并复制到从该文档派生的所有块中。
如果copyContentFormatter设置为true(默认行为),则原始文档中的内容格式化程序(如果设置)也会复制到派生块。
此拆分器对于为具有令牌限制的大型语言模型准备文本特别有用,可确保每个块都在模型的处理能力范围内。
ContentFormatTransformer
确保所有文档的内容格式统一。
KeywordMetadataEnricher
KeywordMetadataEnricher是一个DocumentTransformer,它使用生成式AI模型从文档内容中提取关键字并将其添加为元数据。
Usage
@Component
class MyKeywordEnricher {private final ChatModel chatModel;MyKeywordEnricher(ChatModel chatModel) {this.chatModel = chatModel;}List<Document> enrichDocuments(List<Document> documents) {KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(this.chatModel, 5);return enricher.apply(documents);}
}
Constructor
KeywordMetadataEnricher构造函数接受两个参数:
Behavior
KeywordMetadataEnricher按如下方式处理文档:
Customization
可以通过修改类中的KEYWORDS_TEMPLATE常量来定制关键字提取提示。默认模板为:
\{context_str}. Give %s unique keywords for this document. Format as comma separated. Keywords:
其中{context_str}被替换为文档内容,%s被替换为指定的关键字计数。
Example
ChatModel chatModel = // initialize your chat model
KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(chatModel, 5);Document doc = new Document("This is a document about artificial intelligence and its applications in modern technology.");List<Document> enrichedDocs = enricher.apply(List.of(this.doc));Document enrichedDoc = this.enrichedDocs.get(0);
String keywords = (String) this.enrichedDoc.getMetadata().get("excerpt_keywords");
System.out.println("Extracted keywords: " + keywords);
Notes
KeywordMetadataEnricher需要一个正常工作的ChatModel来生成关键字。
关键字计数必须为1或更大。
富集器将“excerpt_keywords”元数据字段添加到每个处理过的文档中。
生成的关键字以逗号分隔的字符串形式返回。
这种丰富器对于提高文档的可搜索性和为文档生成标签或类别特别有用。
SummaryMetadataEnricher
SummaryMetadataEnricher是一个DocumentTransformer,它使用生成式AI模型为文档创建摘要并将其添加为元数据。它可以为当前文档以及相邻文档(上一个和下一个)生成摘要。
Usage
@Configuration
class EnricherConfig {@Beanpublic SummaryMetadataEnricher summaryMetadata(OpenAiChatModel aiClient) {return new SummaryMetadataEnricher(aiClient,List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));}
}@Component
class MySummaryEnricher {private final SummaryMetadataEnricher enricher;MySummaryEnricher(SummaryMetadataEnricher enricher) {this.enricher = enricher;}List<Document> enrichDocuments(List<Document> documents) {return this.enricher.apply(documents);}
}
Constructor
SummaryMetadataEnricher提供了两个构造函数:
Parameters
chatModel:用于生成摘要的AI模型。
summaryTypes:SummaryType枚举值列表,指示要生成哪些摘要(上一个、当前、下一个)。
summaryTemplate:用于生成摘要的自定义模板(可选)。
元数据模式:指定在生成摘要时如何处理文档元数据(可选)。
Behavior
SummaryMetadataEnricher按如下方式处理文档:
section_summary:当前文档的摘要。
prev_section_summary:上一份文档的摘要(如果可用和要求)。
next_section_summary:下一个文档的摘要(如果可用和需要)。
Customization
摘要生成提示可以通过提供自定义摘要模板进行自定义。默认模板为:
"""
Here is the content of the section:
{context_str}Summarize the key topics and entities of the section.Summary:
"""
Example
ChatModel chatModel = // initialize your chat model
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(chatModel,List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));Document doc1 = new Document("Content of document 1");
Document doc2 = new Document("Content of document 2");List<Document> enrichedDocs = enricher.apply(List.of(this.doc1, this.doc2));// Check the metadata of the enriched documents
for (Document doc : enrichedDocs) {System.out.println("Current summary: " + doc.getMetadata().get("section_summary"));System.out.println("Previous summary: " + doc.getMetadata().get("prev_section_summary"));System.out.println("Next summary: " + doc.getMetadata().get("next_section_summary"));
}
提供的示例演示了预期的行为:
对于两个文档的列表,这两个文档都会收到section_summary。
第一个文档接收next_section_summary,但没有prevent_section_ssummary。
第二个文档收到上一个操作摘要,但没有下一个操作总结。
第一个文档的section_summary与第二个文档的previous_section_summary匹配。
第一个文档的next_section_summary与第二个文档的section_summary匹配。
Notes
SummaryMetadataEnricher需要一个正常工作的ChatModel来生成摘要。
富集器可以处理任何大小的文档列表,正确处理第一个和最后一个文档的边缘情况。
这种丰富器对于创建上下文感知摘要特别有用,可以更好地理解序列中的文档关系。
MetadataMode参数允许控制如何将现有元数据合并到摘要生成过程中。
Writers
File
FileDocumentWriter是一个DocumentWriter实现,它将文档对象列表的内容写入文件。
Usage
@Component
class MyDocumentWriter {public void writeDocuments(List<Document> documents) {FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, false);writer.accept(documents);}
}
Constructors
FileDocumentWriter提供了三个构造函数:
Parameters
fileName:要将文档写入的文件的名称。
withDocumentMarkers:是否在输出中包含文档标记(默认值:false)。
metadataMode:指定要写入文件的文档内容(默认值:metadataMode.NONE)。
append:如果为true,数据将写入文件末尾而不是开头(默认值:false)。
Behavior
FileDocumentWriter按如下方式处理文档:
Document Markers
当withDocumentMarkers设置为true时,编写器将按以下格式为每个文档添加标记:
### Doc: [index], pages:[start_page_number,end_page_number]
Metadata Handling
作者使用两个特定的元数据键:
page_number:表示文档的起始页码。
end_page_number:表示文档的结束页码。
这些用于编写文档标记。
Example
List<Document> documents = // initialize your documents
FileDocumentWriter writer = new FileDocumentWriter("output.txt", true, MetadataMode.ALL, true);
writer.accept(documents);
这将使用所有可用元数据将所有文档写入“output.txt”,包括文档标记,并在文件已存在的情况下附加到文件中。
Notes
编写器使用FileWriter,因此它使用操作系统的默认字符编码编写文本文件。
如果在写入过程中发生错误,将抛出RuntimeException,并将原始异常作为原因。
元数据模式参数允许控制如何将现有元数据合并到编写的内容中。
此编写器对于调试或创建文档集合的人类可读输出特别有用。
VectorStore
提供与各种矢量存储的集成。有关完整列表,请参阅Vector DB文档。