本文详细介绍Document Loaders 和 Document Parsers的使用,为后续开发RAG应用打好基础。
Document Loaders
LangChain4j框架目前支持多种文档Loaders。
| Loaders | APIs | Examples | description |
|---|---|---|---|
| Amazon S3 | AmazonS3DocumentLoader | AmazonS3DocumentLoaderIT.java | 从S3存储中加载文档 |
| Azure Blob Storage | AzureBlobStorageDocumentLoader | AzureBlobStorageDocumentLoaderIT | 从Azure二进制存储中夹在文档 |
| File System | FileSystemDocumentLoader | FileSystemDocumentLoaderTest | 从文件系统加载文档 |
| Github | GitHubDocumentLoader | GitHubDocumentLoaderIT | 从Github中加载文档 |
| Selenium | SeleniumDocumentLoader | SeleniumDocumentLoaderIT | |
| Tencent COS | TencentCosDocumentLoader | TencentCosDocumentLoaderIT | 腾讯云存储加载文档 |
| URL | UrlDocumentLoader | UrlDocumentLoaderTest | 根据URL加载文档 |
UrlDocumentLoader
public class UrlDocumentLoader {
/**
* 从指定的url获取文档。
*
* @param url 文件的url地址
* @param documentParser 从url中解析文本内容的解析器。
* @return document 文档内容
*/
public static Document load(URL url, DocumentParser documentParser) {
return DocumentLoader.load(UrlSource.from(url), documentParser);
}
public static Document load(String url, DocumentParser documentParser) {
return load(createUrl(url), documentParser);
}
// 根据字符串url创建 URL对象。
static URL createUrl(String url) {
try {
return new URL(url);
} catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
}
示例:
public class UrlDocumentLoaderTest implements WithAssertions {
@Test
public void should_load_text_document_from_url() {
String url = "https://raw.githubusercontent.com/langchain4j/langchain4j/main/langchain4j/src/test/resources/test-file-utf8.txt";
Document document = UrlDocumentLoader.load(url, new TextDocumentParser());
assertThat(document.text()).isEqualTo("test\ncontent");
assertThat(document.metadata().getString("url")).isEqualTo(url);
}
@Test
public void test_bad_url() {
String url = "bad_url";
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> UrlDocumentLoader.load(url, new TextDocumentParser()))
.withMessageContaining("no protocol");
}
}
FileSystemDocumentLoader
FileSystemDocumentLoader使用单例设计模式,并提供了两种功能的方法;
loadDocument():加载指定目录下的一个文件, 在某个目录下仅有一个文件。loadDocuments():加载指定目录下的所有文件loadDocumentsRecursively():加载当前目录以及所有子目录下的所有的文件
源码我们仅保留核心方法。
public class FileSystemDocumentLoader {
// 获取文档解析器,如果有自定义实现,则使用自定义实现,没有则使用TextDocumentParser
private static final DocumentParser DEFAULT_DOCUMENT_PARSER = getOrDefault(loadDocumentParser(), TextDocumentParser::new);
public static Document loadDocument(Path filePath, DocumentParser documentParser) {
if (!isRegularFile(filePath)) {
throw illegalArgument("'%s' is not a file", filePath);
}
return DocumentLoader.load(from(filePath), documentParser);
}
// 指定匹配器,并根据匹配器从指定目录加载一组文件
public static List<Document> loadDocuments(Path directoryPath,
PathMatcher pathMatcher,
DocumentParser documentParser) {
if (!isDirectory(directoryPath)) {
throw illegalArgument("'%s' is not a directory", directoryPath);
}
try (Stream<Path> pathStream = Files.list(directoryPath)) {
return loadDocuments(pathStream, pathMatcher, directoryPath, documentParser);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 递归的加载指定目录下被匹配器匹配到的所有文件
public static List<Document> loadDocumentsRecursively(Path directoryPath,
PathMatcher pathMatcher,
DocumentParser documentParser) {
if (!isDirectory(directoryPath)) {
throw illegalArgument("'%s' is not a directory", directoryPath);
}
try (Stream<Path> pathStream = Files.walk(directoryPath)) {
return loadDocuments(pathStream, pathMatcher, directoryPath, documentParser);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static List<Document> loadDocuments(Stream<Path> pathStream,
PathMatcher pathMatcher,
Path pathMatcherRoot,
DocumentParser documentParser) {
List<Document> documents = new ArrayList<>();
pathStream
.filter(Files::isRegularFile) // 过滤掉文件内容被加密或者不可读文件
.map(pathMatcherRoot::relativize) // 将路径转换为相对路径
.filter(pathMatcher::matches) // 匹配
.map(pathMatcherRoot::resolve) //
.forEach(file -> {
try {
Document document = loadDocument(file, documentParser);
documents.add(document);
} catch (BlankDocumentException ignored) {
// blank/empty documents are ignored
} catch (Exception e) {
String message = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
log.warn("Failed to load '{}': {}", file, message);
}
});
return documents;
}
private static DocumentParser loadDocumentParser() {
Collection<DocumentParserFactory> factories = loadFactories(DocumentParserFactory.class);
if (factories.size() > 1) {
throw new RuntimeException("Conflict: multiple document parsers have been found in the classpath. " +
"Please explicitly specify the one you wish to use.");
}
for (DocumentParserFactory factory : factories) {
return factory.create();
}
return null;
}
}
其中使用 Files 工具类进行目录文件的读取;
Files.list():当前目录读取文件
Files.walk():递归遍历读取文件
另外一个需要说明:loadFactories(DocumentParserFactory.class) 使用SPI可插拔的模式,提供了扩展点。LangChain4j实现了 ServiceHelper 对Java的SPI进行了封装。
DocumentSource
DocumentSource 主要读取文件,转换为InputStream和元数据,每种加载器的实现略微不同,下面是其类结构:
classDiagram
DocumentSource <|.. UrlSource
DocumentSource <|.. FileSystemSource
DocumentSource <|.. AmazonS3Source
DocumentSource <|.. AzureBlobStorageSource
DocumentSource <|.. GitHubSource
DocumentSource <|.. TencentCosSource
DocumentSource: + InputStream inputStream()
DocumentSource: + Metadata metadata()
class UrlSource{
- URL url
+ InputStream inputStream()
+ Metadata metadata()
+ UrlSource from(String url)
+ UrlSource from(URL url)
+ UrlSource from(URI uri)
}
class FileSystemSource{
- Path path
+ InputStream inputStream()
+ Metadata metadata()
+ FileSystemSource from(Path filePath)
+ FileSystemSource from(String filePath)
+ FileSystemSource from(File file)
+ FileSystemSource from(URI uri)
}
class AmazonS3Source {
- InputStream inputStream
- String bucket
- String key
+ InputStream inputStream()
+ Metadata metadata()
}
class AzureBlobStorageSource {
- InputStream inputStream
- String accountName
- String containerName
- String blobName
- BobProperties properties
+ InputStream inputStream()
+ Metadata metadata()
}
class GitHubSource {
- InputStream inputStream
- GHContent content
+ InputStream inputStream()
+ Metadata metadata()
}
class TencentCosSource {
- InputStream inputStream
- String bucket
- String key
+ InputStream inputStream()
+ Metadata metadata()
}
对于AmazonS3DocumentLoader、TencentCosDocumentLoader、GitHubDocumentLoader 等不再一一源码分析了,大家有问题可以一起在评论区讨论!
Document Parsers
LangChain4j框架支持如下几种文档解析器
| 解析器名称 | 作用 | APIs | 使用示例 |
|---|---|---|---|
| Text | 解析文本 | TextDocumentParser | TextDocumentParserTest |
| Apache Tika | 解析文档,比如doc、docx、ppt等 | ApacheTikaDocumentParser | ApacheTikaDocumentParserTest |
| Apache POI | 解析文档, 比如doc、docx、xls、xlsx等 | ApachePoiDocumentParser | ApachePoiDocumentParserTest |
| Apache PDFBOX | 解析 PDF 文件 | ApachePdfBoxDocumentParser | ApachePdfBoxDocumentParserTest |
LangChain4j框架支持的四种解析器,实现也是基于大家日常开发中非常熟悉的底层技术,比如POI、PDFBox等,大家可能对 Apache Tika不太熟悉,Apache tike在文档解析、MimeType探测等非常强大,大家可以学习一下 Apache tika。
本部分内容比较简单,这里就没必要在赘述了,有问题大家评论区讨论!
总结
本文主要介绍了文档加载器和文档解析器。两者的区别是一个从某个地方加载文档,另外一个是解析当前文档。本质上都是读取文件转换为Document对象。
当转换为Document对象后,就可以将其嵌入,最终将其存储到向量数据库中,这一过程则是RAG应用的离线部分。