<?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/kr/search-labs/blog/category/java-programming</link>
    </image>
    <link>https://www.elastic.co/kr/search-labs/blog/category/java-programming</link>
    <atom:link href="https://www.elastic.co/kr/search-labs/rss/category/java-programming.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[kr]]></language>
    <lastBuildDate>Wed, 23 Sep 2026 10:46:05 GMT</lastBuildDate>
  <item>
    <title><![CDATA[Elasticsearch를 임베딩 저장소로 활용한 LangChain4j]]></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> 를 구현하여 LLM과 토론하고 <code>ChatMemory</code></p></li><li><p>메모리에 채팅 기록을 유지하여 LLM과의 이전 토론의 맥락을 기억할 수 있습니다.</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> 사용한 것과 동일한 미스트랄 모델을 사용할 수 있습니다. 올라마와 함께 실행 중이었습니다:</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> 을 사용할 수 있습니다. 예를 들어, 여기에 게임 이름을 메타데이터 필드로 추가합니다:</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에 저장하는 대신 "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>예상한 대로 "아웃런" 이 첫 번째 히트작이 됩니다:</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>이 경우 "주문" 결과는 변경되지 않지만 <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[모의 테스트와 Testcontainer를 사용하여 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>시스템 운영자 및 SRE: 시스템 배포 및 유지 관리가 더 쉬워지므로 더 편안합니다.</p></li></ul><p>마지막으로 중요한 것은 시스템의 아키텍처입니다. 시스템이 체계적이고 유지 관리가 쉬우며 아키텍처가 깔끔하고 목적에 부합할 때 저희는 이를 좋아합니다. 그러나 때때로 우리는 "이 방법이 더 테스트하기 쉽다는 핑계로 너무 많은 것을 희생하는 아키텍처를 볼 수 있습니다". 시스템이 그 존재를 정당화하는 필요를 충족시키는 대신 주로 테스트 가능하도록 작성된 경우, 꼬리가 개를 흔드는 상황을 보게 됩니다.</p><h2>두 가지 종류의 테스트: 모의 &amp; 종속성</h2><p>테스트는 여러 가지 방법으로 볼 수 있으며, 따라서 분류할 수도 있습니다. 이 글에서는 테스트 분할의 한 가지 측면, 즉 모의(또는 스텁, 가짜 등)를 사용하는 것과 실제 종속성을 사용하는 것에 대해서만 집중적으로 살펴보겠습니다. 저희의 경우 종속성은 Elasticsearch입니다.</p><p>모의 테스트를 사용하는 테스트는 외부 종속성을 시작할 필요가 없고 모든 것이 메모리 내에서만 이루어지기 때문에 매우 빠릅니다. 자동화된 테스트에서 모킹은 실제 종속성을 사용하지 않고 프로그램의 일부를 테스트하기 위해 실제 객체 대신 가짜 객체를 사용하는 것을 말합니다. 이것이 바로 이러한 기능이 필요한 이유이며, 예를 들어 빠른 탐지 네트워크 테스트에서 빛을 발하는 이유입니다. 입력 유효성 검사. 예를 들어 요청에 음수가 허용되지 않는지 확인하기 위해서만 데이터베이스를 시작하고 호출할 필요가 없습니다.</p><p>하지만 모의고사를 도입하는 데에는 몇 가지 의미가 있습니다:</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>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>예를 들어, 코드가 이전 버전과 호환되는지 확실하지 않고 이전 버전과 호환되지 않는지 확실하지 않기 때문에 유일한 종속성( <code>isCompatibleWithBackend()</code> 참조)으로서 정확히 버전 <code>8.15.x</code> 의 Elasticsearch가 필요합니다. 프로덕션 환경의 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>앞서 말했듯이 모의 테스트를 통해 모든 것을 쉽게 테스트할 수 있는 것은 아닙니다. 하지만 우리가 할 수 있고 해야만 하는 일들도 있습니다. 지금은 <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/">이를 위해 테스트컨테이너의 Elasticsearch 모듈을</a> 사용하려고 하는데, 이 모듈의 요구 사항은 단 한 가지, 즉 Docker에 액세스할 수 있어야 한다는 것입니다. 어떤 각도에서 보면 테스트 컨테이너는 단순히 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>이 예에서는 <code>@Testcontainers</code> 및 <code>@Container</code> 과의 <a href="https://java.testcontainers.org/test_framework_integration/junit_5/">Testcontainers의 JUnit 통합을</a> 사용하므로, 테스트 전에 Elasticsearch를 시작하고 테스트 후에 중지하는 것에 대해 걱정할 필요가 없습니다. 각 테스트 전에 클라이언트를 생성하고 각 테스트 후에 클라이언트를 닫기만 하면 됩니다(더 큰 테스트 세트에 영향을 줄 수 있는 리소스 누수를 방지하기 위해).</p><p>정적이 아닌 필드에 <code>@Container</code> 주석을 달면 각 테스트마다 새 컨테이너가 시작되므로 오래된 데이터나 컨테이너의 상태 재설정에 대해 걱정할 필요가 없습니다. 그러나 많은 테스트에서 이 접근 방식은 성능이 좋지 않을 수 있으므로 다음 게시물 중 하나에서 다른 접근 방식과 비교해보겠습니다.</p><p><strong>참고:</strong></p><code>docker.elastic.co</code> (Elastic의 공식 Docker 이미지 리포지토리)를 사용하면 Docker 허브의 한계를 초과하지 않아도 됩니다.

또한 테스트 환경과 프로덕션 환경에서 동일한 버전의 종속성을 사용하여 호환성을 최대한 보장하는 것이 좋습니다. 또한 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> 보안 및 SSL/TLS가 활성화된 상태에서도 테스트 컨테이너에서 실행 중인 Elasticsearch에 연결할 수 있습니다(버전 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"></a>처럼) 인증서는 <code>elasticsearch.caCertAsBytes()</code> ()를 사용하여 얻을 수 <code>Optional&lt;byte[]&gt;</code> 있으며, 또 다른 편리한 방법은 를 <code>SSLContext</code> 사용하여 를 얻는 <code>createSslContextFromCa()</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> 에 제공하기만 하면 됩니다.</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(String)</code> 이 아닌 <code>rs.getInt(int)</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>다음과 같이 결정한 경우에도 마찬가지입니다. ES|QL 대신 기존의 QueryDSL을 사용하면 언어에 관계없이 쿼리에서 받는 결과는 여전히 동일해야 합니다.</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>외부 종속성과의 통합이 중요하지 않거나 건너뛸 수 있는 시스템 동작("코드" 줄 )을 테스트할 때는 모의 테스트를 사용하세요.</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[Java 애플리케이션에 LLM 통합을 간소화하는 LangChain4j 소개]]></title>
    <description><![CDATA[LangChain4j(Java용 LangChain)는 일반 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>LLM과 논의</p></li><li><p>채팅 메모리를 사용하여 LLM과의 토론 컨텍스트 기억하기</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>나머지 코드에서는 <a href="https://platform.openai.com/signup/">OpenAI에</a> 계정을 등록하여 얻을 수 있는 자체 API 키 또는 데모 목적으로만 LangChain4j 프로젝트에서 제공하는 키를 사용할 것입니다:</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>ChatLanguageModel</code> 을 자동으로 호출하는 <code>chat</code> 메서드를 제공하는 <code>Assistant</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/">Testcontainer를</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>OpenAiChatModel</code> <code>OllamaChatModel</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>