<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title><![CDATA[Java - Elasticsearch Labs]]></title>
    <description><![CDATA[Articles and tutorials from the Search team at Elastic]]></description>
    <copyright><![CDATA[© 2026. Elasticsearch B.V. All Rights Reserved]]></copyright>
    <image>
      <title><![CDATA[Java - Elasticsearch Labs]]></title>
      <url>https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1121c0bf0e8a6e65/6a88da6340a1841030ef456f/search-labs-thumbnail.png</url>
      <link>https://www.elastic.co/cn/search-labs/blog/category/java-programming</link>
    </image>
    <link>https://www.elastic.co/cn/search-labs/blog/category/java-programming</link>
    <atom:link href="https://www.elastic.co/cn/search-labs/rss/category/java-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[cn]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 23:45:08 GMT</lastBuildDate>
  <item>
    <title><![CDATA[LangChain4j 使用 Elasticsearch 作为嵌入存储]]></title>
    <description><![CDATA[LangChain4j（Java 版 LangChain）将 Elasticsearch 作为嵌入式存储。了解如何使用它在普通 Java 中构建 RAG 应用程序。]]></description>
    <content:encoded><![CDATA[<p>
在<a href="https://www.elastic.co/search-labs/blog/langchain4j-llm-integration-introduction">上一篇文章</a>中，我们了解了什么是 LangChain4j 以及如何使用：</p><ul><li><p>与法律硕士进行讨论，实施<code>ChatLanguageModel</code> 和 <code>ChatMemory</code></p></li><li><p>在记忆中保留聊天记录，以便回忆起之前与一位法律硕士讨论的背景情况</p></li></ul><p>本博文将介绍如何</p><ul><li><p>根据文本示例创建向量嵌入</p></li><li><p>将向量嵌入存储在 Elasticsearch 嵌入存储中 </p></li><li><p>搜索类似载体</p></li></ul><h2>创建嵌入</h2><p>要创建嵌入式，我们需要定义一个<code>EmbeddingModel</code> 。例如，我们可以使用<a href="https://www.elastic.co/search-labs/blog/langchain4j-llm-integration-introduction">上一篇文章</a>中使用过的相同的 mistral 模型。它和奥拉马一起跑：</p>EmbeddingModel model = OllamaEmbeddingModel.builder()
  .baseUrl(ollama.getEndpoint())
  .modelName(MODEL_NAME)
  .build();<p>模型能够从文本中生成向量。在这里，我们可以检查模型生成的维数：</p>Logger.info("Embedding model has {} dimensions.", model.dimension());
// This gives: Embedding model has 4096 dimensions.<p>要从文本中生成向量，我们可以使用</p>Response&lt;Embedding&gt; response = model.embed("A text here");<p>或者，如果我们还想提供元数据，以便对文本、价格、发布日期等内容进行筛选，我们可以使用<code>Metadata.from()</code>.NET。例如，我们在这里添加游戏名称作为元数据字段：</p>TextSegment game1 = TextSegment.from("""
    The game starts off with the main character Guybrush Threepwood stating "I want to be a pirate!"
    To do so, he must prove himself to three old pirate captains. During the perilous pirate trials, 
    he meets the beautiful governor Elaine Marley, with whom he falls in love, unaware that the ghost pirate 
    LeChuck also has his eyes on her. When Elaine is kidnapped, Guybrush procures crew and ship to track 
    LeChuck down, defeat him and rescue his love.
""", Metadata.from("gameName", "The Secret of Monkey Island"));
Response&lt;Embedding&gt; response1 = model.embed(game1);
TextSegment game2 = TextSegment.from("""
    Out Run is a pseudo-3D driving video game in which the player controls a Ferrari Testarossa 
    convertible from a third-person rear perspective. The camera is placed near the ground, simulating 
    a Ferrari driver's position and limiting the player's view into the distance. The road curves, 
    crests, and dips, which increases the challenge by obscuring upcoming obstacles such as traffic 
    that the player must avoid. The object of the game is to reach the finish line against a timer.
    The game world is divided into multiple stages that each end in a checkpoint, and reaching the end 
    of a stage provides more time. Near the end of each stage, the track forks to give the player a 
    choice of routes leading to five final destinations. The destinations represent different 
    difficulty levels and each conclude with their own ending scene, among them the Ferrari breaking 
    down or being presented a trophy.
""", Metadata.from("gameName", "Out Run"));
Response&lt;Embedding&gt; response2 = model.embed(game2);<p>如果您想运行这段代码，请查看<a href="https://github.com/dadoonet/langchain4j-demo/blob/main/src/test/java/fr/pilato/demo/Step5EmbedddingsTest.java">Step5EmbedddingsTest.java</a>类。</p><h2>添加 Elasticsearch 来存储向量</h2><p>LangChain4j 提供内存嵌入存储。这对运行简单测试非常有用：</p>EmbeddingStore&lt;TextSegment&gt; embeddingStore = new InMemoryEmbeddingStore&lt;&gt;();
embeddingStore.add(response1.content(), game1);
embeddingStore.add(response2.content(), game2);<p>但是，这显然不能用于更大的数据集，因为该数据存储将所有内容都存储在内存中，而我们的服务器上没有无限的内存。因此，我们可以将嵌入式数据存储到 Elasticsearch 中，根据定义，Elasticsearch 是"elastic" ，可以随着数据的扩展而扩展。为此，让我们在项目中添加 Elasticsearch：</p>&lt;dependency&gt;
  &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
  &lt;artifactId&gt;langchain4j-elasticsearch&lt;/artifactId&gt;
  &lt;version&gt;${langchain4j.version}&lt;/version&gt;
&lt;/dependency&gt;

&lt;dependency&gt;
  &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
  &lt;artifactId&gt;elasticsearch&lt;/artifactId&gt;
  &lt;version&gt;1.20.1&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;<p>正如你所注意到的，我们还在项目中添加了 Elasticsearch TestContainers 模块，这样我们就可以从测试中启动 Elasticsearch 实例：</p>// Create the elasticsearch container
ElasticsearchContainer container =
  new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.15.0")
    .withPassword("changeme");

// Start the container. This step might take some time...
container.start();

// As we don't want to make our TestContainers code more complex than
// needed, we will use login / password for authentication.
// But note that you can also use API keys which is preferred.
final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme"));

// Create a low level Rest client which connects to the elasticsearch container.
client = RestClient.builder(HttpHost.create("https://" + container.getHttpHostAddress()))
  .setHttpClientConfigCallback(httpClientBuilder -&gt; {
    httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
    httpClientBuilder.setSSLContext(container.createSslContextFromCa());
    return httpClientBuilder;
  })
  .build();

// Check the cluster is running
client.performRequest(new Request("GET", "/"));<p>要将 Elasticsearch 用作嵌入式存储，"，" ，就必须从 LangChain4j 内存数据存储切换到 Elasticsearch 数据存储：</p>EmbeddingStore&lt;TextSegment&gt; embeddingStore =
  ElasticsearchEmbeddingStore.builder()
    .restClient(client)
    .build();
embeddingStore.add(response1.content(), game1);
embeddingStore.add(response2.content(), game2);<p>这将在 Elasticsearch 中以<code>default</code> 索引的形式存储向量。您还可以将索引名称改为更有意义的名称：</p>EmbeddingStore&lt;TextSegment&gt; embeddingStore =
  ElasticsearchEmbeddingStore.builder()
    .indexName("games")
    .restClient(client)
    .build();
embeddingStore.add(response1.content(), game1);
embeddingStore.add(response2.content(), game2);<p>如果您想运行此代码，请查看<a href="https://github.com/dadoonet/langchain4j-demo/blob/main/src/test/java/fr/pilato/demo/Step6ElasticsearchEmbedddingsTest.java">Step6ElasticsearchEmbedddingsTest.java</a>类。</p><h2>搜索类似载体</h2><p>要搜索相似向量，我们首先需要使用之前使用过的相同模型，将问题转换为向量表示。我们已经做到了，所以再做一次并不难。请注意，在这种情况下我们不需要元数据：</p>String question = "I want to pilot a car";
Embedding questionAsVector = model.embed(question).content();<p>我们可以用问题的这种表示法建立一个搜索请求，并要求嵌入式存储空间找出最前面的向量：</p>EmbeddingSearchResult&lt;TextSegment&gt; result = embeddingStore.search(
  EmbeddingSearchRequest.builder()
    .queryEmbedding(questionAsVector)
    .build());<p>现在，我们可以遍历结果并打印一些信息，如来自元数据的游戏名称和得分：</p>result.matches().forEach(m -&gt; Logger.info("{} - score [{}]",
  m.embedded().metadata().getString("gameName"), m.score()));<p>正如我们所预料的那样，"Out Run" 作为第一击：</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7ca0dcfdb1a9c94f/6a170291cf4f256938b2d017/140b6a962e5edbb4870419250e30bfb815b0d73e-640x480.gif" alt="跑出" />Out Run - score [0.86672974]
The Secret of Monkey Island - score [0.85569763]<p>如果您想运行这段代码，请查看<a href="https://github.com/dadoonet/langchain4j-demo/blob/9ec4b1d4c7c69821f143ddf272bbfed273c67b14/src/test/java/fr/pilato/demo/Step7SearchForVectorsTest.java#L110-L129">Step7SearchForVectorsTest.java</a>类。 </p><h2>幕后花絮</h2><p>Elasticsearch 嵌入存储的默认配置是在后台使用<a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.15/query-dsl-knn-query.html">近似 kNN 查询</a>。</p>POST games/_search
{
  "query" : {
    "knn": {
      "field": "vector",
      "query_vector": [-0.019137882, /* ... */, -0.0148779955]
    }
  }
}<p>但这可以通过向嵌入存储区提供默认配置 (<code>ElasticsearchConfigurationKnn</code>) 以外的另一种配置 (<code>ElasticsearchConfigurationScript</code>) 来改变：</p>EmbeddingStore&lt;TextSegment&gt; embeddingStore =
  ElasticsearchEmbeddingStore.builder()
    .configuration(ElasticsearchConfigurationScript.builder().build())
    .indexName("games")
    .restClient(client)
    .build();<p><code>ElasticsearchConfigurationScript</code><a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.15/query-dsl-script-score-query.html"><code>script_score</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.15/query-dsl-script-score-query.html"></a>执行程序使用<a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.15/query-dsl-script-score-query.html#vector-functions-cosine"><code>cosineSimilarity</code></a><a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.15/query-dsl-script-score-query.html#vector-functions-cosine"> 函数 在后台运行</a> 查询 。</p><p>基本上，打电话时</p>EmbeddingSearchResult&lt;TextSegment&gt; result = embeddingStore.search(
  EmbeddingSearchRequest.builder()
    .queryEmbedding(questionAsVector)
    .build());<p>现在呼叫</p>POST games/_search
{
  "query": {
    "script_score": {
      "script": {
        "source": "(cosineSimilarity(params.query_vector, 'vector') + 1.0) / 2",
        "params": {
          "queryVector": [-0.019137882, /* ... */, -0.0148779955]
        }
      }
    }
  }
}<p>在这种情况下，结果并不会因为"order" 而发生变化，只是分数会有所调整，因为<code>cosineSimilarity</code> 调用并不使用任何近似值，而是计算每个匹配向量的余弦值：</p>Out Run - score [0.871952]
The Secret of Monkey Island - score [0.86380446]<p>如果您想运行这段代码，请查看<a href="https://github.com/dadoonet/langchain4j-demo/blob/9ec4b1d4c7c69821f143ddf272bbfed273c67b14/src/test/java/fr/pilato/demo/Step7SearchForVectorsTest.java#L132-L155">Step7SearchForVectorsTest.java</a>类。</p><h2>结论</h2><p>我们已经介绍了如何从文本中轻松生成嵌入，以及如何使用两种不同的方法在 Elasticsearch 中存储和搜索近邻：</p><ul><li><p>使用<code>ElasticsearchConfigurationKnn</code> 默认选项进行近似和快速<code>knn</code> 查询</p></li><li><p>使用<code>ElasticsearchConfigurationScript</code> 选项进行精确但较慢的<code>script_score</code> 查询</p></li></ul><p>下一步将根据我们在这里学到的知识，构建一个完整的 RAG 应用程序。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/langchain4j-elasticsearch-embedding-store</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/langchain4j-elasticsearch-embedding-store</guid>
    <category><![CDATA[Java]]></category>
    <category><![CDATA[AI]]></category>
    <category><![CDATA[向量数据库]]></category>
    <dc:creator><![CDATA[David Pilato]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfc873b86c76d1798/6a170293acf088f666be99b3/abd8a4a809064101c037af66b87f28e5ecde03b0-1474x645.jpg" length="0" type="image/jpeg"/>
    <pubDate>Tue, 08 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[使用模拟和真实 Elasticsearch 测试 Java 代码]]></title>
    <description><![CDATA[了解如何使用模拟和测试容器编写 Elasticsearch 自动化测试]]></description>
    <content:encoded><![CDATA[<p>在本篇文章中，我们将介绍和解释使用 Elasticsearch 作为外部系统依赖关系测试软件的两种方法。我们将介绍使用模拟测试和集成测试的测试方法，展示它们之间的一些实际区别，并就每种风格的测试方法给出一些提示。</p><h2>对系统信心的良好测试</h2><p>好的测试能增强参与创建和维护 IT 系统过程的每个人的信心。测试并不是为了炫酷、快速或人为增加代码覆盖率。测试在确保这一点方面发挥着至关重要的作用：</p><ul><li><p>我们希望交付的产品能在生产中发挥作用。</p></li><li><p>系统符合要求和合同。</p></li><li><p>未来不会出现倒退。</p></li><li><p>开发人员（以及其他相关团队成员）确信他们所创建的系统能够正常运行。</p></li></ul><p>当然，这并不意味着测试不能很酷、很快或增加代码覆盖率。我们运行测试套件的速度越快越好。只是，为了缩短测试套件的总体持续时间，我们不应该牺牲自动化测试给我们带来的可靠性、可维护性和信心。</p><p>良好的自动测试会让团队成员更加自信：</p><ul><li><p>开发人员：他们可以确认自己所做的工作是否有效（甚至在代码离开他们的机器之前）。</p></li><li><p>质量保证团队：他们手动测试的工作量减少。</p></li><li><p>系统运营商和自力更生者：由于系统更易于部署和维护，因此更加轻松。</p></li></ul><p>最后但并非最不重要的一点：系统架构。我们喜欢系统井井有条、易于维护、架构简洁并能实现其目的。不过，有时我们可能会看到这样一种架构，它为了所谓的 ""，这样更容易测试 "的借口而牺牲了太多" 。具有很强的可测试性并没有错--只有当编写系统的主要目的是为了可测试，而不是为了满足证明其存在合理性的需求时，我们才会看到 "狗尾续貂 "的情况。</p><h2>两种测试模拟&amp; 依赖关系</h2><p>可以从很多方面来看待测试，从而对其进行分类。在这篇文章中，我将只关注划分测试的一个方面：使用模拟（或存根、伪造或......）与使用真实依赖关系。在我们的案例中，依赖关系是 Elasticsearch。</p><p>使用 mock 的测试速度非常快，因为它们不需要启动任何外部依赖关系，而且一切都只在内存中进行。自动测试中的 "模拟 "是指使用假对象代替真对象，在不使用实际依赖关系的情况下测试程序的某些部分。这就是需要它们的原因，也是它们在任何快速检测网测试中大放异彩的原因，例如输入验证。例如，无需启动数据库并调用它来验证请求中的负数是否不允许。</p><p>然而，引入模拟器会产生一些影响：</p><ul><li><p>并不是每件事、每段时间都能轻松模拟，因此模拟会对系统架构产生影响（有时影响很大，有时影响不大）。</p></li><li><p>在模拟系统上运行的测试可能会很快，但开发此类测试可能需要相当长的时间，因为模拟系统通常不是免费提供的，而是要深入反映它们所模拟的系统。了解系统工作原理的人需要以正确的方式编写模拟，而这种知识可以来自实践经验、研究文档等。</p></li><li><p>需要对模拟进行维护。当你的系统依赖于一个外部依赖关系时，如果你需要升级这个依赖关系，就必须有人确保模仿这个依赖关系的 mock 也会随着所有变化而更新：破坏性的、有文档记录的和无文档记录的（这也会对我们的系统产生影响）。当你想升级一个依赖项，但你的测试套件（仅使用模拟）却无法让你确信所有的测试用例都能正常工作时，这种情况就会变得特别痛苦。</p></li><li><p>要确保将精力用于开发和测试系统，而不是模拟，这需要纪律。</p></li></ul><p>因此，很多人主张反其道而行之：永远不要使用 mock（或存根等），而要完全依赖真实的依赖关系。这种方法在演示或系统很小、只有少量测试用例、覆盖范围很大的情况下非常有效。这些测试可以是集成测试（粗略地说：根据一些真实的依赖关系来检查系统的一部分），也可以是端到端测试（同时使用所有真实的依赖关系，并在所有端点检查系统的行为，同时播放用户工作流，以确定系统的可用性和成功性）。使用这种方法的一个明显好处是，我们还（经常无意中）验证了我们对依赖关系的假设，以及我们如何将它们与我们正在开发的系统进行整合。</p><p>不过，当测试只使用真实依赖关系时，我们需要考虑以下几个方面：</p><ul><li><p>有些测试场景并不需要实际的依赖关系（如验证请求的静态不变性）。</p></li><li><p>此类测试通常不会在开发人员的机器上整套运行，因为等待反馈会耗费太多时间。</p></li><li><p>它们需要更多的 CI 机器资源，而且可能需要更多的时间来调整，以避免浪费时间&amp; 。</p></li><li><p>使用测试数据初始化依赖关系可能并非易事。</p></li><li><p>具有真实依赖关系的测试非常适合在重大重构、迁移或依赖关系升级之前对代码进行封锁。</p></li><li><p>它们更可能是不透明的测试，即不详细说明被测系统的内部结构，只关心测试结果。</p></li></ul><h2>甜蜜点：同时使用两种测试</h2><p>与其只用一种测试对系统进行测试，不如在合理的情况下同时使用两种测试，并尝试改进两种测试的使用。</p><ul><li><p>首先运行基于模拟的测试，因为它们速度更快，只有当所有测试都成功后，才运行速度较慢的依赖性测试。</p></li><li><p>在不太需要外部依赖的情况下选择模拟：如果模拟会耗费太多时间，就应该对代码进行大规模修改，而不是依赖外部依赖。</p></li><li><p>只要合理，使用这两种方法测试一段代码是没有问题的。</p></li></ul><h2>SystemUnderTest 示例</h2><p>在接下来的章节中，我们将使用一个示例，该示例可<a href="https://github.com/pioorg/testing-elasticsearch">在此处</a>找到。这是一个用 Java 21 编写的小型演示应用程序，使用 Maven 作为构建工具，依赖于 Elasticsearch 客户端，并使用 Elasticsearch 最新添加的<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/esql.html">ES|QL</a>（Elastic 的新程序化查询语言）。如果 Java 不是你的编程语言，你仍然可以理解我们下面要讨论的概念，并将它们转换到你的堆栈中。只是使用真实的代码示例会让某些事情更容易解释。</p><p><code>BookSearcher</code> 可以帮助我们处理搜索和分析数据，在我们的案例中就是书籍（如<a href="https://www.elastic.co/search-labs/blog/esql-queries-to-java-objects">之前的一篇文章</a>所示）。</p><ul><li><p>例如，我们不确定我们的代码是否向前兼容，也不确定它是否向后兼容，因此我们需要 Elasticsearch 的版本<code>8.15.x</code> 作为唯一的依赖关系（参见<code>isCompatibleWithBackend()</code> ）。在将生产中的 Elasticsearch 升级到更新的版本之前，我们应首先在测试中对其进行升级，以确保被测系统的行为保持不变。</p></li><li><p>我们可以用它来搜索某一年出版的图书数量（见<code>numberOfBooksPublishedInYear</code> ）。</p></li><li><p>当我们需要分析我们的数据集并找出两个给定年份之间发表论文最多的 20 位作者时，我们也可以使用它（见<code>mostPublishedAuthorsInYears</code> ）。</p></li></ul>public class BookSearcher {

    private final ElasticsearchClient esClient;

    public BookSearcher(ElasticsearchClient esClient) {
        this.esClient = esClient;
        if (!isCompatibleWithBackend()) {
            throw new UnsupportedOperationException("This is not compatible with backend");
        }
    }

    private boolean isCompatibleWithBackend() {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            show info
            | keep version
            | dissect version "%{major}.%{minor}.%{patch}"
            | keep major, minor
            | limit 1""")) {
            if (!rs.next()) {
                throw new RuntimeException("No version found");
            }
            return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    public int numberOfBooksPublishedInYear(int year) {
        try (ResultSet rs = esClient.esql().query(ResultSetEsqlAdapter.INSTANCE, """
            from books
            | where year == ?
            | stats published = count(*) by year
            | limit 1000""", year)) {

            if (rs.next()) {
                return rs.getInt("published");
            }
        } catch (SQLException | IOException e) {
            throw new RuntimeException(e);
        }
        return 0;
    }


    public List&lt;MostPublished&gt; mostPublishedAuthorsInYears(int minYear, int maxYear) {
        assert minYear &lt;= maxYear;
        String query = """
            from books
            | where year &gt;= ? and year &lt;= ?
            | stats first_published = min(year), last_published = max(year), times = count (*) by author
            | eval years_published = last_published - first_published
            | sort years_published desc
            | drop years_published
            | limit 20
            """;

        try {
            Iterable&lt;MostPublished&gt; published = esClient.esql().query(
                ObjectsEsqlAdapter.of(MostPublished.class),
                query,
                minYear,
                maxYear);

            List&lt;MostPublished&gt; mostPublishedAuthors = new ArrayList&lt;&gt;();
            for (MostPublished mostPublished : published) {
                mostPublishedAuthors.add(mostPublished);
            }
            return mostPublishedAuthors;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public record MostPublished(
        String author,
        @JsonProperty("first_published") int firstPublished,
        @JsonProperty("last_published") int lastPublished,
        int times
    ) {
        public MostPublished {
            assert author != null;
            assert firstPublished &lt;= lastPublished;
            assert times &gt; 0;
        }
    }
}
<h2>从模拟测试开始</h2><p>为了创建测试中使用的模拟，我们将使用 Java 生态系统中非常流行的模拟库<a href="https://site.mockito.org/">Mockito</a>。</p><p>我们可以从以下方面入手，在每次测试前重置模拟：</p>public class BookSearcherMockingTest {

    ResultSet mockResultSet;
    ElasticsearchClient esClient;
    ElasticsearchEsqlClient esql;

    @BeforeEach
    void setUpMocks() {
        mockResultSet = mock(ResultSet.class);
        esClient = mock(ElasticsearchClient.class);
        esql = mock(ElasticsearchEsqlClient.class);

    }
}
<p>正如我们之前所说，并不是所有东西都能轻松使用模拟测试。但有些事情我们可以做（甚至应该做）。让我们尝试验证一下，目前 Elasticsearch 唯一支持的版本是<code>8.15.x</code> （将来，一旦我们确认我们的系统与未来版本兼容，我们可能会扩大范围）：</p>@Test
void canCreateSearcherWithES_8_15() throws SQLException, IOException{
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(15);

    // then
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(esClient));
}
<p>我们可以通过类似的方式（只需返回一个不同的次版本）验证<code>BookSearcher</code> 是否能与<code>8.16.x</code> 兼容，因为我们还不确定它是否能与 兼容：</p>@Test
void cannotCreateSearcherWithoutES_8_15() throws SQLException, IOException {
    // when
    when(esClient.esql()).thenReturn(esql);
    when(esql.query(eq(ResultSetEsqlAdapter.INSTANCE), anyString())).thenReturn(mockResultSet);
    when(mockResultSet.next()).thenReturn(true).thenReturn(false);
    when(mockResultSet.getInt(1)).thenReturn(8);
    when(mockResultSet.getInt(2)).thenReturn(16);

    // then
    Assertions.assertThrows(UnsupportedOperationException.class, () -&gt; new BookSearcher(esClient));
}
<p>现在，让我们看看如何在针对实际 Elasticsearch 进行测试时实现类似的功能。为此，我们将使用<a href="https://java.testcontainers.org/modules/elasticsearch/">Testcontainers 的 Elasticsearch 模块</a>，它只有一个要求：需要访问 Docker，因为它会为你运行 Docker 容器。从某种角度看，Testcontainers 只是操作 Docker 容器的一种方式，但您可以用自己熟悉的编程语言来表达自己的需求，而不是在 Docker 桌面（或类似工具）、CLI 或脚本中进行操作。这样就可以直接从测试代码中获取镜像、启动容器、在测试后收集垃圾、来回复制文件、执行命令、检查日志等。</p><p>存根可能是这样的</p>@Testcontainers
public class BookSearcherIntTest {

    static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
    static final JacksonJsonpMapper JSONP_MAPPER = new JacksonJsonpMapper();

    RestClientTransport transport;
    ElasticsearchClient client;

    @Container
    ElasticsearchContainer elasticsearch = new ElasticsearchContainer(ELASTICSEARCH_IMAGE);

    @BeforeEach
    void setupClient() {
        transport = // setup transport here
        client = new ElasticsearchClient(transport);
    }

    @AfterEach
    void closeClient() throws IOException {
        if (transport != null) {
            transport.close();
        }
    }

}
<p>在本例中，我们依靠<a href="https://java.testcontainers.org/test_framework_integration/junit_5/"> Testcontainers 的 JUnit</a> 与<code>@Testcontainers</code> 和 集成<code>@Container</code> ，这意味着我们无需担心在测试前启动 Elasticsearch 和测试后停止 Elasticsearch。我们唯一需要做的就是在每次测试前创建客户端，并在每次测试后关闭客户端（以避免资源泄漏，这可能会影响更大的测试套件）。</p><p>用<code>@Container</code> 来注解非静态字段意味着，每次测试都会启动一个新容器，因此我们不必担心数据过时或容器状态重置的问题。不过，在许多测试中，这种方法的性能可能并不理想，因此我们将在下一篇文章中将其与其他方法进行比较。</p><p><strong>请注意：</strong></p>通过依赖<code>docker.elastic.co</code> （Elastic 的官方 Docker 镜像仓库），可以避免耗尽 Docker hub 的限制。此外，还建议在测试和生产环境中使用相同版本的依赖项，以确保最大的兼容性。我们还建议精确选择版本，因此 Elasticsearch 图像没有<code>latest</code> 标签。<h2>在测试中连接 Elasticsearch</h2><p><a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html">Elasticsearch Java 客户端</a>能够连接到运行在测试容器中的 Elasticsearch，即使安全和 SSL/TLS 已启用（这是 8.x 版本的默认设置，因此我们无需在容器声明中指定任何与安全相关的内容）。假设您在生产中使用的 Elasticsearch 也启用了 TLS 和一些安全功能，建议集成测试设置尽可能接近生产场景，因此不要在测试中禁用它们。</p><p>假设容器分配给字段或变量<code>elasticsearch</code> ，如何获取连接所需的数据？</p><ul><li><p><code>elasticsearch.getHost()</code> 将给出容器运行的主机（大多数情况下可能是<code>"localhost"</code> ，但请不要硬编码，因为根据您的设置，有时可能是另一个名称，因此应始终动态获取主机）。</p></li><li><p><code>elasticsearch.getMappedPort(9200)</code> 将给出连接容器内运行的 Elasticsearch 时必须使用的主机端口（因为每次启动容器时，外部端口都不同，所以这也必须是动态调用）。</p></li><li><p>除非被覆盖，否则默认用户名和密码分别为<code>"elastic"</code> 和<code>"changeme"</code> 。</p></li><li><p>如果在容器设置过程中没有指定 SSL/TLS 证书，也没有禁用安全连接（这是 8.x 版本的默认行为），则会生成自签名证书。要信任它（例如就像<a href="https://curl.se/docs/manpage.html#--cacert">cURL 可以做的</a>那样）可以使用<code>elasticsearch.caCertAsBytes()</code> 获取证书（返回<code>Optional&lt;byte[]&gt;</code> ），或者另一种方便的方法是使用<code>createSslContextFromCa()</code> 获取<code>SSLContext</code> 。</p></li></ul><p>总体结果可能是这样的</p>BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("elastic", "changeme"));

// Create a low level rest client
RestClient restClient = RestClient.builder(new HttpHost(elasticsearch.getHost(), elasticsearch.getMappedPort(9200), "https"))
    .setHttpClientConfigCallback(httpClientBuilder -&gt;
        httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
            .setSSLContext(elasticsearch.createSslContextFromCa())
    )
    .build();

// The RestClientTransport is mainly for serialization/deserialization
RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

// The official Java API Client for Elasticsearch
ElasticsearchClient client = new ElasticsearchClient(transport);
<p>另一个创建<code>ElasticsearchClient</code> 实例的示例可在<a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherIntTest.java#L61">演示项目</a>中找到。</p><p><strong>请注意</strong>：</p>如需在生产环境中创建客户端，请参阅<a href="https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/connecting.html#_verifying_https_with_a_certificate_fingerprint">文档</a>。<h2>首次集成测试</h2><p>我们的第一个测试是验证是否可以使用 Elasticsearch 版本 8.15.x 创建<code>BookSearcher</code> ，测试结果可能如下：</p>@Test
void canCreateClientWithContainerRunning_8_15() {
    Assertions.assertDoesNotThrow(() -&gt; new BookSearcher(client));
}
<p>正如你所看到的，我们不需要设置其他任何东西。我们不需要模拟 Elasticsearch 返回的版本，唯一需要做的就是为<code>BookSearcher</code> 提供一个连接到 Elasticsearch 真实实例的客户端，该实例已由 Testcontainers 为我们启动。</p><h2>集成测试更少关注内部结构</h2><p>让我们做个小实验：假设我们必须停止使用列索引从结果集中提取数据，而必须依赖列名。因此，在<code>isCompatibleWithBackend</code> 方法中</p>return rs.getInt(1) == 8 &amp;&amp; rs.getInt(2) == 15;
<p>我们将有</p>return rs.getInt("major") == 8 &amp;&amp; rs.getInt("minor") == 15;
<p>当我们重新运行这两项测试时，我们会发现与真正 Elasticsearch 的集成测试仍然顺利通过。然而，使用模拟的测试停止工作了，因为我们模拟的调用是<code>rs.getInt(int)</code> ，而不是<code>rs.getInt(String)</code> 。为了让它们通过，我们现在必须根据测试套件中的其他用例，要么模拟它们，要么同时模拟它们。</p><h2>集成测试可以成为杀死苍蝇的大炮</h2><p>即使不需要外部依赖，集成测试也能验证系统的行为。然而，这样使用它们通常会浪费执行时间和资源。让我们来看看<code>mostPublishedAuthorsInYears(int minYear, int maxYear)</code> 方法。前两行如下</p>assert minYear &lt;= maxYear;
String query = // here goes the query
<p>第一条语句是检查一个条件，它不以任何方式依赖于 Elasticsearch（或任何其他外部依赖）。因此，我们不需要启动任何容器，只需验证，如果<code>minYear</code> 大于<code>maxYear</code> ，就会抛出异常。</p><p>一个简单的模拟测试就足以确保这一点，而且测试速度快、不占用资源。设置好模拟后，我们就可以开始了：</p>BookSearcher systemUnderTest = new BookSearcher(esClient);

Assertions.assertThrows(
    AssertionError.class,
    () -&gt; systemUnderTest.mostPublishedAuthorsInYears(2012, 2000)
);
<p>在<a href="https://github.com/pioorg/testing-elasticsearch/blob/e800b4b2ab3d706efcafb9a8182480e69e475b86/src/test/java/testing_elasticsearch/BookSearcherMockingTest.java#L89">此测试用例</a>中，启动依赖关系而不是模拟是一种浪费，因为根本不可能对该依赖关系进行有意义的调用。</p><p>但是，要验证从<code>String query = ...</code> 开始的行为，即查询是正确编写的，结果与预期一致：客户端库能够发送正确的请求和响应，语法没有变化，因此使用集成测试更容易，例如</p>@BeforeEach
void setupDataInContainer() {
    // here we initialise data in the Elasticsearch running in a container
}

@Test
void shouldGiveMostPublishedAuthorsInGivenYears() {
    var systemUnderTest = new BookSearcher(client);
    var list = systemUnderTest.mostPublishedAuthorsInYears(1800, 2010);
    Assertions.assertEquals("Beatrix Potter", list.get(12).author(), "Beatrix Potter was 13th most published author between 1800 and 2010");
}
<p>这样，我们就可以放心，当我们将数据输入到 Elasticsearch 时（不管是现在的版本还是我们选择迁移到的未来版本），我们的查询将完全符合我们的预期：数据格式没有改变，查询仍然有效，所有中间件（客户端、驱动程序、安全性等）都将继续工作。我们不必担心模拟的更新问题，唯一需要做的改动就是确保与诸如................<code>8.15</code> 将改变这一点：</p>static final String ELASTICSEARCH_IMAGE = "docker.elastic.co/elasticsearch/elasticsearch:8.15.0";
<p>如果您决定例如使用传统的 QueryDSL 而不是 ES|QL：从查询中获得的结果（无论使用哪种语言）应该是一样的。</p><h2>必要时使用两种方法</h2><p><code>mostPublishedAuthorsInYears</code> 的情况说明，一种方法可以同时使用两种方法进行测试。也许还应该是这样。</p><ul><li><p>只使用模拟系统意味着我们必须维护模拟系统，并且在升级系统时没有信心。</p></li><li><p>只使用集成测试意味着我们浪费了大量资源，却根本不需要这些资源。</p></li></ul><h2>让我们回顾一下</h2><ul><li><p>可以在 Elasticsearch 中同时使用模拟测试和集成测试。</p></li><li><p>使用模拟测试作为快速检测网，只有当测试成功通过后，才启动带有依赖关系的测试（如使用<code>./mvnw test '-Dtest=!TestInt*' &amp;&amp; ./mvnw test '-Dtest=TestInt*'</code> 或<a href="https://maven.apache.org/surefire/maven-failsafe-plugin/">Failsafe</a>和<a href="https://maven.apache.org/surefire/maven-surefire-plugin/">Surefire</a>插件）。</p></li><li><p>在测试系统行为（"行代码" ）时使用 mock，与外部依赖关系的整合并不重要（甚至可以省略）。</p></li><li><p>使用集成测试来验证对外部系统的假设以及与外部系统的集成。</p></li><li><p>不要害怕同时使用两种方法进行测试，如果这样做有意义的话。</p></li></ul><p>有人可能会说，对版本（在我们的情况下是<code>8.15.x</code> ）如此严格的要求太过分了。仅使用版本标签也可以，但请注意，在本帖中，它代表了不同版本之间可能发生变化的所有其他功能。</p><p>在<a href="https://www.elastic.co/search-labs/blog/automated-integration-tests-faster-elasticsearch">本系列的下一篇</a>中，我们将介绍如何使用测试数据集初始化在测试容器中运行的 Elasticsearch。如果您根据本博客创建了任何内容，或有任何问题，请通过我们的<a href="https://discuss.elastic.co/">讨论论坛</a>和<a href="https://communityinviter.com/apps/elasticstack/elastic-community">社区 Slack 频道</a>告诉我们。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/tests-with-mocks-and-real-elasticsearch</guid>
    <category><![CDATA[Java]]></category>
    <dc:creator><![CDATA[Piotr Przybyl]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7e4d003f09dfbe50/6a1709aba929cf810aae0957/b6bb727815ebdb844aeb36d5c44cdf3657f0e4bc-720x420.jpg" length="0" type="image/jpeg"/>
    <pubDate>Thu, 03 Oct 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[引入 LangChain4j 以简化 LLM 与 Java 应用程序的集成]]></title>
    <description><![CDATA[LangChain4j（LangChain for Java）是一个功能强大的工具集，可用于在纯 Java 中构建 RAG 应用程序。]]></description>
    <content:encoded><![CDATA[<p><a href="https://docs.langchain4j.dev/">LangChain4j 框架</a>正是<a href="https://github.com/langchain4j/langchain4j/blob/main/README.md#introduction">以此为目标</a>于 2023 年创建的：</p>LangChain4j 的目标是简化将 LLM 集成到 Java 应用程序中的过程。<p>LangChain4j 提供了一种标准的方法：</p><ul><li><p>根据给定内容（例如文本）创建嵌入向量</p></li><li><p>将嵌入式存储在嵌入式存储库中</p></li><li><p>在嵌入存储中搜索相似向量</p></li><li><p>与法律硕士讨论</p></li><li><p>使用聊天记忆功能，记住与法律硕士讨论的来龙去脉</p></li></ul><p>此列表并不详尽，LangChain4j 社区一直在实施新功能。</p><p>本帖将介绍该框架的第一个主要部分。</p><h2>将 LangChain4j OpenAI 添加到我们的项目中</h2><p>与所有 Java 项目一样，这只是一个依赖关系问题。这里我们将使用 Maven，但任何其他依赖管理器也可以实现同样的功能。</p><p>作为构建项目的第一步，我们将使用 OpenAI，因此只需添加<code>langchain4j-open-ai</code> 工具：</p>&lt;properties&gt;
  &lt;langchain4j.version&gt;0.34.0&lt;/langchain4j.version&gt;
&lt;/properties&gt;

&lt;dependencies&gt;
  &lt;dependency&gt;
    &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
    &lt;artifactId&gt;langchain4j-open-ai&lt;/artifactId&gt;
    &lt;version&gt;${langchain4j.version}&lt;/version&gt;
  &lt;/dependency&gt;
&lt;/dependencies&gt;
<p>在其余代码中，我们将使用自己的 API 密钥（可通过注册<a href="https://platform.openai.com/signup/">OpenAI</a> 账户获得），或者 LangChain4j 项目提供的 API 密钥（仅供演示使用）：</p>static String getOpenAiApiKey() {
  String apiKey = System.getenv(API_KEY_ENV_NAME);
  if (apiKey == null || apiKey.isEmpty()) {
    Logger.warn("Please provide your own key instead using [{}] env variable", API_KEY_ENV_NAME);
    return "demo";
  }
  return apiKey;
}
<p>现在我们可以创建 ChatLanguageModel 的实例：</p>ChatLanguageModel model = OpenAiChatModel.withApiKey(getOpenAiApiKey());
<p>最后，我们可以问一个简单的问题，并得到答案：</p>String answer = model.generate("Who is Thomas Pesquet?");
Logger.info("Answer is: {}", answer);
<p>给出的答案可能是这样的</p>Thomas Pesquet is a French aerospace engineer, pilot, and European Space Agency astronaut.
He was selected as a member of the European Astronaut Corps in 2009 and has since completed 
two space missions to the International Space Station, including serving as a flight engineer 
for Expedition 50/51 in 2016-2017. Pesquet is known for his contributions to scientific 
research and outreach activities during his time in space.
<p>如果您想运行这段代码，请查看<a href="https://github.com/dadoonet/langchain4j-demo/blob/main/src/test/java/fr/pilato/demo/Step1AiChatTest.java">Step1AiChatTest.java</a>类。</p><h2>使用 langchain4j 提供更多语境</h2><p>让我们添加<code>langchain4j</code> 手工艺品：</p>&lt;dependency&gt;
  &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
  &lt;artifactId&gt;langchain4j&lt;/artifactId&gt;
  &lt;version&gt;${langchain4j.version}&lt;/version&gt;
&lt;/dependency&gt;
<p>它提供了一个工具集，可以帮助我们建立更高级的 LLM 集成，以构建我们的助手。在这里，我们只需创建一个<code>Assistant</code> 接口，该接口提供的<code>chat</code> 方法将自动调用我们之前定义的<code>ChatLanguageModel</code> ：</p>interface Assistant {
  String chat(String userMessage);
}
<p>我们只需请求 LangChain4j<code>AiServices</code> 类为我们构建一个实例：</p>Assistant assistant = AiServices.create(Assistant.class, model);
<p>然后调用<code>chat(String)</code> 方法：</p>String answer = assistant.chat("Who is Thomas Pesquet?");
Logger.info("Answer is: {}", answer);
<p>这与之前的行为相同。那么，我们为什么要修改代码呢？首先，它更优雅，但更重要的是，你现在可以使用简单的注释向 LLM 发出一些指示：</p>interface Assistant {
  @SystemMessage("Please answer in a funny way.")
  String chat(String userMessage);
}
<p>这就是现在的奉献：</p>Ah, Thomas Pesquet is actually a super secret spy disguised as an astronaut! 
He's out there in space fighting aliens and saving the world one spacewalk at a time. 
Or maybe he's just a really cool French astronaut who has been to the International 
Space Station. But my spy theory is much more exciting, don't you think?
<p>如果您想运行这段代码，请查看<a href="https://github.com/dadoonet/langchain4j-demo/blob/main/src/test/java/fr/pilato/demo/Step2AssistantTest.java">Step2AssistantTest.java</a>类。</p><h2>转到另一个 LLM：langchain4j-ollama</h2><p>我们可以利用伟大的<a href="https://ollama.com/">奥拉马项目</a>。在本地计算机上运行 LLM 会有帮助。</p><p>让我们添加<code>langchain4j-ollama</code> 手工艺品：</p>&lt;dependency&gt;
  &lt;groupId&gt;dev.langchain4j&lt;/groupId&gt;
  &lt;artifactId&gt;langchain4j-ollama&lt;/artifactId&gt;
  &lt;version&gt;${langchain4j.version}&lt;/version&gt;
&lt;/dependency&gt;
<p>由于我们要使用测试来运行示例代码，因此让我们在项目中添加<a href="https://java.testcontainers.org/">Testcontainers</a>：</p>&lt;dependency&gt;
  &lt;groupId&gt;org.testcontainers&lt;/groupId&gt;
  &lt;artifactId&gt;ollama&lt;/artifactId&gt;
  &lt;version&gt;1.20.1&lt;/version&gt;
  &lt;scope&gt;test&lt;/scope&gt;
&lt;/dependency&gt;
<p>现在我们可以启动/停止 Docker 容器了：</p>static String MODEL_NAME = "mistral";
static String DOCKER_IMAGE_NAME = "langchain4j/ollama-" + MODEL_NAME + ":latest";

static OllamaContainer ollama = new OllamaContainer(
  DockerImageName.parse(DOCKER_IMAGE_NAME).asCompatibleSubstituteFor("ollama/ollama"));

@BeforeAll
public static void setup() {
  ollama.start();
}

@AfterAll
public static void teardown() {
  ollama.stop();
}
<p>我们只需"，" ，将<code>model</code> 对象改为<code>OllamaChatModel</code> ，而不是之前使用的<code>OpenAiChatModel</code> ：</p>OllamaChatModel model = OllamaChatModel.builder()
  .baseUrl(ollama.getEndpoint())
  .modelName(MODEL_NAME)
  .build();
<p>请注意，提取图像及其模型可能需要一些时间，但一段时间后，您就可以得到答案了：</p>Oh, Thomas Pesquet, the man who single-handedly keeps the French space program running 
while sipping on his crisp rosé and munching on a baguette! He's our beloved astronaut 
with an irresistible accent that makes us all want to learn French just so we can 
understand him better. When he's not floating in space, he's probably practicing his 
best "je ne sais quoi" face for the next family photo. Vive le Thomas Pesquet! 
🚀🌍🇫🇷 #FrenchSpaceHero
<h2>记忆力更好</h2><p>如果我们提出多个问题，系统默认情况下不会记住之前的问题和答案。因此，如果我们在第一个问题之后提问"他是什么时候出生的？" 、我们的应用程序会回答：</p>Oh, you're asking about this legendary figure from history, huh? Well, let me tell 
you a hilarious tale! He was actually born on Leap Year's Day, but only every 400 
years! So, do the math... if we count backwards from 2020 (which is also a leap year), 
then he was born in... *drumroll please* ...1600! Isn't that a hoot? But remember 
folks, this is just a joke, and historical records may vary.
<p>这是无稽之谈。相反，我们应该使用<a href="https://docs.langchain4j.dev/tutorials/chat-memory">聊天记忆</a>：</p>ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
Assistant assistant = AiServices.builder(Assistant.class)
  .chatLanguageModel(model)
  .chatMemory(chatMemory)
  .build();
<p>现在运行同样的问题就能得到有意义的答案：</p>Oh, Thomas Pesquet, the man who was probably born before sliced bread but after dinosaurs! 
You know, around the time when people started putting wheels on suitcases and calling it 
a revolution. So, roughly speaking, he came into this world somewhere in the late 70s or 
early 80s, give or take a year or two - just enough time for him to grow up, become an 
astronaut, and make us all laugh with his space-aged antics! Isn't that a hoot? 
*laughs maniacally*
<h2>结论</h2><p>在<a href="https://www.elastic.co/search-labs/blog/langchain4j-elasticsearch-embedding-store">下一篇文章</a>中，我们将了解如何使用 Elasticsearch 作为嵌入存储，向我们的私有数据集提问。这将为我们提供一种方法，使我们的应用程序搜索工作更上一层楼。</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/langchain4j-llm-integration-introduction</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/langchain4j-llm-integration-introduction</guid>
    <category><![CDATA[Java]]></category>
    <category><![CDATA[AI]]></category>
    <dc:creator><![CDATA[David Pilato]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0435ed6d14579089/6a17e79ae8fbce7e433a192f/cf129b8b25fbe7204e2adca8fca5fec04207f096-720x720.png" length="0" type="image/png"/>
    <pubDate>Mon, 23 Sep 2024 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>