<?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[Lucene - 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[Lucene - 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/lucene</link>
    </image>
    <link>https://www.elastic.co/kr/search-labs/blog/category/lucene</link>
    <atom:link href="https://www.elastic.co/kr/search-labs/rss/category/lucene.xml" rel="self" type="application/rss+xml"/>
    <language><![CDATA[kr]]></language>
    <lastBuildDate>Mon, 28 Sep 2026 16:47:21 GMT</lastBuildDate>
  <item>
    <title><![CDATA[벡터 검색 필터링: 관련성 유지]]></title>
    <description><![CDATA[쿼리와 가장 유사한 결과를 찾기 위해 벡터 검색을 수행하는 것만으로는 충분하지 않습니다. 검색 결과의 범위를 좁히기 위해 필터링이 필요한 경우가 많습니다. 이 문서에서는 Elasticsearch와 Apache Lucene에서 벡터 검색을 위한 필터링이 어떻게 작동하는지 설명합니다.]]></description>
    <content:encoded><![CDATA[<p>벡터 검색만으로는 관련 검색 결과를 찾을 수 없습니다. 검색 결과의 범위를 좁히고 관련 없는 결과를 걸러내는 데 도움이 되는 필터링 기준을 사용하는 것은 매우 일반적입니다.</p><p>벡터 검색에서 필터링이 작동하는 방식을 이해하면 성능과 회상률의 균형을 맞추는 데 도움이 될 뿐만 아니라 필터링 사용 시 벡터 검색의 성능을 높이는 데 사용되는 몇 가지 최적화를 알아볼 수 있습니다.</p><h2>왜 필터링할까요?</h2><p>벡터 검색은 대규모 데이터 세트에서 관련 정보를 찾는 방법을 혁신적으로 개선하여 검색어와 의미적으로 유사한 항목을 발견할 수 있게 해줍니다.</p><p>하지만 단순히 비슷한 아이템을 찾는 것만으로는 충분하지 않습니다. 특정 기준이나 속성에 따라 검색 결과의 범위를 좁혀야 하는 경우가 많습니다.</p><p>이커머스 스토어에서 제품을 검색하고 있다고 상상해 보세요. 순수한 벡터 검색은 시각적으로 유사한 상품을 보여줄 수 있지만 가격대, 브랜드, 재고 여부 또는 고객 평점을 기준으로 필터링할 수도 있습니다. 필터링이 없으면 유사한 상품이 너무 많이 표시되어 원하는 상품을 정확히 찾기 어렵습니다.</p><p>필터링을 통해 검색 결과를 정밀하게 제어할 수 있으므로 검색된 항목이 의미적으로 일치할 뿐만 아니라 필요한 모든 요건을 충족하는지 확인할 수 있습니다. 이를 통해 훨씬 더 정확하고 효율적이며 사용자 친화적인 검색 환경을 제공합니다.</p><p>다양한 데이터 유형에 걸쳐 효과적인 필터링을 사용하는 것이 다른 벡터 데이터베이스와의 주요 차이점 중 하나인 Elasticsearch와 Apache Lucene의 장점입니다.</p><h2>정확한 벡터 검색을 위한 필터링</h2><p>정확한 벡터 검색을 수행하는 방법에는 크게 두 가지가 있습니다:</p><ul><li><p>dense_vector 필드에 <code>flat</code> 인덱스 유형을 사용합니다. 따라서 <code>knn</code> 검색은 대략적인 검색이 아닌 정확한 검색을 사용합니다.</p></li><li><p>벡터 함수를 사용하여 점수를 계산하는 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-script-score-query#vector-functions">스크립트_점수 쿼리를</a> 사용합니다. 모든 인덱스 유형에 사용할 수 있습니다.</p></li></ul><p>정확한 벡터 검색을 실행할 때는 모든 벡터가 쿼리와 비교됩니다. 이 시나리오에서는 필터를 통과한 벡터만 비교하면 되므로 필터링이 성능에 도움이 됩니다.</p><p>어쨌든 모든 벡터가 고려되므로 결과 품질에는 영향을 미치지 않습니다. 흥미롭지 않은 결과를 미리 필터링하여 작업 횟수를 줄일 수 있습니다.</p><p>적용된 필터로 인해 문서 수가 적은 경우 대략적인 검색 대신 정확한 검색을 실행하는 것이 더 효율적일 수 있으므로 이는 매우 중요합니다.</p><p>필터를 통과하는 문서가 1만 개 미만인 경우 정확한 검색을 사용하는 것이 좋습니다. <a href="https://www.elastic.co/search-labs/blog/better-binary-quantization-lucene-elasticsearch">BBQ</a> 인덱스가 비교에 훨씬 빠르므로 기준 인덱스가 10만 개 미만일 때는 정확한 검색을 사용하는 것이 좋습니다. 자세한 내용은 <a href="https://www.elastic.co/search-labs/blog/knn-exact-vs-approximate-search">이 블로그 게시물을</a> 확인하세요.</p><p>필터가 항상 매우 제한적인 경우에는 HNSW 기반 인덱스 유형 대신 <code>flat</code> 인덱스 유형을 사용하여 대략적인 검색 대신 정확한 검색에 초점을 맞춘 인덱싱을 고려할 수 있습니다. 자세한 내용은 <a href="https://www.elastic.co/docs/reference/elasticsearch/mapping-reference/dense-vector#dense-vector-params">index_options의 속성을</a> 참조하세요.</p><h2>대략적인 벡터 검색을 위한 필터링</h2><p>근사 벡터 검색을 실행할 때는 결과 정확도와 성능을 맞바꿉니다. HNSW와 같은 벡터 검색 데이터 구조는 수백만 개의 벡터에서 대략적인 가장 가까운 이웃을 효율적으로 검색합니다. 계산 비용이 많이 드는 벡터 비교를 최소한으로 수행하여 가장 유사한 벡터를 검색하는 데 중점을 둡니다.</p><p>즉, 다른 필터링 속성은 벡터 데이터의 일부가 아닙니다. 용어 사전, 게시 목록, 문서 값 등 데이터 유형마다 이를 찾고 필터링하는 데 효율적인 자체 인덱싱 구조가 있습니다.</p><p>이러한 데이터 구조가 벡터 검색 메커니즘과 분리되어 있다면 벡터 검색에 필터링을 적용하려면 어떻게 해야 할까요? 벡터 검색 후 필터를 적용하거나(사후 필터링) 벡터 검색 전에 필터를 적용하는(사전 필터링) 두 가지 옵션이 있습니다.</p><p>각 옵션에는 장단점이 있습니다. 더 자세히 알아봅시다!</p><h3>사후 필터링</h3><p>사후 필터링은 벡터 검색이 완료된 후 필터를 적용합니다. 즉, 필터는 가장 유사한 벡터 결과 상위 k개를 찾은 후에 적용됩니다.</p><p>물론 결과에 필터를 적용한 후 잠재적으로 k보다 적은 결과를 얻을 수 있습니다. 물론 벡터 검색에서 더 많은 결과를 검색할 수 있지만(k 값이 높을수록) 필터를 적용한 후에도 k 이상의 결과를 얻을 수 있을지는 확신할 수 없습니다.</p><p>사후 필터링의 장점은 벡터 검색의 런타임 동작을 변경하지 않는다는 점입니다. 벡터 검색은 필터링을 인식하지 못합니다. 하지만 검색되는 최종 결과 수는 변경됩니다.</p><p>다음은 <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query">knn 쿼리를</a> 사용한 사후 필터링의 예입니다. 필터링 절이 knn 쿼리와 분리되어 있는지 확인합니다:</p>{
  "query": {
    "bool": {
      "must": {
        "knn": {
          "field": "image-vector",
          "query_vector": [54, 10, -2],
          "k": 5,
          "num_candidates": 50
        }
      },
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}<p><a href="https://www.elastic.co/docs/reference/elasticsearch/rest-apis/filter-search-results#post-filter">포스트 필터를</a> 사용하여 KNN 검색에 포스트 필터링도 사용할 수 있습니다:</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, 2],
    "k": 5,
    "num_candidates": 50
  },
  "post_filter": {
    "term": {
      "file-type": "png"
    }
  }
}<p>knn 검색에 명시적인 사후 필터 섹션을 사용해야 한다는 점에 유의하세요. 사후 필터를 사용하지 않는 경우 knn 검색은 <a href="https://www.elastic.co/docs/solutions/search/vector/knn#_combine_approximate_knn_with_other_features">사후 필터를 수행하는 대신 가장</a> 가까운 이웃 검색 결과를 다른 쿼리 또는 필터와 결합합니다.</p><h3>사전 필터링</h3><p>벡터 검색 전에 필터를 적용하면 먼저 필터를 충족하는 문서를 검색한 다음 해당 정보를 벡터 검색에 전달합니다.</p><p>Lucene은 <a href="https://github.com/apache/lucene/blob/7a60d7ce92392181e137361336e5196bd486cdd9/lucene/core/src/java/org/apache/lucene/util/BitSet.java">비트셋을</a> 사용하여 필터 조건을 충족하는 문서를 효율적으로 저장합니다. 그런 다음 벡터 검색은 조건을 충족하는 문서를 고려하여 HNSW 그래프를 탐색합니다. 결과에 후보를 추가하기 전에 유효한 문서의 비트 집합에 포함되어 있는지 확인합니다.</p><p>그러나 유효한 문서가 아니더라도 후보를 탐색하고 쿼리와 비교해야 합니다. HNSW의 효과는 그래프에서 벡터 간의 연결에 따라 달라지는데, 한 후보 탐색을 중단하면 이웃 후보도 건너뛸 수 있습니다.</p><p>주유소에 가기 위해 운전한다고 생각하세요. 주유소가 없는 도로를 버리면 목적지까지 갈 수 없을 가능성이 높습니다. 다른 길은 내가 원하는 길이 아닐 수도 있지만 목적지까지 <em>연결해</em> 줍니다. HNSW 그래프의 벡터도 마찬가지입니다!</p><p>따라서 사전 필터링을 적용하는 것이 필터를 적용하지 않는 것보다 성능이 떨어집니다. 검색에서 방문하는 <em>모든</em> 벡터에 대한 작업을 수행해야 하며 필터와 일치하지 않는 벡터는 버려야 합니다. 최고의 결과를 얻기 위해 더 많은 노력을 기울이고 더 많은 시간을 투자하고 있습니다.</p><p>다음은 Elasticsearch 쿼리 DSL에서 사전 필터링의 예입니다. 필터링 절이 이제 knn 섹션의 일부가 되었는지 확인합니다:</p>{
  "knn": {
    "field": "image-vector",
    "query_vector": [54, 10, -2],
    "k": 5,
    "num_candidates": 50,
    "filter": {
      "term": {
        "file-type": "png"
      }
    }
  }
}<p>사전 필터링은 <a href="https://www.elastic.co/docs/solutions/search/vector/knn#knn-search-filter-example">knn 검색과</a> <a href="https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-knn-query#knn-query-filtering">knn 쿼리</a> 모두에 사용할 수 있습니다:</p>{
  "query": {
    "knn": {
      "field": "image-vector",
      "query_vector": [-5, 9, -12],
      "k": 5,
      "filter": {
        "term": {
          "file-type": "png"
        }
      }
    }
  }
}<h4>사전 필터링 최적화</h4><p>사전 필터링의 성능을 보장하기 위해 적용할 수 있는 몇 가지 최적화가 있습니다.</p><p>필터가 매우 제한적인 경우 정확한 검색으로 전환할 수 있습니다. 비교할 벡터가 적을 때는 필터를 만족하는 소수의 문서에 대해 정확한 검색을 수행하는 것이 더 빠릅니다.</p><p>이것은 <a href="https://github.com/apache/lucene/blob/eb876b618da5d04c1ad14b04a48321638318493a/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L218">Lucene과</a> Elasticsearch에서 자동으로 적용되는 최적화입니다.</p><p>또 다른 최적화 방법은 필터를 만족하지 않는 벡터를 무시하는 것입니다. 대신 이 메서드는 필터를 통과한 필터링된 벡터의 이웃을 확인합니다. 이 접근 방식은 필터링된 벡터를 고려하지 않고 현재 경로에 연결된 벡터를 계속 탐색하므로 비교 횟수를 효과적으로 줄일 수 있습니다.</p><p>이 알고리즘은 ACORN-1이며, 그 과정은 <a href="https://www.elastic.co/search-labs/blog/filtered-hnsw-knn-search">이 블로그 게시물에</a> 자세히 설명되어 있습니다.</p><h2>문서 수준 보안을 사용한 필터링</h2><p><a href="https://www.elastic.co/docs/deploy-manage/users-roles/cluster-or-deployment-auth/controlling-access-at-document-field-level#document-level-security">DLS(문서 수준 보안)</a> 는 사용자 역할이 검색할 수 있는 문서를 지정하는 Elasticsearch 기능입니다.</p><p>DLS는 쿼리를 사용하여 수행됩니다. 역할에는 인덱스와 연결된 쿼리가 있을 수 있으며, 이 쿼리는 해당 역할에 속한 사용자가 인덱스에서 검색할 수 있는 문서를 효과적으로 제한합니다.</p><p>역할 쿼리는 필터로 사용되어 <a href="https://github.com/elastic/elasticsearch/blob/c3a1cb34294e902a9f46d7e840ea09965019f456/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapper.java#L92">일치하는 문서를 검색하고</a> 비트셋으로 캐시됩니다. 그런 다음 이 비트셋은 기본 Lucene 리더를 래핑하는 데 사용되므로 쿼리에서 반환된 문서, 즉 인덱스에 존재하고 삭제되지 않은 문서만 <em>라이브</em>문서로 간주됩니다.</p><p>knn 쿼리를 수행하기 위해 리더에서 라이브 문서가 검색되므로 사용자가 사용할 <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L196">수 있는 문서만 고려됩니다.</a> 프리필터가 있는 경우 DLS 문서가 프리필터에 <a href="https://github.com/apache/lucene/blob/a211d30097a8e3264d3ef073a054bd31eb847231/lucene/core/src/java/org/apache/lucene/search/AbstractKnnVectorQuery.java#L204">추가됩니다</a>.</p><p>즉, DLS 필터링은 근사 벡터 검색을 위한 프리필터로 작동하며 성능에 미치는 영향과 최적화가 동일합니다.</p><p>정확한 검색을 사용하는 DLS는 필터를 적용하는 것과 동일한 이점이 있습니다. DLS에서 검색되는 문서가 적을수록 정확한 검색의 성능이 향상됩니다. DLS 역할이 매우 제한적인 경우 대략적인 검색 대신 정확한 검색을 사용하는 것도 고려할 수 있습니다.</p><h2>벤치마킹</h2><p>Elasticsearch에서는 벡터 검색 필터링이 효율적인지 확인하고자 합니다. 벡터 <a href="https://elasticsearch-benchmarks.elastic.co/#tracks/so_vector/nightly/default/90d">필터링에 대한 특정 벤치마크가</a> 있어 다양한 필터링을 통해 대략적인 벡터 검색을 수행하여 벡터 검색이 관련성 있는 결과를 최대한 빠르게 검색할 수 있도록 합니다.</p><p>ACORN-1 도입 시 <a href="https://elasticsearch-benchmark-analytics.elastic.co/app/dashboards#/view/43b63e80-5ba2-11ed-aede-a742809feed4?_g=(refreshInterval:(pause:!t,value:60000),time:(from:'2025-05-28T01:27:58.456Z',to:'2025-06-30T13:53:26.430Z'))&amp;_a=()">개선</a> 사항을 확인하세요. 필터를 통과한 벡터가 2개% 뿐인 테스트의 경우 쿼리 지연 시간은 원래 지속 시간의 55% 로 줄어듭니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt820cb0b715cf291c/6a17e227dbb4ff8d49fb5615/3eac3748a33376fc97d957364a5c1f5108d5c58b-1023x896.png" alt="" /><h2>결론</h2><p>필터링은 검색의 필수적인 부분입니다. 벡터 검색에서 필터링이 제대로 작동하는지 확인하고 장단점과 최적화를 이해하는 것이 효율적이고 정확한 검색의 성패를 좌우합니다.</p><p>필터링은 벡터 검색의 성능에 영향을 미칩니다:</p><ul><li><p>필터링을 사용하면 정확한 검색이 더 빠릅니다. 필터링이 충분히 제한적인 경우 대략적인 검색 대신 정확한 검색을 사용하는 것이 좋습니다. 이것은 Elasticsearch의 자동 최적화입니다.</p></li><li><p>사전 필터링 사용 시 대략적인 검색 속도가 느려집니다. 사전 필터링을 사용하면 검색 속도가 느려지는 대신 필터와 일치하는 상위 k개의 결과를 얻을 수 있습니다.</p></li><li><p>사후 필터링은 필터를 적용할 때 필터를 통해 필터링될 수 있으므로 반드시 상위 k개의 결과를 검색하지는 않습니다.</p></li></ul><p>행복한 필터링!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/vector-search-filtering</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/vector-search-filtering</guid>
    <category><![CDATA[벡터 데이터베이스]]></category>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[Elastic 내부]]></category>
    <dc:creator><![CDATA[Carlos Delgado]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt39ede1736e0f1456/6a17e2282f4a5c5031fa8843/03b1dd4c7bda4fbabd8e374bc2e4f12d5be6ef5f-1600x1150.png" length="0" type="image/png"/>
    <pubDate>Wed, 03 Sep 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[HNSW 그래프 병합 속도 향상]]></title>
    <description><![CDATA[여러 개의 HNSW 그래프를 작성하는 데 드는 오버헤드를 줄이기 위해, 특히 그래프 병합 비용을 줄이기 위해 저희가 해온 작업을 살펴보세요.]]></description>
    <content:encoded><![CDATA[<p>과거에는 여러 개의 <a href="https://www.elastic.co/kr/search-labs/blog/hnsw-graph">HNSW 그래프를</a> 검색해야 할 때 발생하는 몇 가지 문제와 이를 완화할 수 있는 방법에 대해 논의한<a href="https://www.elastic.co/kr/search-labs/blog/multi-graph-vector-search">적이 있습니다.</a> 당시 저희는 계획했던 몇 가지 추가 개선 사항을 암시했습니다. 이 게시물은 그 작업의 정점입니다.</p><p>왜 여러 개의 그래프를 사용해야 하나요? 이는 불변 세그먼트라는 루씬의 아키텍처 선택에 따른 부작용입니다. 대부분의 아키텍처 선택과 마찬가지로 장단점이 있습니다. 예를 들어, 저희는 최근에 서버리스 Elasticsearch를 정식 버전으로 출시했습니다. 이러한 맥락에서 우리는 효율적인 인덱스 복제, 인덱스와 쿼리 계산을 분리하고 독립적으로 자동 확장하는 기능 등 불변 세그먼트를 통해 매우 중요한 이점을 얻었습니다. 벡터 양자화의 경우 세그먼트 병합을 통해 데이터 특성에 맞게 파라미터를 업데이트할 수 있습니다. 이러한 맥락에서 데이터 특성을 측정하고 인덱싱 선택을 재검토할 수 있는 기회를 갖는다는 것은 다른 장점도 있다고 생각합니다.</p><p>이 글에서는 여러 개의 HNSW 그래프를 작성하는 데 드는 오버헤드를 크게 줄이고 특히 그래프를 병합하는 데 드는 비용을 줄이기 위해 수행한 작업에 대해 설명합니다.</p><h3>배경</h3><p>관리 가능한 세그먼트 수를 유지하기 위해 Lucene은 주기적으로 세그먼트를 병합해야 하는지 여부를 확인합니다. 이는 현재 세그먼트 수가 기본 세그먼트 크기와 병합 정책에 따라 결정되는 목표 세그먼트 수를 초과하는지 확인하는 것과 같습니다. 개수를 초과하면 제약 조건을 위반하는 동안 Lucene은 세그먼트 그룹을 병합합니다. 이 프로세스는 <a href="https://blog.mikemccandless.com/2011/02/visualizing-lucenes-segment-merges.html">다른 곳에</a> 자세히 설명되어 있습니다.</p><p>Lucene은 쓰기 증폭에서 대수적 증가를 달성하기 때문에 비슷한 크기의 세그먼트를 병합하는 방법을 선택합니다. 벡터 인덱스의 경우 쓰기 증폭은 벡터가 그래프에 삽입되는 횟수입니다. Lucene은 약 10개의 그룹으로 세그먼트를 병합하려고 시도합니다. 결과적으로 벡터는 대략 {10}\left (\frac{n}{n_0}\right )번 그래프에 삽입되며, 여기서  인덱스 벡터 수이고  예상되는 기본 세그먼트 벡터 수입니다. 대수적 증가로 인해 쓰기 증폭은 거대한 인덱스의 경우에도 한 자릿수입니다. 그러나 그래프를 병합하는 데 소요되는 총 시간은 쓰기 증폭에 선형적으로 비례합니다.</p><p>HNSW 그래프를 병합할 때 가장 큰 세그먼트의 그래프를 유지하고 다른 세그먼트의 벡터를 삽입하는 작은 최적화를 이미 수행했습니다. 이것이 위의 9/10 요소의 이유입니다. 아래에서는 병합하는 모든 그래프의 정보를 사용하여 훨씬 더 나은 결과를 얻을 수 있는 방법을 보여줍니다.</p><h3>HNSW 그래프 병합</h3><p>이전에는 가장 큰 그래프를 유지하고 벡터가 포함된 그래프를 무시하고 다른 그래프에서 벡터를 삽입했습니다. 아래에서 사용하는 핵심 인사이트는 우리가 폐기하는 각 HNSW 그래프에 포함된 벡터에 대한 중요한 근접성 정보가 포함되어 있다는 것입니다. 이 정보를 사용하여 적어도 일부 벡터의 삽입을 가속화하고자 합니다.</p><p>병합 정책을 구축하는 데 사용할 수 있는 원자 연산이므로 작은 그래프  )를 큰 그래프 )에 삽입하는 문제에 중점을 둡니다.</p><p>전략은 큰 그래프에 삽입할  정점 하위 집합을 찾는 것입니다. 그런 다음 작은 그래프에서 이러한 정점의 연결성을 사용하여 나머지 정점  삽입하는 속도를 높입니다. 아래에서는 작은 그래프와 큰 그래프에서 각각  및  를 사용하여 정점  이웃을 나타냅니다. 프로세스를 개략적으로 설명하면 다음과 같습니다.</p><p><code>MERGE-HNSW</code></p><p><code>Inputs </code><code> and </code></p><p><code>1</code><code>Find </code><code> to insert into </code><code> using COMPUTE-JOIN-SET</code>
<code>2</code><code>Insert each vertex </code><code> into </code>
<code>3</code><code>for </code><code> do</code>
<code>4</code>
<code>5</code>
<code>6</code><code>FAST-SEARCH-LAYER</code>
<code>7</code><code>SELECT-NEIGHBORS-HEURISTIC</code>
<code>8</code></p><p>아래에서 설명하는 절차를 사용하여 집합  계산합니다(1줄). 그런 다음 표준 HNSW 삽입 절차(2줄)를 사용하여  모든 정점을 큰 그래프에 삽입합니다. 삽입하지 않은 각 정점에 대해 삽입한 이웃 정점과 그 이웃 정점을 큰 그래프(4줄과 5줄)에서 찾습니다. 이 집합으로 시드된 <code>FAST-SEARCH-LAYER</code> 절차(6줄)를 사용하여 HNSW <a href="https://arxiv.org/pdf/1603.09320">논문</a> (7줄)에서 <code>SELECT-NEIGHBORS-HEURISTIC</code> 후보를 찾습니다. 사실상 <code>SEARCH-LAYER</code> 을 <code>INSERT</code> 방법(논문의 알고리즘 1)의 후보 집합을 찾는 것으로 대체하는 것이며, 그 외에는 변경 사항이 없습니다. 마지막으로 방금 삽입한 버텍스를  추가합니다(8행).</p><p>이 기능이 작동하려면  모든 버텍스에  이웃이 하나 이상 있어야 합니다. 실제로, 우리는  버텍스에 대해 최대 레이어 연결성인 일부   실제 HNSW 그래프에서는 정점 각도가 상당히 분산되어 있는 것을 관찰할 수 있습니다. 아래 그림은 Lucene HNSW 그래프의 최하위 레이어에 대한 일반적인 버텍스 도의 누적 밀도 함수를 보여줍니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltc815e1a9c3bb0d06/6a17e178ec0f89308a5a6564/44f001b3a1bc6627e172fed5c52a02cfdcb4cd66-1324x898.png" alt="HNSW 그래프: 버텍스 차수 분포 예시" /><p> 고정값을 사용하는 방법과 버텍스 차수의 함수로 만드는 방법을 살펴봤습니다. 두 번째 선택은 그래프 품질에 미치는 영향을 최소화하면서 속도를 크게 향상시킬 수 있으므로 다음과 같이 선택했습니다.</p><p>정의상 |는 작은 그래프에서 정점  차수와 같다는 점에 유의하세요. 하한이 2라는 것은 차수가 2보다 작은 모든 정점을 삽입한다는 의미입니다.</p><p>간단한 계산 인수를 통해  신중하게 선택하면   직접 삽입하기만 하면 된다는 것을 알 수 있습니다. 구체적으로 그래프의 끝 꼭지점 중 하나를  정확히 삽입하면 그래프의 가장자리에 색을 입힙니다.   최소  이웃을 가지려면  에지를 채색해야 한다는 것을 알 수 있습니다. 또한 다음을 기대합니다.</p><p>여기서  _U\left [N_s(U)|\right] 는 작은 그래프에서 평균 버텍스 차수입니다.  각 정점 u에 대해 최대  의 가장자리를 색칠합니다. 따라서 채색할 에지의 총 개수는 최대 _U\left [|N_s(U)|\right]입니다.  신중하게 선택하면 이 수의 가장자리에 가깝게 색을 칠할 수 있으므로 모든 정점을 포함하려면  다음을 만족해야 합니다.</p><p>이는  {1}{4}|V_s|=\frac{1}{5}|V_s|를 의미합니다.</p><p><code>SEARCH-LAYER</code> 이 런타임을 지배한다면 병합 시간을 최대  단축할 수 있음을 시사합니다. 쓰기 증폭의 대수적 증가를 감안할 때, 이는 매우 큰 인덱스의 경우에도 일반적으로 하나의 그래프를 작성할 때보다 빌드 시간이 두 배밖에 걸리지 않는다는 것을 의미합니다.</p><p>이 전략의 위험은 그래프 품질이 손상된다는 점입니다. 처음에는 노옵으로 시도했습니다 <code>FAST-SEARCH-LAYER</code>. 특히 단일 세그먼트로 병합할 때 지연 시간에 따른 리콜이 영향을 받을 정도로 그래프 품질이 저하되는 것으로 나타났습니다. 그런 다음 그래프를 제한적으로 검색하여 다양한 대안을 탐색했습니다. 결국 가장 효과적인 선택은 가장 단순한 것이었습니다. <code>SEARCH-LAYER</code> 을 사용하되 <code>ef_construction</code>. 이 매개변수화를 통해 우수한 품질의 그래프를 얻으면서도 병합 시간을 평균 30% 조금 넘게 줄일 수 있었습니다.</p><h3>조인 집합 계산</h3><p>좋은 조인 집합을 찾는 것은 HNSW 그래프 커버링 문제로 공식화할 수 있습니다. 욕심 휴리스틱은 최적의 그래프 커버를 근사화하기 위한 간단하고 효과적인 휴리스틱입니다. 우리가 취하는 접근 방식은 한 번에 하나씩 정점을 골라 이득이 감소하는 순서로  추가하는 방식입니다. 이득은 다음과 같이 정의됩니다:</p><p>여기서 는  벡터  개수를 나타내고  은 표시 함수입니다. 이득에는  추가한 버텍스 수의 변화, 즉 이 포함되는데, 이는 덜 커버되는 버텍스를 추가함으로써 목표에 더 가까워지기 때문입니다. 이득 계산은 아래 그림에서 중앙 주황색 버텍스에 대해 설명합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt7a353d56fa09af5f/6a17e17a2f4a5c33f7fa8825/fe11cd55a7d94d9e0b0f4d47ea309a99075c355d-540x474.png" alt="HNSW 그래프에서 조인 집합 J에 추가할 버텍스 게인" /><p>각 버텍스  대해 다음과 같은 상태를 유지합니다:</p><ol><li><p>오래되었는지 여부,</p></li><li><p>그 이득 ),</p></li><li><p> 인접한 정점의 개수를 로 표시합니다,</p></li><li><p>타이 브레이킹에 사용되는 [0,1] 범위의 난수입니다.</p></li></ol><p>조인 집합을 계산하는 의사 코드는 다음과 같습니다.</p><p><code>COMPUTE-JOIN-SET</code></p><p><code>Inputs </code></p><p><code>1</code>
<code>2</code>
<code>3</code><code>for </code><code> do</code>
<code>4</code>
<code>5</code>
<code>6</code>
<code>7</code><code>while </code><code> do
8</code><code> maximum gain vertex in </code>
<code>9</code><code>Remove the state for </code><code> from </code>
<code>10</code><code>if </code><code> is not stale then</code>
<code>11</code>
<code>12</code>
<code>13</code><code>for </code><code> do</code>
<code>14</code><code>mark </code><code> as stale if </code>
<code>15</code><code>mark neighbors of </code><code> stale if </code><code>
16</code><code>
17</code><code>else
18</code><code>
19</code><code>if </code><code> then
20</code><code>
21</code><code>return </code></p><p>먼저 1~5줄에서 상태를 초기화합니다.</p><p>메인 루프의 각 반복에서 처음에는 최대 이득 버텍스(8번째 줄)를 추출하여 무작위로 동점을 끊습니다. 변경하기 전에 버텍스의 게인이 오래되었는지 확인해야 합니다. 특히,  버텍스를 추가할 때마다 다른 버텍스의 이득에 영향을 미칩니다:</p><ol><li><p>모든 이웃이  추가 이웃을 가지고 있기 때문에 이득이 바뀔 수 있습니다 (14행).</p></li><li><p>이제 이웃이 완전히 커버되면 모든 이웃의 이득이 바뀔 수 있습니다(14~16줄).</p></li></ol><p>게인을 지연 방식으로 다시 계산하므로 버텍스의 게인을  삽입하려는 경우에만 다시 계산합니다(18~20행). 이득은 항상 감소하기 때문에 삽입해야 하는 버텍스를 놓칠 수 없습니다.</p><p>종료 시점을 결정하기 위해  추가한 버텍스의 총 이득을 추적하기만 하면 됩니다. 또한,  {exit}적어도 하나의 버텍스는 0이 아닌 이득을 가지므로 항상 진전이 있습니다.</p><h3>결과</h3><p>지원되는 세 가지 거리 메트릭(유클리드, 코사인, 내적 곱)을 모두 포함하는 네 가지 데이터 세트에 대해 실험을 진행했습니다:</p><ol><li><p>쿼라-E5-small: 522931개 문서, 384개 차원, 코사인 유사도를 사용합니다,</p></li><li><p>코히어-위키백과-v2: 1백만 개의 문서, 768개의 차원, 코사인 유사도를 사용합니다,</p></li><li><p>요점 1백만 문서, 960개 차원, 유클리드 거리 사용, 그리고</p></li><li><p>코히어-위키백과-v3: 1M 문서, 1024 크기, 최대 내부 제품 사용.</p></li></ol><p>각 데이터 세트에 대해 두 가지 양자화 수준을 평가합니다:</p><ol><li><p>int8 - 차원당 1바이트 정수를 사용하고</p></li><li><p>BBQ - 차원당 단일 비트를 사용합니다.</p></li></ol><p>마지막으로, 각 실험에서 두 가지 검색 깊이에서 검색 품질을 평가하고 인덱스를 구축한 후와 단일 세그먼트로 강제 병합한 후를 조사했습니다.</p><p>요약하면, 모든 경우에서 그래프 품질을 유지하면서 인덱싱 및 병합 속도를 일관되게 크게 향상시켜 검색 성능을 향상시켰습니다.</p><h4>실험 1: int8 정량화</h4><p>베이스라인에서 제안된 변경 사항인 후보까지의 평균 속도 향상은 다음과 같습니다:</p><p>색인 시간 속도 향상: <strong>1.</strong></p><p>강제 병합 속도 향상: <strong>1.</strong></p><p>이는 다음과 같은 실행 시간 분석에 해당합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8422be122ef1a9c9/6a17e17bbe6086522e00467d/5f9f6d487b4c74c4998aa47465912c1a9743e928-734x479.png" alt="기준 및 후보 병합 전략에 대한 인덱싱 및 병합 시간" /><p>정확성을 위해 정확한 시간은 다음과 같습니다.</p><p></p><p>색인</p><p></p><p>병합</p><p></p><p>데이터 세트</p><p>기준선</p><p>후보</p><p>구축</p><p>후보</p><p>쿼라-E5-small</p><p>112.41s</p><p>81.55s</p><p>113.81s</p><p>70.87s</p><p>위키-코히어-v2</p><p>158.1s</p><p>122.95s</p><p>425.20s</p><p>239.28s</p><p>요점</p><p>141.82s</p><p>119.26s</p><p>536.07s</p><p>279.05s</p><p>위키-코히어-v3</p><p>211.86s</p><p>168.22s</p><p>654.97s</p><p>414.12s</p><p>아래에는 여러 세그먼트가 있는 인덱스(모든 벡터를 인덱싱한 후 기본 병합 전략의 최종 결과)와 단일 세그먼트로 강제 병합한 후의 두 가지 검색 깊이인 recall@10과 recall@100에서 후보(점선)와 기준선을 비교한 리콜 대 지연 시간 그래프가 나와 있습니다. 곡선이 더 높고 왼쪽으로 갈수록 더 좋은데, 이는 더 낮은 지연 시간에서 더 높은 회상률을 의미합니다.</p><p>보시다시피, 여러 세그먼트 인덱스의 경우 Cohere v3 데이터 세트의 후보가 더 우수하고 다른 모든 데이터 세트의 경우 약간 떨어지지만 거의 비슷합니다. 단일 세그먼트로 병합한 후 리콜 곡선은 모든 경우에 대해 거의 동일합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf674622469bf6a90/6a17e17dfbc5f874a84919c9/9195297f19f90b172d832ba56bdada7b0d8a768b-985x392.png" alt="인덱스 구축 후 지연 시간 대비 @10 및 @100 리콜하기" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltde89f7e94ac823f4/6a17e17fe9ea876429a9c507/c866f32d579ae2b841e92ca733dd29c0e214ac6b-986x386.png" alt="단일 세그먼트로 병합 후 지연 시간 대비 @10 및 @100 리콜하기" /><h4>실험 2: BBQ 정량화</h4><p>기준선에서 후보까지의 평균 속도 향상은 다음과 같습니다:</p><p>색인 시간 속도 향상: <strong>1.</strong></p><p>강제 병합 속도 향상: <strong>1.</strong></p><p>이는 다음과 같은 실행 시간 분석에 해당합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5b0eb305222fa5c2/6a17e18125daab514f08a18c/11be0cf6b63480dc703409329357ea4847b55051-740x415.png" alt="기준 및 후보 병합 전략에 대한 인덱싱 및 병합 시간" /><p>정확성을 위해 정확한 시간은 다음과 같습니다.</p><p></p><p>색인</p><p></p><p>병합</p><p></p><p>데이터 세트</p><p>기준선</p><p>후보</p><p>구축</p><p>후보</p><p>쿼라-E5-small</p><p>70.71s</p><p>58.25s</p><p>59.38s</p><p>40.15s</p><p>위키-코히어-v2</p><p>203.08s</p><p>142.27s</p><p>107.27s</p><p>85.68s</p><p>요점</p><p>110.35s</p><p>105.52s</p><p>323.66s</p><p>202.2s</p><p>위키-코히어-v3</p><p>313.43s</p><p>190.63s</p><p>165.98s</p><p>159.95s</p><p>여러 세그먼트 인덱스의 경우, 기준선이 약간 더 나은 코히어 v2를 제외한 거의 모든 데이터 세트에서 후보가 더 우수합니다. 단일 세그먼트 인덱스의 경우 리콜 곡선은 모든 경우에 대해 거의 동일합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb12174abeadbcdd1/6a17e1822f4a5c4cc2fa8829/7da1be27bab5a7f5f9c9a1882ea35761a4a0ba5c-973x383.png" alt="인덱스 구축 후 지연 시간 대비 @10 및 @100 리콜하기" /><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf1b88d4f7fec9905/6a17e184414c64a8de9450b4/a26af30a56c55a12addee7e4fb7e8f606f366668-979x386.png" alt="단일 세그먼트로 병합된 지연 시간 대비 @10 및 @100을 기억해 보세요." /><h3>결론</h3><p>이 블로그에서 설명한 알고리즘은 곧 출시될 Lucene 10.2와 이를 기반으로 하는 Elasticsearch 릴리즈에서 사용할 수 있습니다. 사용자는 이 새 버전에서 병합 성능이 향상되고 인덱스 빌드 시간이 단축되는 이점을 누릴 수 있습니다. 이 변경은 벡터 및 하이브리드 검색을 위한 빠르고 효율적인 Lucene과 Elasticsearch를 만들기 위한 지속적인 노력의 일환입니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/hnsw-graphs-speed-up-merging</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/hnsw-graphs-speed-up-merging</guid>
    <category><![CDATA[벡터 데이터베이스]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Thomas Veasey,Mayya Sharipova]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt9fca6a01d0541cfb/6a17e187033c8d46486bb0e1/49a6c880f5dedd0fa502ece5be124824ee218cc0-1792x1024.png" length="0" type="image/png"/>
    <pubDate>Mon, 07 Apr 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Lucene의 동시성 버그: 낙관적인 동시성 실패를 수정하는 방법]]></title>
    <description><![CDATA[CMU 파스타 연구소의 결정론적 동시성 테스트 프레임워크인 Fray 덕분에 까다로운 루씬 버그를 추적하여 해결했습니다.]]></description>
    <content:encoded><![CDATA[<p>네, 또 다른 버그 수정 블로그입니다. 하지만 이번 이야기는 반전이 있습니다. 오픈소스 영웅이 나타나서 하루를 구해줍니다. </p><p>동시성 버그를 디버깅하는 것은 쉬운 일이 아니지만, 이제부터 시작하겠습니다. 불안정한 장애를 안정적으로 재현 가능한 장애로 전환하는 CMU의 PASTA Lab의 결정론적 동시성 테스트 프레임워크인 Fray를 사용해 보세요. 프레이의 영리한 섀도 잠금 설계와 정밀한 스레드 제어 덕분에 저희는 까다로운 루씬 버그를 추적하여 마침내 문제를 해결했습니다. 이 게시물에서는 오픈 소스 영웅과 도구가 어떻게 동시성 디버깅의 고통을 덜어주고 소프트웨어 세계를 훨씬 더 나은 곳으로 만드는지 살펴봅니다.</p><h2>동시성 버그: 소프트웨어 엔지니어의 골칫거리</h2><p>동시성 버그는 최악입니다. 고치기 어려울 뿐만 아니라 안정적으로 실패하게 만드는 것이 가장 어려운 부분입니다. 이 테스트 실패( <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat#testGlobalVersions</code></a>)를 예로 들어 보겠습니다. 여러 문서 작성 및 업데이트 스레드를 생성하여 루씬의 낙관적인 동시성 모델에 도전합니다. 이 테스트는 낙관적인 동시성 제어에서 경쟁 조건을 노출했습니다. 즉, 문서 작업이 일련의 작업 중 최신 작업이라고 거짓으로 주장할 수 있습니다 😱. 즉, 특정 조건에서는 낙관적인 동시성 제약 조건에 따라 실패해야 할 업데이트 또는 삭제 작업이 실제로 성공할 수도 있습니다.</p><p></p>org.apache.lucene.sandbox.codecs.idversion.TestIDVersionPostingsFormat &gt; testGlobalVersions FAILED
    java.lang.AssertionError: maxSeqNo must be greater or equal to 7442 but was 7441
        at __randomizedtesting.SeedInfo.seed([B97A2BDBC7E40BF6:B4D76006D5101E6]:0)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DocumentsWriterDeleteQueue.close(DocumentsWriterDeleteQueue.java:325)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DocumentsWriter.flushAllThreads(DocumentsWriter.java:659)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.IndexWriter.getReader(IndexWriter.java:576)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenFromWriter(StandardDirectoryReader.java:381)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenIfChanged(StandardDirectoryReader.java:355)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.StandardDirectoryReader.doOpenIfChanged(StandardDirectoryReader.java:345)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.index.DirectoryReader.openIfChanged(DirectoryReader.java:170)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.SearcherManager.refreshIfNeeded(SearcherManager.java:144)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.SearcherManager.refreshIfNeeded(SearcherManager.java:52)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.ReferenceManager.doMaybeRefresh(ReferenceManager.java:167)
        at org.apache.lucene.core@10.0.0-SNAPSHOT/org.apache.lucene.search.ReferenceManager.maybeRefresh(ReferenceManager.java:213)<p>Java 스택 추적을 싫어하는 분들께 사과드립니다. 삭제가 반드시 '삭제'를 의미하는 것은 아닙니다. Lucene의 세그먼트는 읽기 전용이므로 문서 "업데이트"를 나타낼 수도 있습니다.
</p><p>Apache Lucene은 <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> 클래스를 통해 문서를 작성하는 각 스레드를 관리합니다. 이 클래스는 문서 작성을 위한 스레드를 생성하거나 재사용하며 각 쓰기 작업은 <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThread.java"><code>DocumentsWriterPerThread</code></a> (DWPT) 클래스 내에서 해당 정보를 제어합니다. 또한 작성자는 <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> (DWDQ)에서 어떤 문서가 삭제되었는지 추적합니다. 이러한 구조는 모든 문서 변경 작업을 메모리에 보관하고 주기적으로 플러시하여 인메모리 리소스를 확보하고 구조를 디스크에 지속합니다.</p><p></p><p><a href="https://en.wikipedia.org/wiki/Blocking_(computing)">스레드 차단을</a> 방지하고 동시 시스템에서 높은 처리량을 보장하기 위해 Apache Lucene은 매우 중요한 섹션에서만 <a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html">동기화를</a> 시도합니다. 이는 실제로는 좋을 수 있지만 다른 동시 시스템과 마찬가지로 용두사미가 될 수 있습니다.</p><h2>
거짓 희망</h2><p>초기 조사를 통해 적절하게 동기화되지 않은 몇 가지 중요한 섹션을 발견했습니다. 주어진 <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> 에 대한 모든 상호 작용은 둘러싸는 <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriter.java"><code>DocumentsWriter</code></a> 에 의해 제어됩니다. 따라서 개별 메소드가 <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterDeleteQueue.java"><code>DocumentsWriterDeleteQueue</code></a> 에서 적절하게 동기화되지 않을 수 있지만 월드에 대한 액세스는 동기화되어 있습니다(또는 동기화되어야 합니다). (소유권과 액세스 권한이 어떻게 혼동되는지 자세히 설명하지 않겠습니다. 많은 기여자가 작성한 오랜 프로젝트이므로 여기서는 다루지 않겠습니다. 조금만 여유를 가지세요.)</p><p></p><p>하지만 <a href="https://github.com/apache/lucene/blob/40060f8b7080d06a218518445a0a1dfc520c812a/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterFlushControl.java#L572-L576">플러시 중</a> 동기화되지 않은 한 곳을 발견했습니다.</p>// Advance the queue, meaning create a new one to keep track of deletes
// Since we have been flushed, let's starting tracking again
DocumentsWriterDeleteQueue newQueue = documentsWriter.deleteQueue.advanceQueue(perThreadPool.size());
// OK, get the current new maximum sequence number for optimistic concurrency
seqNo = documentsWriter.deleteQueue.getMaxSeqNo();
// Reset to the new queue
documentsWriter.resetDeleteQueue(newQueue);<p>이러한 작업은 단일 원자 작업으로 동기화되지 않습니다. 즉, <code>newQueue</code> 이 생성되고 <code>getMaxSeqNo</code> 을 호출하는 사이에 다른 코드가 <code>documentsWriter</code> 클래스에서 시퀀스 번호를 증가시키면서 실행되었을 수 있습니다. 버그를 찾았습니다!</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc55033280b45913/6a1706b4dc55dec27fe00d30/3f2617a102d28735e755a7539b047c28beda41be-1600x453.jpg" alt="" /><p>
하지만 대부분의 복잡한 버그가 그렇듯 근본 원인을 찾는 것도 쉽지 않았습니다. 바로 그때 영웅이 등장했습니다.</p><h2>전투의 영웅</h2><p>우리의 영웅을 소개합니다: 파스타 연구소의 <a href="https://aoli.al/">아오 리와</a> 그의 동료들. 프레이가 어떻게 하루를 구했는지 설명해 드리겠습니다.</p><p><a href="https://github.com/cmu-pasta/fray">Fray는</a> 카네기멜론 대학교의 <a href="https://pastalab.org/">PASTA</a> 연구소의 연구진이 개발한 결정론적 동시성 테스트 프레임워크입니다. 결정론적 동시성 테스트는 20년 이상 학계에서 광범위하게 연구되어 왔지만, 실무자들은 여전히 신뢰할 수 없고 불안정한 것으로 널리 알려진 스트레스 테스트에 의존하여 동시성 프로그램을 테스트하고 있습니다. 따라서 저희는 일반성과 실제 적용 가능성을 주요 목표로 삼아 결정론적 동시성 테스트 프레임워크를 설계하고 구현하고자 했습니다.</p><p></p><h2>핵심 아이디어</h2><p>프레이의 핵심은 순차적 실행이라는 간단하지만 강력한 원칙을 활용합니다. Java의 동시성 모델은 프로그램에 데이터 경합이 없는 경우 모든 실행이 순차적으로 일관되게 표시된다는 핵심 속성을 <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.3">제공합니다.</a> 즉, 프로그램의 동작은 일련의 프로그램 문으로 표현할 수 있습니다.</p><p>Fray는 대상 프로그램을 순차적으로 실행하는 방식으로 작동하며, 각 단계에서 하나의 스레드를 제외한 모든 스레드를 일시 중지하여 Fray가 스레드 스케줄링을 정밀하게 제어할 수 있도록 합니다. 스레드는 동시성을 시뮬레이션하기 위해 무작위로 선택되지만, 이후 결정론적 리플레이를 위해 선택 사항이 기록됩니다. 실행을 최적화하기 위해 Fray는 스레드가 잠금 또는 원자/휘발성 액세스와 같은 동기화 명령을 실행하려고 할 때만 컨텍스트 전환을 수행합니다. 데이터 레이스 자유의 좋은 특성은 이러한 제한된 컨텍스트 전환만으로도 스레드 인터리빙으로 인해 관찰 가능한 모든 동작을 탐색하기에 충분하다는 것입니다<a href="https://arxiv.org/abs/2501.12618">(저희 논문에는</a> 증명 스케치가 있습니다).</p><p></p><h2>과제: 스레드 스케줄링 제어</h2><p>핵심 아이디어는 간단해 보이지만, 프레이를 구현하는 데는 상당한 어려움이 있었습니다. 스레드 스케줄링을 제어하려면 Fray가 각 애플리케이션 스레드의 실행을 관리해야 합니다. 언뜻 보기에는 동시성 프리미티브를 사용자 정의 구현으로 대체하는 것이 간단해 보일 수 있습니다. 하지만 <a href="https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-6.html">바이트코드 명령어</a>, <a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/locks/ReentrantLock.html">하이레벨 라이브러리</a>, <a href="https://github.com/openjdk/jdk/blob/b720517cb33c2119ec6ed85504bce321de748228/src/java.base/share/classes/java/lang/Object.java#L394">네이티브 메서드가</a> 혼합되어 있는 JVM의 동시성 제어는 복잡합니다.</p><p></p><p>이것은 토끼굴로 밝혀졌습니다:</p><p></p><ul><li><p>예를 들어, 모든 <code>MONITORENTER</code> 인스트럭션에는 동일한 방법의 <code>MONITOREXIT</code> 인스트럭션이 있어야 합니다. Fray가 <code>MONITORENTER</code> 를 스텁/모의 메서드 호출로 대체하는 경우, <code>MONITOREXIT</code> 도 대체해야 합니다.</p></li><li><p><code>object.wait/notify</code> 을 사용하는 코드에서 <code>MONITORENTER</code> 을 대체하는 경우 해당 <code>object.wait</code> 도 대체해야 합니다. 이 교체 체인은 <code>object.notify</code> 이상으로 확장됩니다.</p></li><li><p>JVM은 네이티브 코드 내에서 특정 동시성 관련 메서드(예: 스레드가 종료될 때 <code>object.notify</code> )를 호출합니다. 이러한 작업을 대체하려면 JVM 자체를 수정해야 합니다.</p></li><li><p>클래스 로더 및 가비지 컬렉션(GC) 스레드와 같은 JVM 함수도 동시성 프리미티브를 사용합니다. 이러한 프리미티브를 수정하면 해당 JVM 함수와 불일치가 발생할 수 있습니다.</p></li><li><p>JDK에서 동시성 프리미티브를 교체하면 초기화 단계에서 JVM 충돌이 발생하는 경우가 많습니다.</p></li></ul><p></p><p>이러한 문제들로 인해 동시성 프리미티브의 포괄적인 대체가 불가능하다는 것이 분명해졌습니다.</p><h2>
솔루션: 섀도락 디자인</h2><p>이러한 문제를 해결하기 위해 Fray는 새로운 섀도 잠금 메커니즘을 사용해 동시성 프리미티브를 대체하지 않고 스레드 실행을 오케스트레이션합니다. 섀도 락은 스레드 실행을 안내하는 중개자 역할을 합니다. 예를 들어 잠금을 획득하기 전에 애플리케이션 스레드는 해당 섀도 잠금과 상호 작용해야 합니다. 섀도 잠금은 스레드가 잠금을 획득할 수 있는지 여부를 결정합니다. 스레드를 진행할 수 없는 경우 섀도 락이 스레드를 차단하고 다른 스레드가 실행되도록 허용하여 교착 상태를 방지하고 동시성을 제어할 수 있습니다. 이러한 설계를 통해 Fray는 동시성 시맨틱의 정확성을 유지하면서 스레드 인터리빙을 투명하게 제어할 수 있습니다. 각 동시성 프리미티브는 섀도 잠금 프레임워크 내에서 신중하게 모델링되어 건전성과 완성도를 보장합니다. 자세한 기술적인 내용은 백서에서 확인할 수 있습니다.</p><p></p><p>또한 이 디자인은 미래에도 사용할 수 있도록 설계되었습니다. 동시성 프리미티브에 대한 섀도 잠금의 계측만 요구함으로써 최신 버전의 JVM과의 호환성을 보장합니다. 이는 JVM의 동시성 프리미티브 인터페이스가 비교적 안정적이며 수년 동안 변경되지 않았기 때문에 가능합니다.</p><h2>
테스트 프레이</h2><p>Fray를 구축한 후 다음 단계는 평가였습니다. 다행히도 Apache Lucene과 같은 많은 애플리케이션에는 이미 동시성 테스트가 포함되어 있습니다. 이러한 동시성 테스트는 여러 스레드를 생성하고 일부 작업을 수행한 다음 (일반적으로) 해당 스레드가 완료될 때까지 기다린 다음 일부 속성을 어설트하는 일반 JUnit 테스트입니다. 대부분의 경우 이러한 테스트는 인터리빙을 한 번만 실행하기 때문에 통과합니다. 더 큰 문제는 앞서 설명한 대로 일부 테스트는 CI/CD 환경에서 가끔씩만 실패하기 때문에 이러한 실패를 디버깅하기가 매우 어렵다는 것입니다. Fray로 동일한 테스트를 실행했을 때 수많은 버그를 발견했습니다. 특히, 프레이는 이 블로그의 초점인 <a href="https://github.com/apache/lucene/issues/13127"><code>TestIDVersionPostingsFormat.testGlobalVersions</code></a> 을 포함하여 이전에 보고된 버그 중 신뢰할 수 있는 재현이 없어 수정되지 않은 채로 남아 있던 버그를 재발견했습니다. 다행히도 프레이를 사용하면 문제를 결정적으로 재생하고 개발자에게 자세한 정보를 제공하여 문제를 안정적으로 재현하고 수정할 수 있습니다.</p><p></p><h2>프레이의 다음 단계</h2><p>Elastic의 개발자들로부터 Fray가 동시성 버그 디버깅에 도움이 되었다는 이야기를 듣게 되어 매우 기쁩니다. 앞으로도 더 많은 개발자가 프레이를 사용할 수 있도록 지속적으로 노력할 것입니다.</p><p>우리의 단기 목표는 무작위 값 생성기나 <code>object.hashcode</code> 사용과 같은 다른 비결정적 연산이 있는 경우에도 일정을 결정론적으로 재생하는 Fray의 기능을 강화하는 것입니다. 또한 개발자가 수동 개입 없이 기존 동시성 테스트를 분석하고 디버깅할 수 있도록 Fray의 사용성을 개선하는 것을 목표로 하고 있습니다. 무엇보다도 프로그램에서 동시성 문제를 디버깅하거나 테스트하는 데 어려움을 겪고 계신다면 여러분의 의견을 듣고 싶습니다. 주저하지 마시고 <a href="https://github.com/cmu-pasta/fray">프레이 깃허브 리포지토리에서</a> 이슈를 생성해 주세요.</p><p></p><h2>동시성 버그 수정 시간</h2><p>Ao Li와 PASTA 연구소 덕분에 이제 이 테스트가 안정적으로 실패한 사례가 생겼습니다! 드디어 이 문제를 해결할 수 있게 되었습니다. 핵심 문제는 <a href="https://github.com/apache/lucene/blob/f2e7ae40af0b28b1d5f2edc31f8858229a8523f4/lucene/core/src/java/org/apache/lucene/index/DocumentsWriterPerThreadPool.java"><code>DocumentsWriterPerThreadPool</code></a> 에서 스레드 및 리소스 재사용을 허용하는 방식에 있었습니다.</p>1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t0 20 getNextSequenceNumber 1 called from stack:
&lt;snip&gt;...&lt;/snip&gt;
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_1, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_5, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_2, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_6, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; new Writer: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_4, aborted=false, numDocsInRAM=0, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]<p>여기에서 각 스레드가 생성되는 것을 볼 수 있으며, 0 생성 시 초기 삭제 대기열을 참조합니다.</p><p>그러면 대기열에서 이전 7개의 작업이 올바르게 표시되는 플러시에서 대기열 진행이 이루어집니다.</p>1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t020 advanceQueue called from stack with maxSeq 9 lastSeqNo: 1 maxNumPendingOps: 7:
&lt;snip&gt;...&lt;/snip&gt;
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t525 getNextSequenceNumber 2 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t828 getNextSequenceNumber 3 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t727 getNextSequenceNumber 4 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t626 getNextSequenceNumber 5 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t424 getNextSequenceNumber 6 called from stack:
1&gt; DWDQ: [ generation: 0 ] id: 245403753 tid: t323 getNextSequenceNumber 7 called from stack:<p>그러나 모든 스레드가 플러싱을 완료하기 전에 두 개의 스레드가 추가 문서에 재사용됩니다:</p>1&gt; getAndLock: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]
1&gt; getAndLock: DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds]<p><code>numDocsInRAM</code> 그러면 플러시 중에 7로 계산된 가정된 최대값보다 <code>seqNo</code> 이 증가합니다. 세그먼트 <code>_3</code> 및 <code>_0</code></p>1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_2, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_3, aborted=false, numDocsInRAM=2, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_6, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_4, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_5, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_1, aborted=false, numDocsInRAM=1, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove
1&gt; DocumentsWriterPerThread [pendingDeletes=gen=0, segment=_0, aborted=false, numDocsInRAM=2, deleteQueue=DWDQ: [ generation: 0 ], 0 deleted docIds] checkout to remove<p>따라서 Lucene이 플러시 중 문서 작업의 순서를 잘못 설명하여 이 테스트가 실패하게 됩니다.</p><p>모든 좋은 버그 수정이 그렇듯, 실제 수정 사항은 약 <a href="https://github.com/apache/lucene/pull/13627/files">10줄의 코드입니다</a>. 하지만 두 명의 엔지니어가 실제로 알아내는 데 며칠이 걸렸습니다:</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt71d78f22ce6ec215/6a1706b61949f7bcd0e7a94a/7915e5ba2feba0fb8e846da0af11bfdca470c467-1600x622.jpg" alt="" /><h2>모든 영웅이 망토를 입는 것은 아닙니다</h2><p>네, 진부한 표현이지만 사실입니다.</p><p></p><p>동시 프로그램 디버깅은 매우 중요합니다. 이러한 까다로운 동시성 버그는 디버깅하고 해결하는 데 엄청난 시간이 걸립니다. Rust와 같은 새로운 언어에는 이와 같은 경쟁 조건을 방지하는 메커니즘이 내장되어 있지만, 전 세계 대부분의 소프트웨어는 이미 <a href="https://www.rust-lang.org/">Rust가</a> 아닌 다른 언어로 작성되어 있습니다. 자바는 오랜 시간이 지난 지금도 여전히 가장 많이 사용되는 언어 중 하나입니다. JVM 기반 언어에서 디버깅을 개선하면 소프트웨어 엔지니어링 세계가 더 좋아집니다. 그리고 일부 사람들은 코드가 대규모 언어 모델에 의해 작성될 것이라고 생각하기 때문에, 엔지니어로서 우리가 하는 일은 결국 나쁜 코드가 아니라 나쁜 LLM 코드를 디버깅하는 것일지도 모릅니다. 그러나 소프트웨어 엔지니어링의 미래와 상관없이 동시 프로그램 디버깅은 소프트웨어를 유지 관리하고 구축하는 데 여전히 중요할 것입니다.</p><p></p><p>이보다 훨씬 더 나은 서비스를 만들어준 PASTA Lab의 Ao Li와 동료들에게 감사드립니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/concurrency-bugs-lucene-debugging</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/concurrency-bugs-lucene-debugging</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Benjamin Trent,Ao Li]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf9fcbf843affc566/6a1706b8964ceaea3208bab9/7884e35f40c15fe349379ef7ae4648b21708962f-1070x712.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 07 Feb 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[루씬 래핑 2024]]></title>
    <description><![CDATA[2024년은 아파치 루씬에게 또 다른 중요한 해입니다. 이 블로그에서는 주요 내용을 살펴봅니다.]]></description>
    <content:encoded><![CDATA[<p>2024년에는 3년 만의 대규모 업데이트를 비롯해 흥미로운 개선 사항과 새로운 기능으로 가득 찬 수많은 릴리스가 출시되는 등 Apache Lucene이 상당한 활동을 보였습니다. 주요 특징 몇 가지를 살펴보겠습니다.</p><h2>Lucene &amp; 커뮤니티</h2><p>프로젝트는 그것을 지원하는 커뮤니티만큼만 강력합니다. 20년이 넘는 개발 기간에도 불구하고 Lucene 프로젝트는 열정적이고 적극적인 기여자 덕분에 여전히 활기차고 번창하고 있습니다.</p><p>2024년에 Lucene 프로젝트는 98명의 고유 기여자로부터 2,000개 이상의 커밋과 800개에 가까운 풀 리퀘스트를 받았습니다. 새로운 커미터와 PMC 멤버가 프로젝트에 합류하여 성공을 이끄는 등 기여자의 수가 계속 증가하고 있습니다.</p><h2>루씬 10</h2><p>2024년에는 거의 3년 만에 처음으로 185명의 고유 기여자로부터 2,000개 이상의 커밋을 받은 Lucene 10이 출시되었습니다. 루씬이 따르는 개발 모델에서는 마이너 릴리스에서 많은 개선 사항과 기능을 제공할 수 있지만, 메이저 릴리스에서는 더 큰 기능과 현대화를 제공할 수 있는 기회를 제공합니다. 예를 들어, Lucene 10에는 최소 Java 21이 필요합니다. 최소 Java 버전을 상향 조정하면 Lucene이 최신 Java가 제공하는 개선 사항을 계속 활용할 수 있습니다.</p><p>Lucene 10의 주요 초점은 실행되는 하드웨어를 더 잘 활용하는 것입니다. 주요 특징 몇 가지를 간단히 살펴보겠습니다:</p><ul><li><p><strong>검색 병렬성 향상</strong> - 검색 실행은 이미 세그먼트 전체에서 병렬화되어 있지만, 이제 더 나아가 세그먼트 내에서 병렬화합니다. 이렇게 하면 온디스크 표현과 실행 성능이 분리되어 단일 세그먼트도 최신 시스템에서 코어 수의 이점을 누릴 수 있습니다.</p></li><li><p><strong>향상된 I/O 병렬</strong> 처리 - Lucene이 사용하는 간단한 동기식 I/O 모델이 프리페치 단계로 개선되었습니다. 이렇게 하면 호출 스레드를 차단하지 않으면서도 가까운 시일 내에 인덱스 파일의 영역이 필요하다는 것을 OS에 알립니다.</p></li><li><p><strong>희소 인덱싱으로 CPU 및 스토리지 효율성 향상</strong> - Lucene 10은 다른 데이터 저장소에서 기본 키 인덱싱 또는 영역 인덱싱이라고도 하는 희소 인덱싱을 지원합니다.</p></li></ul><p>루씬 10에 대한 자세한 내용은 루씬 10에 대한 전용 <a href="https://www.elastic.co/search-labs/blog/apache-lucene-10-release-highlights">문서를</a> 참조하세요.</p><h2>루씬 연구 및 혁신</h2><p>2024년에 루씬은 특히 머신 러닝 통합, 벡터 검색, 대규모 데이터 세트 최적화 분야에서 연구와 혁신이 급증했으며, 10개의 개별 <a href="https://scholar.google.com/scholar?as_ylo=2024&amp;q=lucene&amp;hl=en&amp;as_sdt=0,5">연구 논문과 출판물을</a> 참조할 수 있습니다. 주요 연구 분야 및 개발 사항에는 다음이 포함됩니다:</p><ul><li><p><strong>벡터 검색 및 임베딩 지원</strong> - Lucene은 벡터 기반 검색을 위한 강력하고 확장 가능한 솔루션을 제공하여 대규모의 의미론적 검색을 가능하게 합니다. 사용자는 Lucene의 강력한 색인 및 검색 인프라를 활용하여 기존 텍스트 검색의 장점과 최신 벡터 검색의 고급 기능을 결합함으로써 광범위한 검색 및 정보 검색 작업을 위한 포괄적인 솔루션으로 활용할 수 있습니다.</p></li><li><p><strong>하이브리드 검색 모델</strong> - 루씬은 기존의 키워드 기반 검색과 최신 벡터 기반 검색을 결합하는 하이브리드 검색 기법도 연구했습니다. 용어 기반 인덱스와 고밀도 벡터 표현을 병합함으로써 Lucene은 보다 정확하고 맥락에 맞는 검색 결과를 제공하여 기존 검색 엔진의 정확성과 시맨틱 검색의 유연성 사이의 간극을 메울 수 있습니다.</p></li></ul><p>2024년에 진행 중인 연구 노력은 특히 AI, 시맨틱 검색 및 빅 데이터 애플리케이션의 맥락에서 최신 검색 기술의 진화하는 요구 사항에 대한 Lucene의 적응력을 입증합니다. 이 프로젝트는 기존 검색 사용 사례와 최첨단 검색 사용 사례 모두를 위한 강력하고 유연하며 효율적인 플랫폼으로 계속 성장하고 있습니다.</p><h2>2024년 Lucene 릴리즈</h2><p>정확한 반영은 아니지만, 엄청난 양의 릴리스가 커뮤니티의 지속적인 헌신과 에너지를 강조합니다. 이번 업데이트에는 벡터 검색 성능 및 효율성의 대폭적인 개선, 매드바이즈 지원, 포스팅 목록 디코딩 최적화, SIMD를 통한 속도 향상 등이 포함됩니다.</p><p>전체 릴리스 목록은 다음과 같습니다:</p><ul><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-1010-available">10.1.0</a> (2024-12-20)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9121-available">9.12.1</a> (2024-12-13)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-1000-available">10.0.0</a> (2024-10-14)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9120-available">9.12.0</a> (2024-09-28)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-8114-available">8.11.4</a> (2024-09-24)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9111-available">9.11.1</a> (2024-06-27)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9110-available">9.11.0</a> (2024-06-06)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-9100-available">9.10.0</a> (2024-02-20)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-8113-available">8.11.3</a> (2024-02-08)</p></li><li><p><a href="https://lucene.apache.org/core/corenews.html#apache-lucenetm-992-available">9.9.2</a> (2024-01-29)</p></li></ul><p>자세한 정보 및 릴리스 노트는 <a href="https://projects.apache.org/project.html?lucene-core">Lucene Core</a> 페이지에서 확인할 수 있습니다. 또한 이에 상응하는 <a href="https://projects.apache.org/project.html?lucene-pylucene">PyLucene</a> 릴리스도 있습니다.</p><h2>마무리</h2><p>Lucene이 성숙해짐에 따라 헌신적이고 활기찬 커뮤니티 덕분에 계속 발전하고 있습니다. 지금까지 살펴본 바와 같이 2024년은 놀랍도록 생산적인 한 해였으며, 이제 2025년에 가져올 흥미로운 발전을 기대해봅니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/apache-lucene-wrapped-2024</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/apache-lucene-wrapped-2024</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Chris Hegarty]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt901211870015335c/6a17ddf6a292998b08d02b93/29a02c89b3c5adb37a5f900de634ff09cb63fdd9-1792x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[루씬 버그 모험: 손상된 인덱스 예외 수정]]></title>
    <description><![CDATA[때로는 한 줄의 코드를 작성하는 데 며칠이 걸리기도 합니다. 여기에서는 잠재적인 Apache Lucene 인덱스 손상을 해결하기 위해 며칠에 걸쳐 디버깅을 진행했던 엔지니어의 고군분투를 엿볼 수 있습니다.]]></description>
    <content:encoded><![CDATA[<h2>준비하세요: </h2><p>이 특별한 블로그는 평소와 다릅니다. 새로운 기능에 대한 설명이나 튜토리얼이 아닙니다. 이것은 작성하는 데 3일이 걸린 한 줄의 코드에 관한 것입니다. 잠재적인 Apache Lucene 인덱스 손상을 수정할 예정입니다. 몇 가지 팁을 알려드리고자 합니다:</p><ul><li><p>충분한 시간과 올바른 도구만 있다면 모든 결함 테스트는 반복할 수 있습니다.</p></li><li><p>견고한 시스템을 위해서는 여러 단계의 테스트가 중요합니다. 그러나 테스트 수준이 높아질수록 디버깅과 재현이 점점 더 어려워집니다.</p></li><li><p>수면은 훌륭한 디버거입니다</p></li></ul><h2>Elasticsearch 테스트 방법</h2><p>Elastic에서는 Elasticsearch 코드베이스에 대해 실행되는 수많은 테스트가 있습니다. 일부는 단순하고 집중적인 기능 테스트이고, 다른 일부는 단일 노드 "해피 경로" 통합 테스트이며, 또 다른 일부는 장애 시나리오에서 모든 것이 올바르게 작동하는지 확인하기 위해 클러스터를 중단하려고 시도합니다. 테스트가 계속 실패하면 엔지니어 또는 도구 자동화가 깃허브 이슈를 생성하고 특정 팀에서 조사할 수 있도록 플래그를 지정합니다. 이 <a href="https://github.com/elastic/elasticsearch/issues/105122">특정 버그는</a> 지난 테스트에서 발견되었습니다. 이러한 테스트는 까다로워서 여러 번 실행해야만 반복할 수 있는 경우도 있습니다.</p><h2>이 테스트는 실제로 무엇을 테스트하나요?</h2><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt58aab468587a7d35/6a17dd8e3e03d74b8d4f2b5f/63268f3b5714ebf9df8070ec2e3c4f0486ed822e-1600x1088.jpg" alt="깃허브 이슈: https://github.com/elastic/elasticsearch/issues/105122" /><p>이 특별한 테스트는 흥미로운 테스트입니다. 특정 매핑을 생성하여 기본 샤드에 적용합니다. 그런 다음 복제본을 만들려고 할 때. 주요 차이점은 복제본이 문서 구문 분석을 시도할 때 테스트가 예외를 삽입하여 예상치 못한(그러나 예상했던) 방식으로 복구가 실패한다는 것입니다.</p><p></p><p>하지만 모든 것이 예상대로 작동했지만 한 가지 중요한 문제가 있었습니다. 테스트 정리 과정에서 일관성을 검증하는 과정에서 이 테스트에 문제가 발생했습니다.</p><p>
이 테스트는 예상대로 실패했습니다. 일관성 검사 중에 모든 복제된 파일과 기본 Lucene 세그먼트 파일이 일관성이 있는지 확인합니다. 즉, 손상되지 않고 완전히 복제된다는 의미입니다. 부분적인 데이터나 손상된 데이터는 완전히 실패하는 것보다 훨씬 더 나쁩니다. 다음은 실패에 대한 무섭고 축약된 스택 추적입니다.</p><p></p>Caused by: org.apache.lucene.index.CorruptIndexException: Problem reading index from store(ByteSizeCachingDirectory(ElasticsearchMockDirectoryWrapper(HybridDirectory@/opt/buildkite-agent/builds/bk-agent-prod-gcp-1707109485745743789/elastic/elasticsearch-periodic/server/build/testrun/internalClusterTest/temp/org.elasticsearch.indices.recovery.IndexRecoveryIT_40853F21F419B395-001/tempDir-005/node_t0/indices/ZNwxG7VvShuwYV78RTjknA/0/index lockFactory=org.apache.lucene.store.NativeFSLockFactory@2c169f59))) (resource=store(ByteSizeCachingDirectory(ElasticsearchMockDirectoryWrapper(HybridDirectory@/opt/buildkite-agent/builds/bk-agent-prod-gcp-1707109485745743789/elastic/elasticsearch-periodic/server/build/testrun/internalClusterTest/temp/org.elasticsearch.indices.recovery.IndexRecoveryIT_40853F21F419B395-001/tempDir-005/node_t0/indices/ZNwxG7VvShuwYV78RTjknA/0/index lockFactory=org.apache.lucene.store.NativeFSLockFactory@2c169f59))))

    at org.apache.lucene.index.SegmentCoreReaders.&lt;init&gt;(SegmentCoreReaders.java:165)
    at org.apache.lucene.index.SegmentReader.&lt;init&gt;(SegmentReader.java:96)
    at org.apache.lucene.index.ReadersAndUpdates.getReader(ReadersAndUpdates.java:178)
    at org.apache.lucene.index.ReadersAndUpdates.getLatestReader(ReadersAndUpdates.java:243)
    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)
    at org.apache.lucene.index.FilterMergePolicy.keepFullyDeletedSegment(FilterMergePolicy.java:118)
    at org.apache.lucene.index.FilterMergePolicy.keepFullyDeletedSegment(FilterMergePolicy.java:118)
    at org.apache.lucene.index.ReadersAndUpdates.keepFullyDeletedSegment(ReadersAndUpdates.java:822)
    at org.apache.lucene.index.IndexWriter.isFullyDeleted(IndexWriter.java:6078)
    &lt;snip&gt;

    Caused by: java.io.FileNotFoundException: No sub-file with id .kdi found in compound file "_0.cfs" (fileName=_0.kdi files: [_0.pos, .nvm, .fnm, _0.tip, _Lucene90_0.dvd, _0.doc, _0.tim, _Lucene90_0.dvm, _ES87BloomFilter_0.bfm, .fdm, .nvd, _ES87BloomFilter_0.bfi, _0.tmd, .fdx, .fdt])

      at org.apache.lucene.codecs.lucene90.Lucene90CompoundReader.openInput(Lucene90CompoundReader.java:170)
      at org.apache.lucene.codecs.lucene90.Lucene90PointsReader.&lt;init&gt;(Lucene90PointsReader.java:63)
      at org.apache.lucene.codecs.lucene90.Lucene90PointsFormat.fieldsReader(Lucene90PointsFormat.java:74)
      at org.apache.lucene.index.SegmentCoreReaders.&lt;init&gt;(SegmentCoreReaders.java:152)
      &lt;snip&gt;
<p>강제 복제 실패로 인해 복제된 샤드가 결국 손상되었습니다! 오류의 핵심 부분을 쉬운 말로 설명해 드리겠습니다.</p><p></p><p>Lucene은 세그먼트 기반 아키텍처로, 각 세그먼트가 자체 읽기 전용 파일을 알고 관리합니다. 이 특정 <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/index/SegmentCoreReaders.java">세그먼트는 세그먼트코어리더를</a> 통해 모든 것이 정상적으로 작동하는지 검증하고 있었습니다. 각 핵심 리더에는 특정 세그먼트에 대해 어떤 필드 유형과 파일이 존재하는지 나타내는 메타데이터가 저장되어 있습니다. 그러나 <a href="https://github.com/apache/lucene/blob/add9c09c84ee66d4522c566c9f679035a0dfec13/lucene/core/src/java/org/apache/lucene/codecs/lucene90/Lucene90PointsFormat.java">Lucene90PointsFormat의</a> 유효성을 검사할 때 예상되는 특정 파일이 누락되었습니다. 세그먼트 <code>_0.cfs</code> 파일을 사용하면 <code>kdi</code> 이라는 포인트 형식 파일이 예상됩니다. <code>cfs</code> 는 "복합 파일 시스템" 의 약자로, Lucene은 때때로 모든 필드 유형과 모든 작은 파일을 하나의 큰 파일로 결합하여 보다 효율적인 복제 및 리소스 활용을 위해 사용합니다. 실제로 세 가지 포인트 파일 확장자( <code>kdd</code>, <code>kdi</code>, <code>kdm</code> 의 세 가지 확장자가 모두 누락되었습니다. 루씬 세그먼트가 포인트 파일을 찾을 것으로 예상되는 위치에 어떻게 들어가야 하는데 파일이 없습니다!!! 무서운 손상 버그인 것 같습니다!</p><p></p><h2>모든 버그 수정의 첫 번째 단계, 복제하기</h2><p></p><p>이 특정 버그의 오류를 재현하는 것은 매우 고통스러운 작업이었습니다. Elasticsearch에서는 <a href="https://en.wikipedia.org/wiki/Random_testing">무작위 값 테스트를</a> 활용하지만, 모든 실패를 조사할 수 있도록 모든 실패에 대해 (희망적으로) 재현 가능한 무작위 시드를 제공해야 합니다. 이 방법은 <a href="https://en.wikipedia.org/wiki/Race_condition">경쟁 조건으로</a> 인한 장애를 제외한 모든 장애에 대해 잘 작동합니다.</p>./gradlew ':server:internalClusterTest' --tests "org.elasticsearch.indices.recovery.IndexRecoveryIT.testDoNotInfinitelyWaitForMapping" -Dtests.seed=40853F21F419B395 -Dtests.jvm.argline="-Des.concurrent_search=true" -Dtests.locale=id-ID -Dtests.timezone=Asia/Jerusalem -Druntime.java=21<p>몇 번을 시도해도 특정 시드는 로컬에서 실패를 반복하지 않았습니다. 그러나 테스트를 실행하고 더 반복 가능한 실패를 향해 나아갈 수 있는 방법이 있습니다.</p><p></p><p>특정 테스트 스위트를 사용하면 <code>-Dtests.iters</code> 매개 변수를 통해 동일한 명령에서 지정된 테스트를 두 번 이상 실행할 수 있습니다. 하지만 이것만으로는 충분하지 않았고, 실행 스레드가 전환되어 이 경쟁 조건이 발생할 가능성이 높아지는지 확인해야 했습니다. 시스템의 또 다른 문제점은 테스트 실행 시간이 너무 오래 걸려 테스트 러너가 타임아웃을 일으킨다는 점입니다. 결국 다음과 같은 악몽 배쉬를 사용하여 테스트를 반복적으로 실행했습니다:</p>for run in {1..10}; do ./gradlew ':server:internalClusterTest' --tests "org.elasticsearch.indices.recovery.IndexRecoveryIT.testDoNotInfinitelyWaitForMapping" -Dtests.jvm.argline="-Des.concurrent_search=true" -Dtests.iters=10 ; done || exit 1<p><a href="https://github.com/ColinIanKing/stress-ng">스트레스를</a> 받습니다. 이렇게 하면 점심시간에 CPU 코어를 먹는 프로세스를 빠르게 시작할 수 있습니다. 실패한 테스트를 여러 번 반복하면서 무작위로 스트레스를 스팸으로 전송한 결과 마침내 실패를 재현할 수 있었습니다. 한 걸음 더 가까이. 시스템에 스트레스를 주려면 다른 터미널 창을 열고 실행하면 됩니다:</p>stress-ng --cpu 16<h2>
버그 공개</h2><p>

테스트 실패로 버그가 대부분 반복 가능하므로 이제 원인을 찾아야 할 때입니다. 이 특정 테스트를 이상하게 만드는 것은 루씬이 포인트 값을 기대하기 때문에 던지는 것이지만, 테스트에서 직접 추가하는 것은 없다는 것입니다. 텍스트 값만 입력할 수 있습니다. 이 때문에 저는 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/optimistic-concurrency-control.html">낙관적인 동시성 제어</a> 필드에 대한 최근 변경 사항을 살펴보기로 했습니다: <code>_seq_no</code> 와 <code>_primary_term</code>. 이 두 가지 모두 포인트로 색인되며 모든 Elasticsearch 문서에 존재합니다.</p><p></p><p>실제로 <a href="https://github.com/elastic/elasticsearch/pull/105036">커밋으로</a> 인해 <code>_seq_no</code> 매퍼가 변경되었습니다! 예! 이것이 원인일 것입니다! 하지만 흥분은 잠시뿐이었습니다. 이렇게 하면 문서에 필드가 추가되는 순서만 변경됩니다. 이 변경 전에는 <code>_seq_no</code> 필드가 문서의 마지막에 추가되었습니다. 그 후에 먼저 추가되었습니다. Lucene 문서에 필드를 추가하는 순서 때문에 이 오류가 발생할 리가 없습니다...</p><p></p><p>네, 필드를 추가한 순서를 변경한 것이 실패의 원인이었습니다. 놀랍게도 이것은 Lucene 자체의 버그로 밝혀졌습니다! 구문 분석되는 필드의 순서를 변경해도 문서 구문 분석의 동작은 변경되지 않습니다.</p><p></p><h2>Lucene의 버그</h2><p>실제로 Lucene의 버그는 다음 조건에 초점을 맞췄습니다:</p><ul><li><p>포인트 값 필드 색인화(예 <code>_seq_no</code>)</p></li><li><p>분석 중 텍스트 필드 던지기를 색인화하려고 합니다.</p></li><li><p>이 이상한 상태에서 텍스트 인덱스 분석 예외를 경험하는 작성자로부터 <a href="https://blog.mikemccandless.com/2011/06/lucenes-near-real-time-search-is-fast.html">거의 실시간에 가까운 리더를</a> 엽니다.</p></li></ul><p>하지만 아무리 여러 가지 방법을 시도해도 완벽하게 복제할 수 없었습니다. 저는 Lucene 코드베이스 전체에 디버깅을 위한 일시 중지 지점을 직접 추가했습니다. 예외 경로 중에 무작위로 리더를 열려고 시도했습니다. 이 장애가 발생한 정확한 경로를 찾기 위해 몇 메가바이트의 로그를 출력하기도 했습니다. 도저히 할 수 없었습니다. 하루 종일 싸우고 지는 데 시간을 보냈습니다.</p><p></p><p>그리고 잠을 잤습니다.</p><p></p><p>다음 날 원본 스택 추적을 다시 읽다가 다음 줄을 발견했습니다:</p><p>
</p>    at org.apache.lucene.index.SoftDeletesRetentionMergePolicy.keepFullyDeletedSegment(SoftDeletesRetentionMergePolicy.java:82)<p>모든 재창조를 시도할 때 보존 병합 정책을 구체적으로 설정한 적이 없습니다. <a href="https://github.com/apache/lucene/blob/5f0fa2b291ff9e7d878642f025a70c15b788a470/lucene/core/src/java/org/apache/lucene/index/SoftDeletesRetentionMergePolicy.java">SoftDeletesRetentionMergePolicy는</a> 복제본에서 삭제를 정확하게 복제하고 문서가 실제로 제거될 때 모든 동시성 제어를 담당할 수 있도록 Elasticsearch에서 사용됩니다. 그렇지 않으면 Lucene이 모든 권한을 가지며 병합 시 해당 항목을 제거합니다.</p><p></p><p>이 정책을 추가하고 위에서 언급한 가장 기본적인 단계를 복제하자 장애가 즉시 복제되었습니다.</p><p>
<a href="https://github.com/apache/lucene/issues/13353">루씬에서 버그를</a> 발견한 것이 이렇게 기뻤던 적이 없었습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt89d37c3454ec298b/6a17dd90ec0f8993c75a6511/c7d9ce3de5278923a8454097e2bdf487c861168a-1600x920.jpg" alt="깃허브 이슈 https://github.com/apache/lucene/issues/13353" /><p>
Elasticsearch에서는 경쟁 조건으로 표시되었지만, 모든 조건이 충족되면 Lucene에서는 반복적으로 실패하는 테스트를 작성하는 것이 간단했습니다.</p><p></p><p>결국 모든 좋은 버그가 그렇듯 단 한 줄의 코드로 해결되었습니다. 단 한 줄의 코드로 며칠 동안 작업할 수 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdab62db3402b27f7/6a17dd92dbb4ff23e7fb55ad/2c10a02eb41da181b1dceee47186c076c9b14a90-1600x539.jpg" alt="한 줄의 코드 수정" /><p>하지만 그만한 가치가 있었습니다.</p><h2>
끝이 아닙니다.</h2><p>저와 함께 이 거친 여정이 즐거우셨기를 바랍니다! 소프트웨어, 특히 Elasticsearch와 Apache Lucene처럼 널리 사용되고 복잡한 소프트웨어를 작성하는 것은 보람 있는 일입니다. 하지만 때로는 매우 실망스러울 때도 있습니다. 저는 소프트웨어를 좋아하기도 하고 싫어하기도 합니다. 버그 수정은 끝나지 않았습니다!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/lucene-corrupted-index-exception</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/lucene-corrupted-index-exception</guid>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbc25119c4add97ba/6a17dd94420229633929f4f7/2c918bad62530ff6fbe419092b2ca44bfe9408c4-944x612.jpg" length="0" type="image/jpeg"/>
    <pubDate>Fri, 27 Dec 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[Elasticsearch와 OpenSearch: 벡터 검색 성능 비교]]></title>
    <description><![CDATA[Elasticsearch는 벡터 검색에서 별도 설정 없이 OpenSearch보다 2~12배 더 빠릅니다.]]></description>
    <content:encoded><![CDATA[<p><a href="https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison#up-to-12x-faster-out-of-the-box">요약: Elasticsearch는 최대 12배 더 빠릅니다</a> - Elastic에서는 커뮤니티로부터 시맨틱 검색/벡터 검색 영역에서 특히 Elasticsearch와 OpenSearch의 성능 차이를 명확히 해달라는 요청을 많이 받았습니다. 따라서 명확한 데이터 기반의 비교를 제공하기 위해 이 성능 테스트를 수행했으며, 그 취지는 모호함 없이 사용자에게 명확한 사실만을 전달한다는 데 있었습니다. 결과는 <strong>Elasticsearch가 벡터 검색에서 OpenSearch보다 최대 12배 더 빨라</strong>, 더 적은 컴퓨팅 리소스를 필요로 한다는 것을 보여줍니다. 이는 검색 및 조회용으로 Lucene을 최고의 벡터 데이터베이스로 통합하려는 Elastic의 노력을 반영합니다.</p><p>벡터 검색은 특히 AI, 머신 러닝 같은 분야에서 유사성 검색을 수행하는 방식을 혁신하고 있습니다. 벡터 임베딩 모델의 채택이 증가함에 따라 수백만 개의 고차원 벡터를 효율적으로 검색하는 능력이 중요해지고 있습니다.</p><p>벡터 데이터베이스를 지원하는 데 있어 Elastic과 OpenSearch는 눈에 띄게 다른 접근 방식을 취해 왔습니다. Elastic은 Elasticsearch와 함께 Apache Lucene을 최적화하는 데 막대한 투자를 진행했으며, 이를 통해 벡터 검색 애플리케이션 분야에서 업계 최고 수준의 옵션으로 자리매김했습니다. 이에 비해 OpenSearch는 Lucene의 범위를 넘어 다른 벡터 검색 구현을 통합하며 그 중점 분야를 확장했습니다. Elastic은 전략적으로 Lucene에 집중하여, Elasticsearch 버전에서 고도로 통합된 지원을 제공해 각 구성 요소가 서로의 기능을 보완하고 강화하는 향상된 기능 세트를 제공할 수 있게 되었습니다.</p><p>이 블로그에서는 다양한 구성과 벡터 엔진에 대해 Elasticsearch 8.14와 OpenSearch 2.14를 자세히 비교하고 있습니다. 이 성능 분석에서 Elasticsearch는 벡터 검색 작업에서 우수한 플랫폼임이 입증되었으며, 앞으로 출시될 <a href="https://www.elastic.co/search-labs/blog/vector-similarity-computations-ludicrous-speed">기능들</a>은 이러한 차이를 더욱 <a href="https://www.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">크게</a> 확대할 것입니다. OpenSearch와 비교했을 때 모든 벤치마크 트랙에서 뛰어난 성능을 보였으며, <strong>평균적으로 2~12배 더 빠른 성능을 제공했습니다</strong>. 이는 <code>so_vector</code> (2M 벡터, 768D), <code>openai_vector</code> (2.5M 벡터, 1536D), <code>dense_vector</code> (10M 벡터, 96D)를 포함한 다양한 벡터 수와 차원을 사용하는 시나리오에서 진행되었으며, 모든 항목은 Google 클라우드에서 필요한 인프라를 프로비저닝하기 위한 Terraform 스크립트와 테스트 실행용 Kubernetes 매니페스트와 함께 <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance">이 리포지토리</a>에서 확인할 수 있습니다.</p><p>이 블로그에 자세히 설명된 결과는 <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap">이전에 발표되고 제3자가 검증한 연구</a> 결과를 보완합니다. 해당 연구에서는 Elasticsearch가 가장 일반적인 검색 분석 작업(텍스트 쿼리, 정렬, 범위, 날짜 히스토그램, 용어 필터링)에서 OpenSearch보다 40%–140% 더 빠르다는 것을 보여줍니다. 이제 또 다른 차별화 요소인 벡터 검색을 추가할 수 있습니다.</p><h2>기본 설정으로 최대 12배 더 빠른 성능</h2><p>4개의 벡터 데이터 세트에 대한 집중 벤치마크에는 근사 KNN 및 정확한 KNN 검색이 모두 포함되었습니다. 다양한 크기, 차원, 구성을 고려하여 총 <code>40.189.820</code> 개의 캐시되지 않은 검색 요청에 대해 수행했습니다. 결과: <strong>Elasticsearch가 벡터 검색에서 OpenSearch보다 최대 12배 더 빨라</strong>, 더 적은 컴퓨팅 리소스를 필요로 한다는 것을 보여줍니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt34b83c6eba3bcb6e/6a17d727dbb4ff18d6fb54fb/cdb26e91f085b90e9b12aeb8fee53b04d365ecae-1440x1156.webp" alt="p90 평균" /><p>그림 1: Elasticsearch와 OpenSearch의 다양한 조합에서 수행된 ANN 및 정확한 KNN 작업 그룹.</p><p><code>knn-10-100</code> 같은 그룹은  및  설정의 KNN 검색을 의미합니다. HNSW 벡터 검색에서 는 쿼리 벡터에 대해 검색할 최근접 이웃의 수를 결정합니다. 결과적으로 얼마나 많은 유사한 벡터를 찾을지 지정합니다. 은 각 세그먼트에서 검색할 후보 벡터의 수를 설정합니다. 후보자가 많을수록 정확도가 향상되지만 더 많은 계산 리소스가 필요합니다.</p><p>또한 다양한 양자화 기법과 엔진별 최적화를 활용하여 테스트했으며, 각 트랙, 작업, 벡터 엔진에 대한 자세한 결과는 아래에서 확인할 수 있습니다.</p><h2>정확한 KNN과 근사 KNN</h2><p>다양한 데이터 세트와 사용 사례를 다룰 때, 벡터 검색에 적합한 접근 방식은 달라집니다. 이 블로그에서 <code>knn-*</code> 로 표시된 모든 작업은 <code>knn-10-100</code> 처럼 <strong>근사 KNN</strong>을 사용하고, <code>script-score-*</code> 는 <strong>정확한 KNN</strong>을 참조합니다. 그렇다면 이 둘의 차이점은 무엇이며, 왜 중요할까요?</p><p>본질적으로 더 큰 데이터 세트를 처리할 때는, 확장성이 뛰어난 근사 K-최근접 이웃(ANN) 방법이 선호됩니다. 필터링 프로세스가 필요한 상대적으로 작은 데이터 세트의 경우, 정확한 KNN 방법이 이상적입니다.</p><p>정확한 KNN은 무차별 대입 방식을 사용하여 하나의 벡터와 데이터 세트의 다른 모든 벡터 사이의 거리를 계산합니다. 그런 다음 이 거리의 순위를 매겨 개의 최근접 이웃을 찾습니다. 이 방법은 정확한 일치를 보장하지만, 대규모의 고차원 데이터 세트에 대한 확장성 문제가 있습니다. 그러나 정확한 KNN이 필요한 경우가 많이 있습니다.</p><ul><li><p><strong>리스코어링</strong>: 어휘 또는 시맨틱 검색 후 벡터 기반 리스코어링을 포함하는 시나리오에서는 정확한 KNN이 필수적입니다. 예를 들어, 제품 검색 엔진에서 초기 검색 결과는 텍스트 쿼리(예: 키워드, 카테고리)를 기반으로 필터링할 수 있으며, 필터링된 항목과 연관된 벡터를 사용하여 보다 정확한 유사성 평가를 수행할 수 있습니다.</p></li><li><p><strong>개인화</strong>: 많은 사용자가 각각 비교적 적은 수(예: 1백만)의 고유 벡터로 표현될 때, 사용자별 메타데이터(예: user_id)로 색인을 정렬하고 벡터를 이용한 무차별 대입 스코어링이 효율적입니다. 이 접근 방식을 사용하면 개별 사용자 선호도에 맞춘 정확한 벡터 비교를 기반으로, 개인화된 추천이나 콘텐츠 제공이 가능합니다.</p></li></ul><p>따라서 정확한 KNN은 벡터 유사도에 기반한 최종 순위와 추천이 정확하며 사용자 선호도에 맞게 조정됩니다.</p><p>반면에 근사 KNN(ANN)은 대규모 고차원 데이터 세트에서 정확한 KNN보다 데이터를 더 빠르고 효율적으로 검색할 수 있는 방법을 사용합니다. 쿼리와 모든 지점 사이의 가장 가까운 정확한 거리를 측정하여 계산 및 확장 문제로 이어지는 무차별 대입 방식 대신, ANN은 특정 기술을 사용해 데이터 세트에서 검색 가능한 벡터의 인덱스와 차원을 효율적으로 재구성합니다. 이로 인해 약간의 부정확성이 발생할 수 있지만, 검색 프로세스의 속도가 크게 향상되어 대규모 데이터 세트를 처리하는 데 효과적인 대안이 됩니다.</p><p>이 블로그에서는 <code>knn-*</code> 로 표시된 모든 작업은 <code>knn-10-100</code> 과 같이 <strong>근사 KNN</strong>을 사용하고, <code>script-score-*</code> 는 <strong>정확한 KNN</strong>을 참조합니다.</p><h2>테스트 방법론</h2><p>Elasticsearch와 OpenSearch는 BM25 검색 작업을 위한 API 측면에서 비슷하지만, 후자가 전자의 포크이기 때문에 포크 이후에 도입된 벡터 검색에는 해당되지 않습니다. OpenSearch는 알고리즘에 있어서 Elasticsearch와는 다른 접근 방식을 취했습니다. <code>lucene</code> 외에도, 각각 고유한 구성과 제한 사항을 가진 2개의 다른 엔진인 <code>nmslib</code> 및 <code>faiss</code> 를 도입했습니다. 예를 들어, OpenSearch의 <code>nmslib</code> 은 많은 사용 사례에서 필수적인 기능인 필터를 허용하지 않습니다.</p><p>3개 엔진 모두 계층적으로 탐색 가능한 작은 세계(Hierarchical Navigable Small World, HNSW) 알고리즘을 사용합니다. HNSW 알고리즘은 근사 최근접 이웃을 검색하는 데 효율적이며 고차원 데이터를 처리할 때 특히 강력합니다. <code>faiss</code>는 두 번째 알고리즘인 <code>ivf</code>도 지원하지만, 데이터 세트에 대한 사전 학습이 필요하기 때문에, 여기서는 HNSW에만 집중하려고 합니다. HNSW의 핵심 아이디어는 데이터를 여러 계층의 연결된 그래프로 구성하는 것이며, 각 계층은 데이터 세트의 다양한 세분성을 나타냅니다. 검색은 가장 거친 보기를 제공하는 최상위 레이어에서 시작하여 기본 수준에 도달할 때까지 점점 더 세밀한 레이어로 진행됩니다.</p><p>두 검색 엔진은 모두 공정한 테스트 근거를 확보하기 위해 통제된 환경에서 동일한 조건으로 테스트되었습니다. 적용된 방법은 Elasticsearch, OpenSearch, Rally에 대한 전용 Node 풀을 사용하여 <a href="https://www.elastic.co/blog/elasticsearch-opensearch-performance-gap#testing-methodology">이전에 게시된 성능 비교</a>와 유사합니다. <a href="https://github.com/elastic/elasticsearch-opensearch-vector-performance/blob/main/terraform/main.tf">terraform 스크립트</a>는 (모든 소스와 함께) Kubernetes 클러스터를 프로비저닝하는 데 사용할 수 있습니다.</p><ul><li><p>3개의 <code>e2-standard-32</code> 머신(128GB RAM 및 32개의 CPU)이 있는 Elasticsearch용 Node 풀 1개</p></li><li><p>3개의 <code>e2-standard-32</code> 머신(128GB RAM 및 32개의 CPU)이 있는 OpenSearch용 Node 풀 1개</p></li><li><p>2개의 <code>t2a-standard-16</code> 머신(64GB RAM 및 16개의 CPU)이 있는 Rally용 Node 풀 1개</p></li></ul><p>각 '트랙'(또는 테스트)은 서로 다른 엔진, 서로 다른 구성, 서로 다른 벡터 유형을 포함하는 각 구성에 대해 10회씩 실행되었습니다. 트랙에는 트랙에 따라 1,000번에서 10,000번 반복되는 작업이 있습니다. 예를 들어 네트워크 시간 초과로 인해 트랙의 작업 중 하나가 실패하면, 모든 작업이 폐기되므로 모든 결과는 문제 없이 시작되고 완료된 트랙을 나타냅니다. 모든 테스트 결과는 통계적으로 검증되어, 개선이 우연이 아님을 보장합니다.</p><h2>자세한 결과</h2><p>왜 평균 지연 시간이 아닌 99번째 백분위수를 사용하여 비교해야 할까요? 한 예로 특정 지역의 평균 주택 가격을 들어 보겠습니다. 평균 가격은 비싼 지역을 나타낼 수 있지만, 자세히 살펴보면 대부분의 주택은 훨씬 낮은 가격에 거래되고 일부 고급 부동산만 평균 수치를 부풀리고 있는 것으로 드러날 수 있습니다. 이는 평균 가격이 해당 지역의 주택 가치 전체를 정확하게 나타내지 못할 수 있음을 보여줍니다. 이는 응답 시간를 조사하는 것과 유사하며, 평균값은 중요한 문제를 숨길 수 있습니다.</p><h4>작업</h4><ul><li><p>근사 KNN(k:10 n:50)</p></li><li><p>근사 KNN(k:10 n:100)</p></li><li><p>근사 KNN(k:100 n:1000)</p></li><li><p>근사 KNN(k:10 n:50) 및 키워드 필터 적용</p></li><li><p>근사 KNN(k:10 n:100) 및 키워드 필터 적용</p></li><li><p>근사 KNN(k:100 n:1000) 및 키워드 필터 적용</p></li><li><p>근사 KNN(k:10 n:100) 및 인덱싱 병행</p></li><li><p>정확한 KNN(스크립트 점수)</p></li></ul><h4>벡터 엔진</h4><ul><li><p><code>lucene</code> Elasticsearch와 OpenSearch 모두, 버전 9.10에서</p></li><li><p><code>faiss</code> OpenSearch에서</p></li><li><p><code>nmslib</code> OpenSearch에서</p></li></ul><h4>벡터 유형</h4><ul><li><p><code>hnsw</code> Elasticsearch 및 OpenSearch에서</p></li><li><p><code>int8_hnsw</code> Elasticsearch에서(자동 8비트 양자화 적용 HNSW: <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">링크</a>)</p></li><li><p><code>sq_fp16 hnsw </code>OpenSearch에서(자동 16비트 양자화 적용 HNSW: <a href="https://opensearch.org/docs/2.14/search-plugins/knn/knn-vector-quantization#faiss-16-bit-scalar-quantization">링크</a>)</p></li></ul><h4>기본 설정 상태에서 바로 사용 가능, 동시 세그먼트 검색 지원</h4><p>아시다시피, Lucene은 Java로 작성된 고성능 텍스트 검색 엔진 라이브러리로 Elasticsearch, OpenSearch, Solr 등 많은 검색 플랫폼의 중추적인 역할을 합니다. 핵심적으로 Lucene은 데이터를 세그먼트로 구성하며, 세그먼트는 본질적으로 독립된 인덱스로서 Lucene이 검색을 보다 효율적으로 실행할 수 있게 해 줍니다. 따라서 Lucene 기반 검색 엔진에 검색을 요청하면, 해당 세그먼트에서 순차적으로 또는 병렬로 검색이 실행됩니다.</p><p>OpenSearch는 동시 세그먼트 검색을 선택적 플래그로 도입했지만 기본적으로 사용하지 않습니다. <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/">여기</a>에 자세히 설명된 대로 특수 인덱스 설정 <code>index.search.concurrent_segment_search.enabled</code>을 사용하여 활성화해야 하며, 몇 가지 <a href="https://opensearch.org/docs/latest/search-plugins/concurrent-segment-search/#other-considerations">제한 사항</a>이 있습니다.</p><p>반면 Elasticsearch는 <a href="https://github.com/elastic/elasticsearch/pull/101230">기본 설정으로</a> 세그먼트를 동시에 검색합니다. 따라서 이 블로그에서 비교할 때 다양한 벡터 엔진과 벡터 유형뿐만 아니라 다양한 구성도 고려됩니다.</p><ul><li><p>Elasticsearch ootb: 기본 설정 상태에서 바로 사용 가능한 Elasticsearch, 동시 세그먼트 검색 지원;</p></li><li><p>OpenSearch ootb: 동시 세그먼트 검색이 기본적으로 활성화되지 않은 상태;</p></li><li><p>OpenSearch css: 동시 세그먼트 검색이 활성화된 상태</p></li></ul><p>이제 테스트한 각 벡터 데이터 세트에 대한 자세한 결과를 살펴보겠습니다.</p><h2>250만 개의 벡터, 1,536차원(openai_vector)</h2><p>가장 간단하지만 차원 면에서 가장 큰 트랙인 <a href="https://github.com/elastic/rally-tracks/edit/master/openai_vector">openai_vector</a>부터 시작합니다. 이 트랙은 OpenAI의 <a href="https://openai.com/blog/new-and-improved-embedding-model">text-embedding-ada-002 모델</a>을 사용하여 생성된 임베딩으로 강화된 <a href="https://huggingface.co/datasets/BeIR/nq">NQ 데이터 세트</a>를 사용합니다. 근사 KNN만 테스트하고 작업은 5개뿐이므로 가장 간단합니다. 인덱싱 없이 독립형으로, 인덱싱과 함께, 단일 클라이언트와 8개의 동시 클라이언트를 사용하여 테스트합니다.</p><h3>작업</h3><ul><li><p><strong>standalone-search-knn-10-100-multiple-clients</strong>: 8개의 클라이언트로 250만 개의 벡터 동시에 검색(k: 10, n:100)</p></li><li><p><strong>standalone-search-knn-100-1000-multiple-clients</strong>: 8개의 클라이언트로 250만 개의 벡터 동시에 검색(k: 100, n:1000)</p></li><li><p><strong>standalone-search-knn-10-100-single-client</strong>: 단일 클라이언트로 250만 개의 벡터 검색(k: 10, n: 100)</p></li><li><p><strong>standalone-search-knn-100-1000-single-client</strong>: 단일 클라이언트로 250만 개의 벡터 검색(k: 100, n: 1000)</p></li><li><p><strong>parallel-documents-indexing-search-knn-10-100</strong>: 250만 개의 벡터 검색하는 동시에 추가로 100,000개의 문서를 색인(k:10, n:100).</p></li></ul><p>평균 p99 성능은 다음과 같이 요약됩니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf5791848eb0c12fb/6a17d72925daab9cae08a09e/eea0b2b49c690baada3e09d6968e513bfffe51a9-1440x318.webp" alt="openai_vector 표" /><p>여기서 k:10 및 n:100 조건에서 인덱싱(예: 읽기+쓰기)과 함께 벡터 검색을 수행할 때 Elasticsearch가 OpenSearch보다 <strong>3~ 8배 정도 빠르다</strong>는 것을 관찰했습니다. 또한 동일한  및  조건에서, 인덱싱 없이 수행할 경우 <strong>2~3배 더 빠르다</strong>는 결과가 나왔습니다. :100 및 :1000(<em>standalone-search-knn-100-1000-single-client</em> 및 <em>standalone-search-knn-100-1000-multiple-clients</em>)에서 Elasticsearch는 평균적으로 OpenSearch보다 <strong>2~7배</strong> 더 빠릅니다.</p><p>자세한 결과에서는 정확한 사례와 벡터 엔진을 비교해 보여줍니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltdbe41d3187ced7ec/6a17d72a445de951c44cff4c/a7a761ed631d3e6211beb83d9d93d752d10123c9-1440x1728.webp" alt="openai_vector" /><h4>리콜</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0.969485</p><p>0.995138</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0.781445</p><p>0.784817</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0.96519</p><p>0.995422</p><p>OpenSearch-2.14.0@faiss</p><p>0.984154</p><p>0.98049</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0.980012</p><p>0.97721</p><p>OpenSearch-2.14.0@nmslib</p><p>0.982532</p><p>0.99832</p><h2>1,000만 개의 벡터, 96차원(dense_vector)</h2><p>1천만 개의 벡터, 96차원으로 구성된 <a href="https://github.com/elastic/rally-tracks/tree/master/dense_vector">dense_vector</a>에서. <a href="https://big-ann-benchmarks.com/">Yandex DEEP1B</a> 이미지 데이터 세트를 기반으로 합니다. 데이터 세트는 <code>learn.350M.fbin</code>이라는 '샘플 데이터' 파일의 처음 1천만 개의 벡터로 생성됩니다. 검색 작업은 '쿼리 데이터' 파일 쿼리 <code>public.10K.fbin</code>의 벡터를 사용합니다.</p><p>Elasticsearch와 OpenSearch는 이 데이터 세트에서 매우 우수한 성능을 발휘합니다. 특히 일반적으로 읽기 전용 인덱스에서 수행되는 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html">강제 병합</a> 이후에는 더욱 그렇습니다. 이는 인덱스를 조각 모음하여 검색 대상으로 단일 '테이블'을 구성하는 것과 유사합니다.</p><h3>작업</h3><p>각 작업은 100개의 요청으로 워밍업한 후 1,000개의 요청을 측정합니다.</p><ul><li><p><strong>knn-search-10-100</strong>: 1천만 개의 벡터 검색(k: 10, n: 100)</p></li><li><p><strong>knn-search-100-1000</strong>: 1천만 개의 벡터 검색(k: 100, n: 1000)</p></li><li><p><strong>knn-search-10-100-force-merge</strong>: 강제 병합 후 1천만 개의 벡터 검색(k: 10, n: 100)</p></li><li><p><strong>knn-search-100-1000-force-merge</strong>: 강제 병합 후 1천만 개의 벡터 검색(k: 100, n:1000)</p></li><li><p><strong>knn-search-100-1000-concurrent-with-indexing</strong>: 1천만 개의 벡터를 검색하면서 동시에 <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/challenges/default.json#L76C36-L76C37">데이터 세트의 5%</a> 업데이트(k: 100, n: 1000)</p></li><li><p><strong>script-score-query</strong>: <a href="https://github.com/elastic/rally-tracks/blob/master/dense_vector/queries.json">2천 개의 특정 벡터</a>에 대한 정확한 KNN 검색.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt4629d06af85eb96c/6a17d72c6864a423e7b685dc/174995e0a2156d86359cdb7aa446dfaae6312ea4-1440x316.webp" alt="dense_vector" /><p>Elasticsearch와 OpenSearch 모두 근접 KNN에서 우수한 성능을 보였습니다. <em>knn-search-100-1000-force-merge</em>와 <em>knn-search-10-100-force-merge</em>에서 인덱스가 병합되면(즉, 세그먼트가 하나만 있는 경우) <code>nmslib</code> 와 <code>faiss</code>를 사용할 때 모두 약 15ms로 매우 근접하지만 OpenSearch가 다른 엔진보다 성능이 더 좋습니다.</p><p>그러나 <em>knn-search-10-100</em>과 <em>knn-search-100-1000</em>에서 인덱스에 여러 세그먼트가 있는 경우(인덱스가 문서에 대한 업데이트를 받는 일반적인 상황) Elasticsearch는 대기 시간을 약 7ms와 16ms로 유지하는 반면, 다른 모든 OpenSearch 엔진은 더 느립니다.</p><p>또한 인덱스를 동시에 검색하고 작성하는 경우<em>(knn-search-100-1000-concurrent-with-indexing</em>) Elasticsearch는 지연 시간을 15ms(13.8ms) 미만으로 유지하여 OpenSearch(49.3ms)보다 거의 <strong>4배 빠르고</strong>, 동시 세그먼트 검색이 활성화된 경우에도 여전히 더 빠릅니다(17.9ms). 하지만 차이가 너무 작아 의미 있는 수준이라고 보기는 어렵습니다.</p><p>정확한 KNN의 경우 차이가 훨씬 큽니다. Elasticsearch는 OpenSearch보다 <strong>6배 더 빠릅니다</strong>(약 260ms vs. 1600ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt254f43bcaa3dbfc2/6a17d72ddbb4ffc780fb54ff/17aec6be31117440bc4d1f99984aed95df1c4f6b-1440x1728.webp" alt="dense_vector" /><h4>리콜</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>0.969843</p><p>0.996577</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>0.775458</p><p>0.840254</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>0.971333</p><p>0.996747</p><p>OpenSearch-2.14.0@faiss</p><p>0.9704</p><p>0.914755</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>0.968025</p><p>0.913862</p><p>OpenSearch-2.14.0@nmslib</p><p>0.9674</p><p>0.910303</p><h2>2백만 개의 벡터, 768차원(so_vector)</h2><p>이 <a href="https://github.com/elastic/rally-tracks/tree/master/so_vector">트랙</a> <code>so_vector</code>은 2022년 4월 21일에 <a href="https://archive.org/download/stackexchange/stackoverflow.com-Posts.7z">다운로드한 StackOverflow 게시물 덤프</a>에서 파생되었습니다. 질문 문서만 포함되어 있으며, 답변 문서는 모두 제거되었습니다. 각 질문 제목은 문장 변환기 모델 <a href="https://huggingface.co/sentence-transformers/multi-qa-mpnet-base-cos-v1">multi-qa-mpnet-base-cos-v1</a>을 사용하여 벡터로 인코딩되었습니다. 이 데이터 세트에는 처음 2백만 개의 질문이 포함되어 있습니다.</p><p>이전 트랙과 달리, 각 문서에는 필터링 및 하이브리드 검색을 통한 근접 KNN과 같은 테스트 기능을 지원하기 위해 벡터 외에 다른 필드가 포함되어 있습니다. OpenSearch용<code>nmslib</code> 는 <a href="https://opensearch.org/docs/latest/search-plugins/knn/filter-search-knn/#k-nn-search-with-filters">필터를 지원하지 않기 때문에</a> 이 테스트에는 포함되지 않았습니다.</p><h3>작업</h3><p>각 작업은 100개의 요청으로 워밍업한 후, 100개의 요청을 측정합니다. 테스트에는 16개의 검색 유형 * 2개의 서로 다른 k 값 * 3개의 서로 다른 n 값이 포함되어 있으므로, 단순화를 위해 작업을 그룹화했습니다.</p><ul><li><p><strong>knn-10-50</strong>: 필터 없이 2백만 개의 벡터 검색(k:10, n:50)</p></li><li><p><strong>knn-10-50-filtered</strong>: <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">필터를 적용</a>하여 2백만 개의 벡터 검색(k:10, n:50)</p></li><li><p><strong>knn-10-50-after-force-merge</strong>: 강제 병합 후 필터를 적용하여 2백만 개의 벡터 검색(k:10, n:50)</p></li><li><p><strong>knn-10-100</strong>: 필터 없이 2백만 개의 벡터 검색(k:10, n:100)</p></li><li><p><strong>knn-10-100-filtered</strong>: <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">필터를 적용하여</a> 2백만 개의 벡터 검색(k:10, n:100)</p></li><li><p><strong>knn-10-100-after-force-merge</strong>: 강제 병합 후 필터를 적용하여 2백만 개의 벡터 검색(k:10, n:100)</p></li><li><p><strong>knn-100-1000</strong>: 필터 없이 2백만 개의 벡터 검색(k:100, n:1000)</p></li><li><p><strong>knn-100-1000-filtered</strong>: <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json">필터를 적용하여</a> 2백만 개의 벡터 검색(k:100 , n:1000)</p></li><li><p><strong>knn-100-1000-after-force-merge</strong>: 강제 병합 후 필터를 적용하여 2백만 개의 벡터 검색(k:100, n:1000)</p></li><li><p><strong>exact-knn</strong>: <a href="https://github.com/elastic/rally-tracks/blob/master/so_vector/operations/default.json#L56">필터 적용 여부에 관계없이</a> 정확한 KNN 검색.</p></li></ul><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8b44e1306af35877/6a17d72f577262aca11bca3e/d4ed2982d55370cad4b2048b23ce97caa55017c0-1440x316.webp" alt="so_vector 표" /><p>Elasticsearch는 이 테스트에서 기본 설정 상태에서 OpenSearch보다 <strong>일관되게 빠릅니다</strong>. OpenSearch가 더 빠른 경우는 두 가지뿐이며, 그 차이도 크지 않습니다(<em>knn-10-100</em> 및 <em>knn-100-1000</em>). 필터와 함께 <em>knn-10-50</em>, <em>knn-10-100</em>, <em>knn-100-1000</em> 작업은 최대 <strong>7배</strong>(112ms vs 803ms)의 차이를 보입니다.</p><p>두 솔루션의 성능은 '강제 병합' 후에 균등해 지는 것으로 보이며, 이는 <em>knn-10-50-after-force-merge</em>, <em>knn-10-100-after-force-merge</em>, <em>knn-100-1000-after-force-merge</em> 에서 확인할 수 있습니다. 그러한 작업에서 <code>faiss</code>가 더 빠릅니다.</p><p>정확한 KNN의 성능은 다시 한 번 매우 다르게 나타났으며, 이번에는 Elasticsearch가 OpenSearch보다 <strong>13배 더 빨랐습니다</strong>(약 385ms vs 5262ms).</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltf80ccc7c2df84559/6a17d7314b055de00d43203f/615cb9228eb05ddd2e9512b3a6a5bc88d4088a1a-1440x1440.webp" alt="so_vector" /><h4>리콜</h4><p></p><p>knn-recall-10-100</p><p>knn-recall-100-1000</p><p>knn-recall-10-50</p><p>Elasticsearch-8.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>Elasticsearch-8.14.0@lucene-int8_hnsw</p><p>1</p><p>0.986667</p><p>1</p><p>OpenSearch-2.14.0@lucene-hnsw</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@faiss-sq_fp16</p><p>1</p><p>1</p><p>1</p><p>OpenSearch-2.14.0@nmslib</p><p>0.9674</p><p>0.910303</p><p>0.976394</p><h2>Elasticsearch와 Lucene이 명백한 승자</h2><p>Elastic에서는 RAG(Retrieval-Augmented Generation)를 비롯한 검색 및 조회 사용 사례를 위한 최고의 벡터 데이터베이스를 제공할 수 있도록 Apache Lucene과 Elasticsearch를 끊임없이 혁신하고 있습니다. 최근의 발전으로 성능이 획기적으로 향상되어 벡터 검색이 이전보다 <a href="https://search-labs.elastic.co/search-labs/blog/elasticsearch-lucene-vector-database-gains">더 빠르고 공간 효율적</a>이 되었으며, 이는 Lucene 9.10에서 얻은 이점을 기반으로 합니다. 이 블로그는 최신 버전을 비교한 결과 Elasticsearch가 OpenSearch보다 최대 12배 빠르다는 연구를 소개합니다.</p><p>두 제품 모두 동일한 버전의 Lucene(<a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/release-notes-8.14.0.html">Elasticsearch 8.14 릴리즈 노트</a> 및 <a href="https://github.com/opensearch-project/OpenSearch/blob/2.14/release-notes/opensearch.release-notes-2.14.0.md">OpenSearch 2.14 릴리즈 노트</a>)을 사용한다는 점은 주목할 만합니다.</p><p>Elastic의 빠른 혁신은 온프레미스 및 Elastic Cloud 고객뿐만 아니라 당사의 <a href="https://www.elastic.co/search-labs/blog/stateless-your-new-state-of-find-with-elasticsearch">무상태(stateless) 플랫폼</a>을 사용하는 고객에게도 더 많은 혜택을 가져다 줄 것입니다. <a href="https://www.elastic.co/search-labs/blog/int4-scalar-quantization-in-lucene">int4에 대한 스칼라 양자화</a> 지원과 같은 기능은 <a href="https://www.elastic.co/search-labs/blog/evaluating-scalar-quantization">int8 테스트</a>와 마찬가지로, 엄격한 테스트를 통해 고객이 재현율 저하 없이 이러한 기술을 활용할 수 있도록 보장할 것입니다.</p><p>AI 및 머신 러닝 애플리케이션의 확산으로 인해 최신 검색 엔진에서 벡터 검색 효율성은 타협할 수 없는 기능이 되고 있습니다. 대용량·고복잡 벡터 데이터의 요구를 충족할 수 있는 강력한 검색 엔진을 찾고 있는 조직에게는 Elasticsearch가 확실한 해답입니다.</p><p>기존 플랫폼을 확장하든 새로운 프로젝트를 시작하든, 벡터 검색 요구 사항을 위해 Elasticsearch를 통합하는 것은 가시적이고 장기적인 이점을 얻을 수 있는 전략적 움직임입니다. 입증된 성능 우위를 바탕으로 Elasticsearch는 차세대 검색 혁신을 이끌 준비가 되어 있습니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/elasticsearch-opensearch-vector-search-performance-comparison</guid>
    <category><![CDATA[벡터 데이터베이스]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Ugo Sangiorgi]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt5d70b25967c2194e/6a17d732b1e11383f879f0ca/13c3c0053e2968fb835ba2f90f34bec3a011b5c0-880x592.webp" length="0" type="image/webp"/>
    <pubDate>Wed, 26 Jun 2024 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[루씬의 스칼라 양자화 이해하기]]></title>
    <description><![CDATA[자동 바이트 양자화, 세그먼트별 양자화, &amp; 성능 인사이트를 포함하여 Elastic이 어떻게 Lucene에 스칼라 양자화를 도입했는지 살펴보세요.]]></description>
    <content:encoded><![CDATA[<h2>Lucene의 자동 바이트 정량화</h2><p>HNSW는 벡터를 저장하고 검색하는 강력하고 유연한 방법이지만, 빠르게 실행하려면 상당한 양의 메모리를 필요로 합니다. 예를 들어, 768차원의 1MM float32 벡터를 쿼리하려면 약  램이 필요합니다. 상당한 수의 벡터를 검색하기 시작하면 비용이 많이 듭니다. 약  적은 메모리를 사용하는 한 가지 방법은 바이트 정량화를 사용하는 것입니다. Lucene과 그에 따른 Elasticsearch는 한동안  벡터 인덱싱을 지원했지만 이러한 벡터를 구축하는 것은 사용자의 책임이었습니다. 이제 곧 Lucene에  스칼라 양자화가 도입될 예정입니다.</p><h2>스칼라 양자화 101</h2><p>모든 양자화 기술은 원시 데이터의 손실 변환으로 간주됩니다. 즉, 공간 확보를 위해 일부 정보가 손실된다는 의미입니다. 스칼라 양자화에 대한 자세한 설명은 다음을 참조하세요: <a href="https://www.elastic.co/search-labs/scalar-quantization-101">스칼라 양자화 101을</a> 참조하세요. 높은 수준에서 스칼라 양자화는 손실 압축 기술입니다. 간단한 계산을 통해 리콜에 거의 영향을 미치지 않으면서도 공간을 크게 절약할 수 있습니다.</p><h2>아키텍처 살펴보기</h2><p>Elasticsearch 작업에 익숙하신 분들은 이러한 개념에 이미 익숙하실 수도 있지만, 검색을 위한 문서 배포에 대한 간략한 개요는 다음과 같습니다.</p><p>각 Elasticsearch 인덱스는 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/size-your-shards.html#size-your-shards">여러 개의 샤드로</a> 구성됩니다. 각 샤드는 단일 노드에만 할당할 수 있지만, 인덱스당 여러 개의 샤드를 사용하면 노드 간에 컴퓨팅 병렬 처리를 할 수 있습니다.</p><p>각 샤드는 하나의 <a href="https://lucene.apache.org/core/9_8_0/core/org/apache/lucene/index/package-summary.html">루씬 인덱스로</a> 구성됩니다. Lucene 인덱스는 여러 개의 읽기 전용 세그먼트로 구성됩니다. 색인하는 동안 문서가 버퍼링되고 주기적으로 읽기 전용 세그먼트로 플러시됩니다. 특정 조건이 충족되면 이러한 세그먼트는 백그라운드에서 더 큰 세그먼트로 병합될 수 있습니다. 이 모든 것은 구성할 수 있으며 나름의 복잡성을 가지고 있습니다. 그러나 세그먼트와 병합에 대해 이야기할 때는 읽기 전용 Lucene 세그먼트와 이러한 세그먼트의 자동 주기적 병합에 대해 이야기하고 있습니다. 세그먼트 병합 및 디자인 결정에 대해 <a href="https://www.elastic.co/search-labs/vector-search-elasticsearch-rationale">자세히 알아보세요</a>.</p><h2>루씬의 세그먼트별 정량화</h2><p>Lucene의 모든 세그먼트에는 개별 벡터, HNSW 그래프 인덱스, 양자화된 벡터, 계산된 사분위수 등이 저장됩니다. 간결성을 위해 여기서는 Lucene이 정량화된 벡터와 원시 벡터를 저장하는 방식에 초점을 맞추겠습니다. 모든 세그먼트에 대해  파일의 원시 벡터, 양자화된 벡터 및 단일 보정 승수 플로트( ), 그리고  파일 내의 양자화 관련 메타데이터를 추적합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1861d0af970fd096/6a17d9887f6f15a45dc099c0/507a4d578f8a5d2225b11d16ac1fc0b7dd39eaa1-1207x228.png" alt=".vec 파일" /><p>그림 1: 원시 벡터 스토리지 파일의 단순화된 레이아웃.  값은 4바이트이므로  디스크 공간을 차지합니다. 정량화 중이므로 HNSW 검색 중에는 로드되지 않습니다. 특별히 요청된 경우에만 사용됩니다(예 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#query-rescorer">재점수를</a> 통한 무차별 대입) 또는 세그먼트 병합 중 재정량화를 위해 사용합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt8c52b51939357280/6a17d9897b54f9130f8b3761/ffc6e8bffefc5cd36f14aae315184c89e04a6587-1440x207.png" alt=".veq 파일" /><p>그림 2:  단순화된 레이아웃 파일을 만듭니다.  공간을 차지하며 검색 중에 메모리에 로드됩니다.  정확도와 기억력을 높이기 위해 점수를 조정하는 데 사용되는 보정 승수 플로트를 설명하는 값입니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt28e007988f81e5c0/6a17d98b25daabe23208a0d9/555cc92d7d179d8c9711cefbaaac3e0b815d2e42-1440x182.png" alt=".vmq 파일" /><p>그림 3: 메타데이터 파일의 단순화된 레이아웃. 여기에서 이 세그먼트에 대해 계산된 사분위수와 함께 양자화 및 벡터 구성을 추적합니다.</p><p>따라서 각 세그먼트에 대해 양자화된 벡터뿐만 아니라 이러한 양자화된 벡터를 만드는 데 사용된 분위수와 원래의 원시 벡터를 저장합니다. 그렇다면 왜 원시 벡터를 보관하는 것일까요?</p><h2>사용자와 함께 성장하는 정량화</h2><p>Lucene은 주기적으로 읽기 전용 세그먼트를 플러시하기 때문에 각 세그먼트는 모든 데이터의 일부만 볼 수 있습니다. 즉, 전체 데이터의 해당 샘플 세트에 대해서만 계산된 사분위수가 직접 적용됩니다. 샘플이 전체 말뭉치를 적절히 대표한다면 큰 문제가 되지 않습니다. 하지만 Lucene을 사용하면 다양한 방식으로 인덱스를 정렬할 수 있습니다. 따라서 세그먼트별 사분위수 계산에 편향을 추가하는 방식으로 정렬된 데이터를 인덱싱할 수 있습니다. 또한 원할 때마다 데이터를 플러시할 수 있습니다! 샘플 세트는 하나의 벡터일 정도로 작을 수 있습니다. 또 다른 장점은 병합이 발생하는 시기를 제어할 수 있다는 점입니다. 기본값과 주기적 병합이 설정되어 있지만, <a href="https://www.elastic.co/guide/en/elasticsearch/reference/8.10/indices-forcemerge.html">_force_merge</a> API를 통해 원할 때마다 병합을 요청할 수 있습니다. 그렇다면 어떻게 하면 이 모든 유연성을 허용하면서도 좋은 리콜을 제공하는 우수한 정량화를 제공할 수 있을까요?</p><p>루씬의 벡터 양자화는 시간이 지남에 따라 자동으로 조정됩니다. Lucene은 읽기 전용 세그먼트 아키텍처로 설계되었기 때문에 각 세그먼트의 데이터가 변경되지 않았음을 보장하고 업데이트가 가능한 시점을 코드에 명확하게 구분합니다. 즉, 세그먼트 병합 중에 필요에 따라 사분위수를 조정하고 벡터를 다시 정량화할 수 있습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt0d9a2390958c3e82/6a17d98cbe6086e4fd0045d5/646baa8279719d06c05c1a4fdb80a0c060e0dd94-1440x446.png" alt="여러 세그먼트 백분위수" /><p>그림 4: 서로 다른 사분위수를 가진 세 가지 예시 세그먼트.</p><p>하지만 재정량화에는 비용이 많이 들지 않을까요? 약간의 오버헤드가 있긴 하지만, Lucene은 지능적으로 사분위수를 처리하고 필요한 경우에만 완전히 정량화합니다. 그림 4의 세그먼트를 예로 들어 보겠습니다. 세그먼트   각각  문서를, 세그먼트   문서만 제공한다고 가정해 보겠습니다. Lucene은 사분위수의 가중 평균을 취하고 그 결과 병합된 사분위수가 세그먼트의 원래 사분위수에 충분히 근접하면 해당 세그먼트를 다시 정량화할 필요가 없으며 새로 병합된 사분위수를 활용합니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blte2596c10ee616cf3/6a17d98eec0f8903b85a6497/58fa20d60dc337ca7108eb01e84a07c330e87ee6-1440x447.png" alt="병합된 사분위수" /><p>그림 5: 세그먼트    문서가 있고   문서만 있는 병합된 사분위수의 예입니다.</p><p>그림 5에서 시각화된 상황을 보면 병합된 결과 사분위수가   원래 사분위수와 매우 유사하므로 벡터를 정량화하는 것이 정당화되지 않음을 알 수 있습니다. 세그먼트 , 너무 많이 벗어난 것 같습니다. 결과적으로  벡터는 새로 병합된 사분위수 값으로 다시 정량화됩니다.</p><p>실제로 병합된 사분위수가 원래의 사분위수와 극적으로 다른 극단적인 경우가 있습니다. 이 경우 각 세그먼트에서 샘플을 가져와서 사분위수를 완전히 다시 계산합니다.</p><h2>정량화 성능 &amp; 숫자</h2><p>그렇다면 속도가 빠르며 여전히 좋은 기억력을 제공하나요? <code>c3-standard-8</code> GCP 인스턴스에서 실험을 실행하여 수집한 수치는 다음과 같습니다.  공정한 비교를 위해 메모리에 원시 벡터를 저장할 수 있을 만큼 큰 인스턴스를 사용했습니다. 최대 내부 제품을 사용하여  <a href="https://huggingface.co/datasets/Cohere/wikipedia-22-12-simple-embeddings">Cohere Wiki</a> 벡터를 색인화했습니다.</p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltfa48e3724ad97fed/6a17d98f445de96aca4cff88/81958f99b8a1397f669db48de00f264cacc6b49f-576x455.png" alt="정량화 리콜" /><p>그림 6: 양자화된 벡터와 원시 벡터의 Recall@10. 양자화된 벡터의 검색 성능은 원시 벡터보다 훨씬 빠르며, 5개만 더 수집해도 리콜을 빠르게 복구할 수 있습니다(  표시).</p><p>그림 6은 그 이야기를 보여줍니다. 예상대로 리콜 차이가 있긴 하지만, 그 차이는 크지 않습니다. 그리고 벡터를 5개만 더 수집하면 리콜 차이가 사라집니다. 이 모든 것이  빠른 세그먼트 병합과  벡터의 1/4 메모리로 가능합니다.</p><h2>결론</h2><p>Lucene은 어려운 문제에 대한 고유한 솔루션을 제공합니다. 정량화에는 '훈련' 또는 '최적화' 단계가 필요하지 않습니다. Lucene에서는 그냥 작동합니다. 데이터가 바뀌어도 벡터 인덱스를 '다시 학습'해야 할 염려가 없습니다. Lucene은 중요한 변경 사항을 감지하고 데이터의 수명 기간 동안 이를 자동으로 처리합니다. 이 기능을 Elasticsearch에 언제 도입할지 기대해 주세요!</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/scalar-quantization-in-lucene</guid>
    <category><![CDATA[Lucene]]></category>
    <category><![CDATA[ML 연구]]></category>
    <dc:creator><![CDATA[Benjamin Trent]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltb382d99ffac9992c/6a17d991b1e113640179f12c/dc07f16d347a68476bded5df9adb831452f61278-1024x1024.jpg" length="0" type="image/jpeg"/>
    <pubDate>Sat, 11 Nov 2023 00:00:00 GMT</pubDate>
  </item>
  <item>
    <title><![CDATA[학술 논문 구현하기: Elasticsearch와 Lucene에서 배운 교훈]]></title>
    <description><![CDATA[연구 논문을 소프트웨어 애플리케이션에 통합하는 전략에 대해 알아보고, Elasticsearch와 Lucene에 대한 경험을 바탕으로 살펴보세요.]]></description>
    <content:encoded><![CDATA[<p>이 게시물에서는 소프트웨어 애플리케이션에서 학술 논문을 구현하는 전략을 공유합니다. 다른 엔지니어들이 저희의 경험을 통해 배울 수 있기를 바라는 마음에서 Elasticsearch와 Lucene의 사례를 활용했습니다. 이러한 전략을 읽고 "하지만 이건 소프트웨어 개발일 뿐이야!"라고 생각할 수도 있습니다. 엔지니어로서 우리는 이미 올바른 관행과 도구를 갖추고 있으며, 새로운 도전에 적응하기만 하면 됩니다.</p><h2>배경</h2><p>Elasticsearch를 개발하는 동안, 우리는 때때로 이를 해결하기 위한 간단하거나 확립된 접근 방식이 없는 중요한 문제에 직면하게 됩니다. "흠, 이 문제를 다룬 학술 논문이 있나요?"라고 묻는 것은 당연한 질문입니다. 때로는 학문적 연구가 영감의 원천이 되기도 합니다. 새로운 알고리즘이나 데이터 구조를 제안하는 논문을 접하고 "정말 유용할 것 같다!"라고 생각하게 됩니다. 다음은 Elasticsearch와 Apache Lucene이 학술 작업을 통합하는 방법의 몇 가지 예입니다:</p><ul><li><p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/search-aggregations-metrics-cardinality-aggregation.html">카디널리티 집계를 위한</a> <a href="https://research.google/pubs/pub40671/">HyperLogLog++</a></p></li><li><p><a href="https://www.elastic.co/blog/improving-response-latency-in-elasticsearch-with-adaptive-replica-selection">적응형 복제본 선택을</a> <a href="https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-suresh.pdf">위한 C3 알고리즘</a></p></li><li><p>루씬에서 가장 가까운 벡터 검색을 위한 <a href="https://arxiv.org/abs/1603.09320">계층적 탐색 가능한 작은 세계 그래프(HNSW)</a></p></li><li><p><a href="https://github.com/elastic/ml-cpp/pull/488">머신 러닝 분류를</a> 개선하기<a href="https://jmlr.csail.mit.edu/papers/volume17/15-308/15-308.pdf">위한 MIC 통계</a></p></li><li><p><a href="https://www.elastic.co/blog/faster-retrieval-of-top-hits-in-elasticsearch-with-block-max-wand">Lucene에서 더 빠른 인기 검색을</a> 위한<a href="http://engineering.nyu.edu/~suel/papers/bmw.pdf">블록 최대 WAND</a></p></li><li><p>... 그리고 <a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.15/query-dsl-combined-fields-query.html">더 많은</a> <a href="https://github.com/elastic/elasticsearch/blob/b2a9328890b23e7ccf6c66a3b13d6d65e453a3dd/server/src/main/java/org/elasticsearch/search/sort/BucketedSort.java#L302-L323">것</a></p></li></ul><p>학술 논문은 데이터 집약적인 시스템을 개발하는 엔지니어에게 매우 귀중한 자료입니다. 그러나 알고리즘 설명이 복잡하고 중요한 실용적인 세부 사항이 생략된 경우가 많아 구현하기가 어렵고 오류가 발생하기 쉽습니다. 예를 들어, 데이터 세트에 따라 결과가 크게 달라지는 머신 러닝 알고리즘을 어떻게 철저하게 테스트할 수 있을까요?</p><h2>소프트웨어 종속성을 평가할 때와 마찬가지로 논문을 평가하세요.</h2><p>새 소프트웨어 종속성을 추가하려면 신중한 평가가 필요합니다. 다른 패키지가 부정확하거나 느리거나 안전하지 않은 경우 우리 프로젝트도 마찬가지일 수 있기 때문입니다. 개발자는 종속성을 가져오기 전에 종속성의 품질을 평가해야 합니다.</p><p>구현을 고려 중인 학술 논문에도 동일하게 적용됩니다. 알고리즘이 논문에 발표되었기 때문에 정확하고 성능이 좋아야 한다고 생각할 수 있습니다. 그러나 검토 과정을 통과했더라도 학술 논문에는 문제가 있을 수 있습니다. 정확성 증명은 현실적이지 않은 가정에 의존하는 것일 수도 있습니다. 또는 '실험' 섹션에서 기준선보다 훨씬 더 나은 성능을 보여 주지만 이는 특정 데이터 세트에만 해당됩니다. 논문 품질이 뛰어나더라도 그 접근 방식이 프로젝트에 적합하지 않을 수 있습니다.</p><p>학술 논문에 대한 '의존성' 여부를 고려할 때는 소프트웨어 패키지에 대해 동일한 질문을 하는 것이 도움이 됩니다:</p><ul><li><p>라이브러리가 널리 사용되고 '실전 테스트'를 거쳤나요? → 다른 패키지에서 이 문서를 구현한 적이 있으며 잘 작동했나요?</p></li><li><p>성능 벤치마크가 제공되나요? 정확하고 공정해 보이나요? → 논문에 실제 실험이 포함되어 있나요? 디자인이 잘 되어 있나요?</p></li><li><p>성능 개선이 복잡성을 정당화할 만큼 충분히 큰가요? → 논문이 강력한 기준 접근 방식과 비교되나요? 이 기준선을 얼마나 능가하나요?</p></li><li><p>이 접근 방식이 우리 시스템과 잘 통합되나요? → 알고리즘의 가정과 트레이드오프가 우리 사용 사례에 맞는가?</p></li></ul><p>소프트웨어 패키지가 경쟁사와의 성능 비교를 발표할 때 항상 가장 빠르게 나오는 패키지가 있습니다! 제3자가 벤치마크를 설계했다면 더 균형 잡힌 결과를 얻을 수 있습니다. 학술 논문에도 동일한 현상이 적용됩니다. 알고리즘이 원본 논문에서 좋은 성능을 보였을 뿐만 아니라 다른 논문에서도 강력한 기준이 되는 것으로 나타난다면 그 알고리즘은 견고할 가능성이 매우 높습니다.</p><h2>창의적으로 테스트하기</h2><p>학술 논문의 알고리즘은 우리가 일상적으로 접하는 알고리즘 유형보다 더 정교한 동작을 하는 경우가 많습니다. 아마도 더 나은 속도를 위해 정확도를 희생하는 근사 알고리즘일 것입니다. 또는 대규모 데이터 세트를 받아 (때로는 예상치 못한) 결과를 생성하는 머신 러닝 방법일 수도 있습니다. 이러한 알고리즘의 동작을 간단한 방법으로 특성화할 수 없다면 어떻게 테스트를 작성할 수 있을까요?</p><h3>불변값에 집중</h3><p>단위 테스트를 설계할 때 알고리즘에 이 예제 입력을 제공하면 해당 출력을 가져야 한다는 식으로 예제 측면에서 생각하는 것이 일반적입니다. 안타깝게도 대부분의 수학 알고리즘은 예제 기반 테스트만으로는 그 동작을 충분히 커버할 수 없습니다.</p><p>Elasticsearch가 검색 요청을 처리할 노드를 파악하는 데 사용하는 C3 알고리즘을 살펴보겠습니다. 노드의 이전 서비스 및 응답 시간, 대기열 크기를 통합하는 미묘한 공식을 사용하여 각 노드의 순위를 매깁니다. 몇 가지 예를 테스트한다고 해서 공식을 제대로 이해했는지 확인할 수 있는 것은 아닙니다. 서비스 시간이 증가하면 노드의 순위가 감소하는가? 등 한 발 물러서서 불변성 테스트에 대해 생각해 보는 것도 도움이 됩니다. 대기열 크기가 0인 경우, 논문에서 주장한 것처럼 응답 시간에 따라 순위가 결정되나요?</p><p>불변값에 집중하면 여러 가지 일반적인 경우에 도움이 될 수 있습니다:</p><ul><li><p>이 방법은 순서에 구애받지 않아야 하나요? 그렇다면 입력 데이터를 다른 순서로 전달해도 동일한 출력을 얻을 수 있습니다.</p></li><li><p>알고리즘의 어떤 단계가 클래스 확률을 생성하나요? 그렇다면 이 확률의 합계는 1이 되어야 합니다.</p></li><li><p>함수가 원점을 중심으로 대칭인가요? 그렇다면 입력의 부호를 뒤집으면 출력의 부호도 뒤집히게 됩니다.</p></li></ul><p>C3를 처음 구현할 때 응답 시간 대신 응답 시간의 역수를 실수로 사용하는 공식에 버그가 있었습니다. 이는 느린 노드가 더 높은 순위를 차지할 수 있다는 것을 의미했습니다! 문제를 수정할 때 향후 실수를 방지하기 위해 <a href="https://github.com/elastic/elasticsearch/pull/70283">불변 확인을 추가했습니다</a>.</p><h3>레퍼런스 구현과 비교</h3><p>저자들은 논문과 함께 알고리즘의 구현을 공개할 예정입니다. (많은 저널에서 저자가 결과 재현을 위한 코드를 게시하도록 요구하기 때문에 논문에 실험이 포함된 경우 특히 그렇습니다.) 이 참조 구현과 비교하여 접근 방식을 테스트하여 알고리즘의 중요한 세부 사항을 놓치지 않았는지 확인할 수 있습니다.</p><p>가장 가까운 이웃 검색을 위한 Lucene의 HNSW 구현을 개발하는 동안, 논문 저자의 <a href="https://issues.apache.org/jira/browse/LUCENE-9937">참조 라이브러리와 비교하여 테스트했습니다</a>. 동일한 데이터 세트에 대해 Lucene과 라이브러리를 모두 실행하여 결과의 정확도와 수행한 계산 횟수를 비교했습니다. 이 수치가 거의 일치하면 Lucene이 알고리즘을 충실히 구현한다는 것을 알 수 있습니다.</p><p>알고리즘을 시스템에 통합할 때는 여러 코어로 확장하거나 휴리스틱을 추가하여 성능을 개선하는 등 수정 또는 확장을 해야 하는 경우가 많습니다. 먼저 "바닐라" 버전을 구현하고 레퍼런스와 비교하여 테스트한 다음 점진적으로 변경하는 것이 가장 좋습니다. 이렇게 하면 사용자 지정하기 전에 모든 핵심 부분을 캡처했다고 확신할 수 있습니다.</p><h3>기존 알고리즘과의 결투</h3><p>마지막 섹션에서는 테스트 불변수에 대한 또 다른 아이디어를 제시합니다. 알고리즘의 출력을 더 간단하고 이해하기 쉬운 알고리즘의 출력과 비교하는 것입니다. 예를 들어, 상위 결과에 표시되지 않는 문서를 건너뛰어 문서 검색 속도를 높여주는 Lucene의 블록 최대 WAND 알고리즘을 생각해 보세요. 모든 경우에 블록맥스 WAND가 어떻게 작동해야 하는지 정확히 설명하기는 어렵지만, 적용한다고 해서 상위 결과가 바뀌지는 않는다는 것은 알고 있습니다! 따라서 테스트에서는 여러 개의 무작위 검색 쿼리를 생성한 다음 <a href="https://github.com/apache/lucene/blob/main/lucene/core/src/test/org/apache/lucene/search/TestWANDScorer.java#L669">WAND 최적화를 적용하거나 적용하지 않은 상태에서 실행하여</a> 결과가 항상 일치하는지 확인할 수 있습니다.</p><p>이러한 테스트의 중요한 측면은 비교를 실행하기 위해 <a href="https://www.elastic.co/blog/elasticsearch-testing-qa-increasing-coverage-randomizing-test-runs">무작위 입력을 생성한다는</a> 것입니다. 이를 통해 생각하지 못했던 사례를 연습하고 예상치 못한 문제를 발견할 수 있습니다. 예를 들어, BM25F 채점에 대한 Lucene의 무작위 비교 테스트는 <a href="https://issues.apache.org/jira/browse/LUCENE-10039">미묘한 에지 케이스에서 버그를 발견하는</a> 데 도움이 되었습니다. 알고리즘에 무작위 입력을 공급하는 아이디어는 컴퓨터 보안의 일반적인 테스트 기법인 <a href="https://en.wikipedia.org/wiki/Fuzzing">퍼징</a> 개념과 밀접한 관련이 있습니다.</p><p>Elasticsearch와 Lucene은 이 테스트 접근 방식을 자주 사용합니다. 두 알고리즘(TestDuelingAnalyzers, testDuelTermsQuery...) 간에 "결투" 를 언급하는 테스트가 표시되면 이 전략이 실행 중인 것입니다.</p><h2>논문 용어 사용</h2><p>다른 개발자가 여러분의 코드를 작업할 때는 해당 문서를 참조하여 세부 사항을 따라야 합니다. <a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/search/aggregations/metrics/HyperLogLogPlusPlus.java#L24-L39">Elasticsearch의 HyperLogLog++ 구현에 대한 코멘트가</a> 이를 잘 설명해줍니다: "논문을 읽지 않고 이 클래스가 하는 일을 이해하려고 하는 것은 모험적인 일로 간주됩니다." 이 메서드 주석도 좋은 예가 됩니다. 여기에는 학술 논문 링크가 포함되어 있으며, 원래 설명된 알고리즘에 어떤 수정 사항이 있었는지 강조 표시되어 있습니다.</p><p>개발자는 문서를 기반으로 코드를 이해하게 되므로 동일한 용어를 사용하는 것이 도움이 됩니다. 수학적 표기는 간결하기 때문에 일반적으로 '좋은 스타일'로 간주되지 않지만 논문의 맥락에서는 매우 명확한 이름이 될 수 있습니다. 학술 논문의 공식은 <a href="https://github.com/elastic/elasticsearch/blob/4f22f437ee50cacb94b37b457be1da0b8ba0e8ce/server/src/main/java/org/elasticsearch/node/ResponseCollectorService.java#L151">rS, muBarSInverse와</a> 같은 암호화된 변수 이름을 Elasticsearch에서 접하게 되는 몇 안 되는 경우 중 하나입니다.</p><p>
<em>저자가 추천하는 논문 읽기 방법: 큰 커피와 함께.</em></p><img src="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt12d81453c706f7cb/6a17d80e0b0bede6badd3424/d03a8e2a50e173b15a4ec3732810c63ff53321f6-1440x1081.jpg" alt="elastic-blog-학술논문.jpg" /><h2>작성자에게 이메일을 보낼 수 있습니다.</h2><p>어려운 논문을 작성하다 보면 공식을 잘못 이해한 건지, 오타가 있는 건지 몰라 몇 시간 동안 고민할 때가 있습니다. 오픈 소스 프로젝트인 경우 GitHub 또는 StackOverflow에서 질문할 수 있습니다. 그렇다면 학술 논문은 어디에서 구할 수 있을까요? 작성자는 바빠 보이고 이메일에 짜증이 날 수 있습니다.</p><p>오히려 많은 학자들이 자신의 아이디어가 실용화되고 있다는 소식을 듣고 기뻐하며 이메일을 통해 기꺼이 질문에 답변해 줍니다. 그들이 잘 알고 있는 제품이라면 웹사이트에 애플리케이션을 등록할 수도 있습니다!</p><p>또한 학계에서 소프트웨어 개발과 동일한 많은 도구를 사용하여 공개적으로 논문을 토론하는 경향이 증가하고 있습니다. 문서에 소프트웨어 패키지가 함께 제공되는 경우 <a href="https://github.com/facebookresearch/faiss/issues/1928">Github에서 일반적인 질문에</a> 대한 답변을 찾을 수 있습니다. "이론적 컴퓨터 과학" 및 "교차 검증"과 같은 Stack Exchange 커뮤니티에는 <a href="https://cstheory.stackexchange.com/questions/49296/problem-in-the-paper-stable-minimum-space-partitioning-in-linear-time">인기 있는 논문에 대한 자세한 토론도</a> 포함되어 있습니다. 일부 학회에서는 모든 논문 리뷰를 온라인으로 게시하기 시작했습니다. 이 리뷰에는 접근 방식에 대한 유용한 인사이트를 얻을 수 있는 저자와의 <a href="https://openreview.net/forum?id=H1eA7AEtvS">주고받는 토론이</a> 포함되어 있습니다.</p><h2>계속하기</h2><p>이 게시물에서는 학술 논문 선택의 기본 사항에 중점을 두고 다음과 같이 설명합니다. </p><p>를 올바르게 구현하는 방법을 설명하지만 실제로 알고리즘을 배포하는 모든 측면을 다루지는 않습니다. 예를 들어 알고리즘이 복잡한 시스템에서 하나의 구성 요소에 불과한 경우 구성 요소의 변경이 엔드투엔드 개선으로 이어지도록 하려면 어떻게 해야 할까요? 알고리즘을 통합하는 과정에서 원본 논문에서 다루지 않은 상당한 수정이나 확장이 필요하다면 어떻게 해야 할까요? 이러한 주제는 향후 포스팅에서 더 많이 공유하고자 하는 중요한 주제입니다.</p>]]></content:encoded>
    <link>https://www.elastic.co/search-labs/blog/implementing-academic-papers-lessons-learned-from-elasticsearch-and-lucene</link>
    <guid isPermaLink="true">https://www.elastic.co/search-labs/blog/implementing-academic-papers-lessons-learned-from-elasticsearch-and-lucene</guid>
    <category><![CDATA[ML 연구]]></category>
    <category><![CDATA[Lucene]]></category>
    <dc:creator><![CDATA[Julie Tibshirani]]></dc:creator>
    <enclosure url="https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltbd7e961f594005e7/6a17d80f033c8d981f6bb009/d240bfef29e9d432069059b312dd044eb76eec6c-1440x840.png" length="0" type="image/png"/>
    <pubDate>Wed, 29 Sep 2021 00:00:00 GMT</pubDate>
  </item>
  </channel>
</rss>