提取、轉換和加載(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文檔。