LangChain4j Document Loaders和 Parsers 详解和应用

2,757 阅读4分钟

本文详细介绍Document Loaders 和 Document Parsers的使用,为后续开发RAG应用打好基础。

Document Loaders

LangChain4j框架目前支持多种文档Loaders。

LoadersAPIsExamplesdescription
Amazon S3AmazonS3DocumentLoaderAmazonS3DocumentLoaderIT.java从S3存储中加载文档
Azure Blob StorageAzureBlobStorageDocumentLoaderAzureBlobStorageDocumentLoaderIT从Azure二进制存储中夹在文档
File SystemFileSystemDocumentLoaderFileSystemDocumentLoaderTest从文件系统加载文档
GithubGitHubDocumentLoaderGitHubDocumentLoaderIT从Github中加载文档
SeleniumSeleniumDocumentLoaderSeleniumDocumentLoaderIT
Tencent COSTencentCosDocumentLoaderTencentCosDocumentLoaderIT腾讯云存储加载文档
URLUrlDocumentLoaderUrlDocumentLoaderTest根据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解析文本TextDocumentParserTextDocumentParserTest
Apache Tika解析文档,比如doc、docx、ppt等ApacheTikaDocumentParserApacheTikaDocumentParserTest
Apache POI解析文档, 比如doc、docx、xls、xlsx等ApachePoiDocumentParserApachePoiDocumentParserTest
Apache PDFBOX解析 PDF 文件ApachePdfBoxDocumentParserApachePdfBoxDocumentParserTest

LangChain4j框架支持的四种解析器,实现也是基于大家日常开发中非常熟悉的底层技术,比如POI、PDFBox等,大家可能对 Apache Tika不太熟悉,Apache tike在文档解析、MimeType探测等非常强大,大家可以学习一下 Apache tika

本部分内容比较简单,这里就没必要在赘述了,有问题大家评论区讨论!

总结

本文主要介绍了文档加载器和文档解析器。两者的区别是一个从某个地方加载文档,另外一个是解析当前文档。本质上都是读取文件转换为Document对象。

当转换为Document对象后,就可以将其嵌入,最终将其存储到向量数据库中,这一过程则是RAG应用的离线部分。