Spring AI:ETL Pipeline

19 阅读8分钟

ETL介绍

提取、转换和加载 (ETL) 框架是检索增强生成 (RAG) 用例中数据处理的支柱。

ETL 管道协调从原始数据源到结构化向量存储的数据流,确保数据以最优格式供 AI 模型检索。(ETL的作用就是从数据源提取数据为Document,对Document进行处理(提高文档向量匹配的精度),进行存储)

ETL 的组件

ETL 管道有三个主要组件:

  • DocumentReader 实现了 Supplier<List<Document>>
  • DocumentTransformer 实现了 Function<List<Document>, List<Document>>
  • DocumentWriter 实现了 Consumer<List<Document>>

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管道:

image.png 来自 Spring AI 官方文档

Spring AI 提供的实现组件

image.png Spring AI 1.1.0 官方

DocumentReaders

JSON

JsonReader 处理 JSON 文档,将其转换为 Document 对象的列表。

构造函数选项

JsonReader 提供了几个构造函数选项

  1. JsonReader(Resource resource)
  2. JsonReader(Resource resource, String… jsonKeysToUse)
  3. JsonReader(Resource resource, JsonMetadataGenerator jsonMetadataGenerator, String… jsonKeysToUse)
参数
  • resource:指向 JSON 文件的 Spring Resource 对象。

  • jsonKeysToUse:JSON 中的键数组,应将其用作生成 Document 对象中的文本内容。

  • jsonMetadataGenerator:一个可选的 JsonMetadataGenerator,用于为每个 Document 创建元数据。 JsonReader 按如下方式处理 JSON 内容

  • 它可以处理 JSON 数组和单个 JSON 对象。

  • 对于每个 JSON 对象(无论是数组中的还是单个对象)

    • 它根据指定的 jsonKeysToUse 提取内容。
    • 如果未指定键,它将整个 JSON 对象用作内容。
    • 它使用提供的 JsonMetadataGenerator(如果未提供则使用空生成器)生成元数据。
    • 它使用提取的内容和元数据创建一个 Document 对象。
使用 JSON 指针

JsonReader 现在支持使用 JSON 指针检索 JSON 文档的特定部分。此功能允许您轻松地从复杂的 JSON 结构中提取嵌套数据。

get(String pointer) 方法
public List<Document> get(String pointer)

此方法允许您使用 JSON 指针检索 JSON 文档的特定部分。

注意 在文档中没有明说,但是从源码来看,jsonKeysToUse作用于get()方法最后生成Document这一步,使用stream api 进行一个过滤,所以pointer指向jsongKeysToUse没有说明的key,返回的是一个空的DocumentList

文本

TextReader 处理纯文本文档,将其转换为 Document 对象的列表。

构造函数选项

TextReader 提供两个构造函数选项

  1. TextReader(String resourceUrl)
  2. TextReader(Resource resource)
参数
  • resourceUrl:表示要读取的资源 URL 的字符串。
  • resource:指向文本文件的 Spring Resource 对象。
行为

TextReader 按如下方式处理文本内容

  • 它将文本文件的全部内容读入单个 Document 对象。

  • 文件的内容成为 Document 的内容。

  • 元数据会自动添加到 Document 中

  • 通过 getCustomMetadata() 添加的任何自定义元数据都包含在 Document 中。

TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", "source.txt");
List<Document> docs = textReader.read();

HTML (JSoup)

JsoupDocumentReader 处理 HTML 文档,使用 JSoup 库将其转换为 Document 对象的列表。

JsoupDocumentReaderConfig 允许您自定义 JsoupDocumentReader 的行为

  • charset:指定 HTML 文档的字符编码(默认为 "UTF-8")。
  • selector:一个 JSoup CSS 选择器,用于指定从哪些元素中提取文本(默认为 "body")。
  • separator:用于连接来自多个选定元素的文本的字符串(默认为 "\n")。
  • allElements:如果为 true,则从 <body> 元素中提取所有文本,忽略 selector(默认为 false)。
  • groupByElement:如果为 true,则为 selector 匹配的每个元素创建一个单独的 Document(默认为 false)。
  • includeLinkUrls:如果为 true,则提取绝对链接 URL 并将其添加到元数据中(默认为 false)。
  • metadataTags:要提取内容的 <meta> 标签名称列表(默认为 ["description", "keywords"])。
  • additionalMetadata:允许您向所有创建的 Document 对象添加自定义元数据。
JsoupDocumentReaderConfig config = JsoupDocumentReaderConfig.builder()
    .selector("article p")
    .charset("UTF-8")
    .includeLinkUrls(true)
    .metadataTags(List.of("author", "date"))
    .build();
JsoupDocumentReader reader = new JsoupDocumentReader(resource, config);
List<Document> docs = reader.get();

Markdown

MarkdownDocumentReader 处理 Markdown 文档,将其转换为 Document 对象的列表。

MarkdownDocumentReaderConfig 允许您自定义 MarkdownDocumentReader 的行为

  • horizontalRuleCreateDocument:设置为 true 时,Markdown 中的水平线将创建新的 Document 对象。
  • includeCodeBlock:设置为 true 时,代码块将包含在与周围文本相同的 Document 中。设置为 false 时,代码块会创建单独的 Document 对象。
  • includeBlockquote:设置为 true 时,引用块将包含在与周围文本相同的 Document 中。设置为 false 时,引用块会创建单独的 Document 对象。
  • additionalMetadata:允许您向所有创建的 Document 对象添加自定义元数据
MarkdownDocumentReaderConfig mdCfg = MarkdownDocumentReaderConfig.builder()
    .withHorizontalRuleCreateDocument(true)
    .withIncludeCodeBlock(false)
    .build();
MarkdownDocumentReader mdReader = new MarkdownDocumentReader(resource, mdCfg);
List<Document> docs = mdReader.get();

PDF 页面

PagePdfDocumentReader 使用 Apache PdfBox 库解析 PDF 文档

@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 段落

ParagraphPdfDocumentReader 使用 PDF 目录(例如 TOC)信息将输入 PDF 拆分为文本段落,并为每个段落输出一个 Document。注意:并非所有 PDF 文档都包含 PDF 目录。

@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 文档

@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();
    }
}

转换器

TextSplitter

TextSplitter 是一个抽象基类,用于帮助分割文档以适应 AI 模型的上下文窗口。

TokenTextSplitter

TokenTextSplitter 是 TextSplitter 的一个实现,它使用 CL100K_BASE 编码根据令牌计数将文本分割成块。

造函数选项

TokenTextSplitter 提供两个构造函数选项

  1. TokenTextSplitter():使用默认设置创建拆分器。
  2. TokenTextSplitter(int defaultChunkSize, int minChunkSizeChars, int minChunkLengthToEmbed, int maxNumChunks, boolean keepSeparator)
参数
  • defaultChunkSize:每个文本块的令牌目标大小(默认值:800)。
  • minChunkSizeChars:每个文本块的最小字符大小(默认值:350)。
  • minChunkLengthToEmbed:要包含的块的最小长度(默认值:5)。
  • maxNumChunks:从文本生成的最大块数(默认值:10000)。
  • keepSeparator:是否在块中保留分隔符(如换行符)(默认值:true)。
TokenTextSplitter splitter = new TokenTextSplitter();
// 或 new TokenTextSplitter(800, 350, 5, 10_000, true);
List<Document> chunks = splitter.apply(docs);

keywordMetadataEnricher

KeywordMetadataEnricher 是一个 DocumentTransformer,它使用生成式 AI 模型从文档内容中提取关键字并将其添加为元数据。

构造函数选项

KeywordMetadataEnricher 提供两个构造函数选项

  1. KeywordMetadataEnricher(ChatModel chatModel, int keywordCount):使用默认模板并提取指定数量的关键字。
  2. KeywordMetadataEnricher(ChatModel chatModel, PromptTemplate keywordsTemplate):使用自定义模板进行关键字提取。
行为

KeywordMetadataEnricher 按如下方式处理文档

  1. 对于每个输入文档,它使用文档内容创建一个提示。
  2. 它将此提示发送到提供的 ChatModel 以生成关键字。
  3. 生成的关键字将以 "excerpt_keywords" 键添加到文档的元数据中。
  4. 返回经过富集的文档。
KeywordMetadataEnricher enricher = KeywordMetadataEnricher.builder(chatModel)
    .keywordCount(5)
    .build();
List<Document> enriched = enricher.apply(docs);
// 元数据键 excerpt_keywords

SummaryMetadataEnricher

SummaryMetadataEnricher 是一个 DocumentTransformer,它使用生成式 AI 模型为文档创建摘要并将其添加为元数据。它可以为当前文档以及相邻文档(上一文档和下一文档)生成摘要。

造函数

SummaryMetadataEnricher 提供两个构造函数

  1. SummaryMetadataEnricher(ChatModel chatModel, List<SummaryType> summaryTypes)
  2. SummaryMetadataEnricher(ChatModel chatModel, List<SummaryType> summaryTypes, String summaryTemplate, MetadataMode metadataMode)
参数
  • chatModel:用于生成摘要的 AI 模型。
  • summaryTypes:一个 SummaryType 枚举值列表,指示要生成哪些摘要(PREVIOUS、CURRENT、NEXT)。
  • summaryTemplate:用于摘要生成的自定义模板(可选)。
  • metadataMode:指定在生成摘要时如何处理文档元数据(可选)。
行为

SummaryMetadataEnricher 按如下方式处理文档

  1. 对于每个输入文档,它使用文档内容和指定的摘要模板创建一个提示。

  2. 它将此提示发送到提供的 ChatModel 以生成摘要。

  3. 根据指定的 summaryTypes,它会向每个文档添加以下元数据

    • section_summary:当前文档的摘要。
    • prev_section_summary:上一文档的摘要(如果可用且已请求)。
    • next_section_summary:下一文档的摘要(如果可用且已请求)。
  4. 返回经过富集的文档。

SummaryMetadataEnricher summarizer = new SummaryMetadataEnricher(
    chatModel,
    List.of(SummaryType.PREVIOUS, SummaryType.CURRENT, SummaryType.NEXT));
List<Document> withSummary = summarizer.apply(docs);
// section_summary, prev_section_summary, next_section_summary

写入器

写入文件

FileDocumentWriter 是 DocumentWriter 的实现,它将 Document 对象列表的内容写入文件。

构造函数

FileDocumentWriter 提供三个构造函数

  1. FileDocumentWriter(String fileName)
  2. FileDocumentWriter(String fileName, boolean withDocumentMarkers)
  3. FileDocumentWriter(String fileName, boolean withDocumentMarkers, MetadataMode metadataMode, boolean append)
参数
  • fileName:要写入文档的文件名。
  • withDocumentMarkers:是否在输出中包含文档标记(默认值:false)。
  • metadataMode:指定要写入文件的文档内容(默认值:MetadataMode.NONE)。
  • append:如果为 true,数据将写入文件末尾而不是开头(默认值:false)。
行为

FileDocumentWriter 按如下方式处理文档

  1. 它为指定的文件名打开一个 FileWriter。

  2. 对于输入列表中的每个文档

    1. 如果 withDocumentMarkers 为 true,它会写入包含文档索引和页码的文档标记。
    2. 它根据指定的 metadataMode 写入文档的格式化内容。
  3. 写入所有文档后,文件将关闭。

向量数据库存储