AWS 기술 블로그

Amazon Aurora PostgreSQL에서 pgvector 0.8.0을 통한 벡터 검색 성능 및 관련성 향상

이 글은 AWS Database Blog에 게시된 Supercharging vector search performance and relevance with pgvector 0.8.0 on Amazon Aurora PostgreSQL by Shayon Sanyal을 한국어 번역 및 편집하였습니다.

효율적인 벡터 유사성 검색은 시맨틱 검색, 추천 시스템, 그리고 검색 증강 생성(RAG) 구현에 있어 핵심 구성 요소가 되었습니다.
Amazon Aurora PostgreSQL-Compatible Edition은 이제 pgvector 0.8.0을 지원하여 벡터 검색 기능에 상당한 개선사항을 제공하며, 시맨틱 검색과 RAG가 필요한 PostgreSQL 기반 AI 애플리케이션에 대해 Aurora를 더욱 매력적인 선택지로 만들고 있습니다.

이 포스트에서는 Aurora PostgreSQL-Compatible의 pgvector 0.8.0이 어떻게 최대 9배 빠른 쿼리 처리와 100배 더 정확한 검색 결과를 제공하는지 살펴보며, 대규모 벡터 검색을 구현할 때 엔터프라이즈 AI 애플리케이션이 직면하는 주요 확장성 문제를 해결하는 방법을 탐구합니다.

pgvector 0.8.0 개선사항

벡터 데이터베이스가 중요한 인프라 구성 요소로 등장했지만, 효과적인 벡터 검색은 시맨틱 애플리케이션을 구동하는 핵심 기능입니다. 조직이 수백만 또는 수십억 개의 벡터를 처리하도록 AI 애플리케이션을 확장함에 따라, 기존 벡터 검색 구현의 한계가 드러나고 있습니다. pgvector 0.8.0은 이러한 프로덕션 문제를 직접적으로 해결하는 여러 중요한 개선사항을 도입했으며, 특히 대규모 데이터셋에 대한 필터링된 쿼리 작업 시 더욱 효과적입니다.

  • 성능 개선 – pgvector 0.8.0은 버전 0.7.4와 비교하여 특정 쿼리 패턴에 대해 최대 5.7배의 쿼리 성능 향상을 제공합니다. 이러한 개선사항은 이 포스트의 후반부에서 더 자세히 살펴보겠습니다.
  • 완전한 결과 세트 – 0.8.0의 새로운 iterative_scan 기능은 근사 최근접 이웃(ANN) 인덱스 검색이 필요한 필터 쿼리에 대해 향상된 재현율을 제공하며, 이는 불완전한 결과를 반환할 수 있었던 이전 버전에 비해 중요한 개선사항입니다.
  • 향상된 쿼리 계획 – 0.8.0의 개선된 비용 추정은 복잡한 필터링된 검색에 대해 B-tree와 같은 전통적인 인덱스를 선택하는 등 더 효율적인 실행 경로로 이어집니다.
  • 유연한 성능 튜닝 – relaxed_order와 strict_order라는 두 가지 모드의 iterative_scan 도입은 성능 대비 조정 가능한 정확도를 제공합니다.

오버필터링의 문제점

이번 릴리스의 중요성을 이해하려면, 많은 개발자들이 프로덕션 환경으로 전환할 때 직면하는 벡터 검색의 근본적인 문제를 이해하는 것이 중요합니다. pgvector의 이전 버전에서는 벡터 유사성 검색을 전통적인 SQL 필터와 결합할 때, 벡터 인덱스 스캔이 완료된 후에 필터링이 발생했습니다. 이러한 접근 방식은 오버필터링이라는 문제를 야기했으며, 쿼리가 예상보다 적은 결과를 반환하거나 심지어 전혀 결과를 반환하지 않을 수도 있었습니다. 또한 시스템이 많은 벡터를 검색한 후 필터링 과정에서 대부분을 버리게 되어 성능과 확장성 문제를 야기했습니다.

다음 시나리오를 살펴보겠습니다: 수백만 개의 제품 임베딩을 가진 전자상거래 서비스가 있습니다. “여름 드레스”를 검색하면서 “여성 의류”와 “미디엄 사이즈” 필터를 적용할 때, pgvector의 이전 버전들은 다음과 같은 단계를 따랐습니다:

  1. 벡터 인덱스를 스캔하여 “여름 드레스”의 최근접 이웃을 찾습니다.
  2. 해당 이웃들에 category = “여성 의류”와 size = “미디엄”과 같은 SQL 필터를 적용합니다.
  3. 남은 결과를 반환하는데, 특히 필터가 데이터의 작은 부분만 일치하는 경우 너무 적거나 심지어 빈 결과가 될 수 있습니다.

HNSW(Hierarchical Navigable Small World)는 벡터 유사성 검색을 가속화하는 pgvector의 인덱싱 알고리즘입니다. 이는 벡터들이 최근접 이웃들과 연결된 다층 그래프 구조를 생성하여 벡터 공간을 통한 효율적인 탐색을 가능하게 합니다. 기본 검색 설정(hnsw.ef_search = 40)을 사용하는 HNSW 인덱스에서, 데이터의 10%만이 필터와 일치한다면, 실제로 저장된 관련 벡터가 얼마나 많든 상관없이 대략 4개의 사용 가능한 결과만 얻게 됩니다.

반복적 인덱스 스캔

pgvector 0.8.0반복적 인덱스 스캔을 도입하여 필터링된 벡터 검색에서 쿼리 신뢰성과 성능을 크게 향상시킵니다. 이 과정은 다음과 같이 작동합니다:

  1. 벡터 인덱스를 스캔합니다.
  2. 필터(예: 메타데이터 조건)를 적용합니다.
  3. 벡터 유사성과 필터 기준을 모두 만족하는 충분한 결과가 있는지 확인합니다.
  4. 그렇지 않으면, 필요한 수의 일치 항목을 찾거나 구성 가능한 제한에 도달할 때까지 점진적으로 스캔을 계속합니다.

이 접근 방식은 과도하게 엄격한 필터로 인한 조기 중단(이전 버전의 일반적인 문제)을 방지하여 거짓 음성(false negative)을 줄이고, 전체 재스캔을 피하거나 너무 적은 결과를 반환하지 않음으로써 성능을 향상시킵니다. 이는 복잡한 필터링 요구사항을 가진 프로덕션급 벡터 검색 애플리케이션에 특히 유용합니다. Aurora PostgreSQL-Compatible에서 이 새로운 기능의 강력함을 보여주는 실용적인 예제를 통해 실제 작동 모습을 살펴보겠습니다. 먼저 샘플 제품 데이터로 테이블을 생성해보겠습니다:

CREATE TABLE products (
    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title TEXT,
    description TEXT,
    category TEXT,
    embedding VECTOR(384)
);

-- HNSW를 사용하여 임베딩 컬럼에 인덱스 생성
CREATE INDEX products_embedding_idx ON products USING hnsw (embedding vector_cosine_ops);

-- 효율적인 필터링을 위해 카테고리 컬럼에도 인덱스 생성
CREATE INDEX ON products (category);

이제 이 테이블에 다양한 카테고리의 수천만 개 제품 임베딩을 채웠다고 가정해보겠습니다. 사용자가 “편안한 하이킹 부츠”와 유사한 제품을 검색하지만 아웃도어 장비 카테고리의 아이템만 원할 때, 다음과 같은 쿼리를 실행할 것입니다:

SELECT 
    product_name, 
    category, 
    embedding <=> '[vector for "comfortable hiking boots"]' AS distance
FROM products 
WHERE category = 'outdoor gear'
ORDER BY distance
LIMIT 20;

pgvector 0.8.0 이전

이전 버전에서는 1,000만 개의 제품이 있지만 재고가 있는 아웃도어 장비 카테고리 제품이 50,000개(0.5%)에 불과한 경우, 기본 HNSW 스캔은 많은 관련 제품을 놓치고 몇 개의 결과만 반환할 가능성이 높았습니다. 해결 방법들은 최적이 아니었습니다:

  • 더 많은 벡터를 스캔하기 위해 hnsw.ef_search를 증가시키기 (성능 저하)
  • 각 카테고리별로 별도의 인덱스 생성 (유지 관리 복잡)
  • 애플리케이션 레벨 페이징 구현 (복잡성 증가)

Aurora PostgreSQL에서 pgvector 0.8.0 사용하기

반복적 스캔을 활성화하고 차이점을 확인해보겠습니다:

-- 반복적 스캔 활성화
SET hnsw.iterative_scan = 'relaxed_order';

-- 동일한 쿼리 실행
SELECT 
    product_name, 
    category, 
    embedding <=> '[vector for "comfortable hiking boots"]' AS distance
FROM products 
WHERE category = 'outdoor gear'
ORDER BY distance
LIMIT 20;

이제 pgvector는 쿼리를 만족할 만큼 충분한 결과를 찾을 때까지 자동으로 인덱스 스캔을 계속하여, 성능을 유지하면서 사용자가 완전하고 관련성 높은 결과 세트를 볼 수 있도록 보장합니다. “충분한 결과”의 기준값은 설정할 수 있습니다: 시스템이 중단하기 전에 스캔할 튜플 수를 제어할 수 있습니다. HNSW 인덱스의 경우, 이는 기본값이 20,000인 hnsw.max_scan_tuples 매개변수에 의해 관리됩니다. 데이터셋과 성능 목표에 따라 이를 조정할 수 있습니다: SET hnsw.max_scan_tuples = 20000; 이는 필터링된 벡터 검색 중 재현율 (실제로 발견되는 관련 결과의 백분율)과 성능 간의 트레이드오프에 대한 세밀한 제어를 제공합니다. 참고: relaxed_order를 사용할 때는 적절한 순서를 보장하기 위해 나중에 결과를 재정렬해야 할 수 있습니다. 예를 들어:

SELECT * FROM (
  -- 원본 쿼리
) p ORDER BY p.distance * 1;

이는 최종 재정렬 작업을 강제합니다.

반복적 스캔을 위한 구성 옵션

pgvector 0.8.0은 반복적 스캔을 위한 세 가지 모드를 제공합니다:

  • off – 전통적인 동작, 반복적 스캔 없음 (기본값)
  • strict_order – 정확한 거리 순서를 유지하면서 반복적으로 스캔
  • relaxed_order – 근사 순서로 반복적 스캔 (더 나은 성능)

대부분의 프로덕션 사용 사례에서 relaxed_order는 성능과 정확도의 최적 균형을 제공합니다. 이는 relaxed_order가 pgvector로 하여금 결과를 완벽하게 정렬하기보다는 발견되는 대로 반환함으로써 속도를 우선시할 수 있게 하기 때문입니다. 엄격한 순서와 비교하여 일반적으로 결과 품질의 95-99%를 유지하면서 쿼리 지연 시간을 크게 줄입니다. 완벽한 순위보다 1초 미만의 응답 시간이 더 중요한 실제 애플리케이션(추천 시스템 및 시맨틱 검색 등)에서, 이러한 트레이드오프는 사용자 경험에 최소한의 실질적 영향을 미치면서 상당한 성능 향상을 제공합니다. hnsw.max_scan_tuples 매개변수 외에도, 재현율을 향상시키기 위해 hnsw.scan_mem_multiplier 매개변수를 구성할 수 있습니다. 이 매개변수는 work_mem의 배수로 사용할 최대 메모리 양을 지정합니다(기본값 1).

Aurora PostgreSQL-Compatible에서 RAG 애플리케이션 확장

이러한 개선사항이 실제 RAG 애플리케이션에 미치는 영향을 살펴보겠습니다: 제품 설명에서 생성된 384차원 벡터 임베딩으로 각각 표현되는 1,000만 개의 제품을 보유한 온라인 마켓플레이스를 상상해봅니다. 고객들은 전체 카탈로그를 검색하거나 카테고리, 가격 범위, 또는 평점으로 필터링할 수 있습니다. pgvector의 이전 버전에서는 각 쿼리 패턴에 대해 매개변수를 신중하게 조정하지 않으면 필터링된 검색이 관련 제품을 놓칠 수 있었습니다. Aurora PostgreSQL-Compatible의 pgvector 0.8.0을 사용하면, 데이터베이스가 자동으로 조정되어 완전한 결과를 생성합니다. pgvector 0.8.0 개선사항의 실제 영향을 보여주기 위해, 우리는 프로덕션 규모의 현실적인 전자상거래 워크로드를 사용하여 Aurora PostgreSQL-Compatible에서 pgvector 0.7.4와 0.8.0 모두에 대한 광범위한 벤치마킹을 수행했습니다. 우리의 테스트는 기업들이 대규모 제품 검색 시스템을 배포할 때 직면하는 시나리오에 초점을 맞췄습니다.

벤치마크 설정

우리는 여러 카테고리에 걸쳐 현실적인 전자상거래 특성을 가진 1,000만 개 제품의 합성 데이터셋을 생성했습니다. 이러한 벤치마크를 재현 가능하게 만들기 위해, 데이터셋을 생성한 방법은 다음과 같습니다:

데이터 생성 과정

  1. 제품 메타데이터 생성: faker와 numpy 같은 라이브러리를 사용한 Python 스크립트로 현실적인 제품 메타데이터를 생성했습니다:
import pandas as pd
import numpy as np
from faker import Faker
from sentence_transformers import SentenceTransformer
import random
# 현실적인 텍스트 생성을 위한 faker 초기화
fake = Faker()
# 문장 변환기 모델 초기화
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
# 현실적인 분포를 가진 제품 카테고리 정의
categories = {
    'electronics': 0.20,  # 제품의 20%
    'clothing': 0.25,
    'home_goods': 0.15,
    'beauty': 0.10,
    'books': 0.05,
    'toys': 0.05,
    'sports': 0.05,
    'grocery': 0.10,
    'office': 0.05
}
# 합성 제품 데이터 생성
num_products = 10_000_000  # 1,000만 개 제품
products = []
for i in range(num_products):
    # 분포에 기반하여 카테고리 선택
    category = np.random.choice(
        list(categories.keys()), 
        p=list(categories.values())
    )
    
    # 카테고리 컨텍스트와 함께 제목 생성
    title = fake.catch_phrase()
    
    # 더 자세한 설명
    description = fake.paragraph(nb_sentences=3)
    
    # Query D 테스트를 위한 "스마트" 제품 추가
    if i % 50 == 0:  # 제품의 2%가 제목에 "smart"를 포함
        title = "Smart " + title
        
    products.append({
        'id': i+1,
        'title': title,
        'description': description,
        'category': category,
        # 임베딩은 다음 단계에서 추가됨
    })
    
    # 진행 상황 출력
    if i % 100000 == 0:
        print(f"Generated {i} products")
# DataFrame으로 변환
df = pd.DataFrame(products)
  1. 임베딩 생성: all-MiniLM-L6-v2 SentenceTransformer 모델을 사용하여 384차원 임베딩을 생성했습니다:
# 메모리 관리를 위해 배치로 임베딩 생성
batch_size = 1000
for i in range(0, len(df), batch_size):
    end = min(i + batch_size, len(df))
    batch = df.iloc[i:end]
    
    # 제목 + 설명으로부터 임베딩 생성
    texts = [f"{row.title}. {row.description}" for _, row in batch.iterrows()]
    embeddings = model.encode(texts)
    
    # 임베딩 저장
    for j, embedding in enumerate(embeddings):
        df.at[i+j, 'embedding'] = embedding.tolist()
    
    print(f"Generated embeddings for products {i} to {end}")
  1. PostgreSQL로 데이터 로딩: 효율적인 데이터 로딩을 위해 PostgreSQL의 COPY 명령을 사용했습니다:
# CSV 파일로 데이터 내보내기 (더 빠른 내보내기를 위해 임베딩 컬럼 제외)
csv_file = "product_metadata.csv"
df[['id', 'title', 'description', 'category']].to_csv(csv_file, index=False)
# 효율적인 로딩을 위해 임베딩을 별도의 바이너리 파일로 내보내기
embedding_file = "product_embeddings.binary"
with open(embedding_file, 'wb') as f:
    for _, row in df.iterrows():
        embedding = np.array(row['embedding'], dtype=np.float32)
        f.write(embedding.tobytes())
-- 데이터 로딩을 위한 SQL
CREATE TABLE products (
    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title TEXT,
    description TEXT,
    category TEXT,
    embedding vector(384)
);
-- COPY를 사용하여 메타데이터 로딩
COPY products (id, title, description, category)
FROM '/path/to/product_metadata.csv' 
CSV HEADER;
-- 바이너리 데이터를 읽는 사용자 정의 함수를 사용하여 임베딩 로딩
SELECT load_embeddings('/path/to/product_embeddings.binary', 'products', 'embedding', 384);

우리가 생성한 데이터셋은 다음과 같은 특성을 가졌습니다:

  • 현실적인 분포를 가진 9개 카테고리에 걸친 1,000만 개 제품
  • 제품 제목과 설명으로부터 생성된 384차원 임베딩
  • 필터링된 쿼리 테스트를 위해 제목에 “smart”가 포함된 제품 2%
  • 다양성과 현실적인 콘텐츠를 보장하기 위해 Faker를 사용하여 생성된 자연어 텍스트

또한 벡터 유사성 검색에서 일반적으로 사용되는 필터 작업을 최적화하기 위해 카테고리 컬럼에 B-tree 인덱스가 포함되었습니다. 이 데이터셋은 조직이 포괄적인 제품 검색 시스템을 위해 구축하는 것을 반영합니다. 이 설정은 위의 코드 스니펫을 사용하여 재현할 수 있으며, 테스트 환경에 필요에 따라 규모를 조정할 수 있습니다. 이러한 테스트를 위해 우리는 제품 카탈로그 스키마를 사용했습니다:

CREATE TABLE products (
    id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    title TEXT,
    description TEXT,
    category TEXT,
    embedding vector(384)
);
-- 벡터 유사성 검색을 위한 HNSW 인덱스 생성
CREATE INDEX products_embedding_idx ON products 
    USING hnsw (embedding vector_cosine_ops);
-- 효율적인 필터링을 위한 카테고리 인덱스 생성
CREATE INDEX ON products (category);

우리는 다음과 같은 샘플 쿼리들을 실행했습니다:

  • 쿼리 A – 기본 검색 (상위 10개):
SELECT 
    id, 
    title, 
    description, 
    embedding <=> %s::vector AS distance
FROM products
ORDER BY distance
LIMIT 10;
  • 쿼리 B – 대용량 결과 세트 (상위 1,000개):
SELECT 
    id, 
    title, 
    description, 
    embedding <=> %s::vector AS distance
FROM products
ORDER BY distance
LIMIT 1000;
  • 쿼리 C – 카테고리 필터링 검색:
SELECT 
    id, 
    title, 
    description, 
    embedding <=> %s::vector AS distance
FROM products
WHERE category = 'category1'
ORDER BY distance
LIMIT 10;
  • 쿼리 D – 복합 필터링 검색:
SELECT 
    id, 
    title, 
    description, 
    category, 
    embedding <=> %s::vector AS distance
FROM products
WHERE category IN ('category1', 'category2', 'category3')
AND title ILIKE '%%smart%%'  -- 'smart'가 포함된 제목 필터링
ORDER BY distance
LIMIT 100;
  • 쿼리 E – 매우 대용량 결과 세트 (10,000개):
SELECT 
    id, 
    title, 
    description, 
    category, embedding <=> %s::vector AS distance
FROM products
ORDER BY distance
LIMIT 10000;

테스트 방법론

벤치마크는 일관된 측정을 제공하면서 실제 벡터 검색 시나리오를 복제하도록 설계되었습니다:

  • 인프라 – (AWS Graviton4 프로세서로 구동되는) db.r8g.4xlarge 인스턴스에서 실행되는 두 개의 별도 Aurora PostgreSQL 클러스터
  • 데이터셋 – 384차원 임베딩을 가진 1,000만 개 제품
  • 인덱스 구성 – 공정한 비교를 위해 테스트 전반에 걸쳐 동일한 매개변수를 가진 HNSW 인덱스
  • 캐시 관리 – 일관된 콜드 스타트 성능을 제공하기 위해 테스트 간 버퍼 캐시 정리
  • 쿼리 실행 – 쿼리 A, B, C는 각각 100회씩 실행되었으며, 더 집약적인 쿼리 D와 E는 각각 20회와 5회 실행되었고, 보고된 지연 시간 값은 통계적 유의성을 제공하고 이상값의 영향을 최소화하기 위해 실행 횟수 전반의 평균을 나타냅니다
  • 테스트 구성 – 우리는 다음과 같은 구성을 사용했습니다:
    • 0.7.4 기준선: ef_search=40
    • 0.7.4: ef_search=200
    • 0.8.0 기준선: ef_search=40, iterative_scan=off
    • 0.8.0: ef_search=40, iterative_scan=strict_order
    • 0.8.0: ef_search=40, iterative_scan=relaxed_order
    • 0.8.0: ef_search=200, iterative_scan=strict_order
    • 0.8.0: ef_search=200, iterative_scan=relaxed_order

성능 개선

우리의 성능 테스트는 다양한 쿼리 패턴에 걸쳐 pgvector 0.8.0의 상당한 개선사항을 보여주었습니다. 다음 표는 서로 다른 구성에 대한 p99 지연 시간 측정값(밀리초 단위)을 보여줍니다.

쿼리
유형
0.7.4 기준선
(ef_search=40)
0.7.4
(ef_search=200)
0.8.0
최적 구성
최적 구성 개선 사항
A 123.3 ms 394.1 ms 13.1 ms ef_search=40, relaxed_order 9.4배
빠름
B 104.2 ms 341.4 ms 83.5 ms ef_search=200, relaxed_order 1.25배
빠름
C 128.5 ms 333.4 ms 85.7 ms ef_search=200, relaxed_order 1.5배
빠름
D 127.4 ms 318.6 ms 70.7 ms ef_search=200, relaxed_order 1.8배
빠름
E 913.4 ms 427.4 ms 160.3 ms ef_search=200, relaxed_order 5.7배
빠름

pgvector 0.8.0의 성능 개선은 1,000만 개 제품 규모에서도 다양한 쿼리 패턴에 걸쳐 상당했습니다. 특정 기준에 맞는 제품을 특정 카테고리 내에서 검색하는 일반적인 전자상거래 쿼리의 경우, 실행 시간이 pgvector 0.7.4의 120밀리초 이상에서 0.8.0의 단 70밀리초로 감소하면서 더 포괄적인 결과를 반환했습니다. 특히 인상적인 것은 pgvector 0.8.0의 개선된 비용 추정 기능이 자동으로 더 효율적인 실행 계획을 선택한 방법입니다. 우리의 필터링된 쿼리 테스트에서, 플래너는 비용을 올바르게 추정하고 벡터 연산 복잡성에 대한 더 현실적인 평가를 제공했습니다. 아래 “향상된 비용 추정 및 쿼리 계획” 섹션에서 보여주듯이, pgvector 0.8.0의 비용 추정(7,224.63 비용 단위)은 버전 0.7.4(116.84 비용 단위)와 비교하여 벡터 연산의 실제 계산 요구사항을 더 정확하게 반영하여, 더 나은 실행 계획 선택과 더 완전한 결과 세트로 이어졌습니다.

재현율 및 결과 완전성 개선

수백만 개의 벡터로 작업할 때 원시 성능보다 더 중요한 것은 결과 품질의 상당한 개선입니다. 우리의 테스트는 결과 완전성에서 상당한 차이를 보여주었습니다. 재현율은 Y개의 예상 결과 중 X개를 반환한다는 의미이며, 100%가 완벽한 재현율임을 기억합니다:

쿼리 0.7.4 기준선 (ef_search=40) 0.7.4 (ef_search=200) 0.8.0 with strict_order 0.8.0 with relaxed_order
카테고리 필터링 검색 10% 0% 100% 100%
복합 필터링 검색 1% 0% 100% 100%
매우 대용량 결과 세트 5% 5% 100% 100%

높은 선택성을 가진 쿼리(특정 카테고리의 제품)의 경우, pgvector 0.7.4는 요청된 결과의 일부만 반환했습니다. 0.8.0에서 반복적 스캔이 활성화되면서, 우리는 결과 완전성에서 최대 100배의 개선을 확인했으며, 이는 사용자 경험을 상당히 향상시켰습니다. 다음은 이러한 개선사항을 확인하기 위해 테스트한 쿼리 패턴입니다.

-- 반복적 스캔 활성화 (pgvector 0.8.0 전용)
SET hnsw.iterative_scan = 'relaxed_order';

-- 다중 필터를 가진 전자상거래 쿼리
SELECT id, title, category, embedding <=> '[query vector]'::vector AS distance
FROM products
WHERE category = 'category1'
ORDER BY distance
LIMIT 100;

다양한 반복적 스캔 모드와 ef_search 값

우리는 서로 다른 반복적 스캔 모드와 ef_search 값 간의 트레이드오프를 이해하기 위해 다양한 pgvector 0.8.0 구성에 대한 상세한 비교를 수행했습니다.

구성 쿼리 A
(상위
10개)
쿼리 B
(상위
1000개)
쿼리 C
(필터링)
쿼리 D
(복합)
쿼리 E
(대용량)
0.8.0 기준선 (ef_search=40, iterative_scan=off) 19.3 ms 18.8 ms 20.0 ms 15.7 ms 99.8 ms
0.8.0 (ef_search=40, iterative_scan=strict_order) 18.1 ms 277.9 ms 197.1 ms 203.2 ms 344.0 ms
0.8.0 (ef_search=40, iterative_scan=relaxed_order) 13.1 ms 164.1 ms 150.8 ms 99.1 ms 397.9 ms
0.8.0 (ef_search=200, iterative_scan=strict_order) 28.8 ms 133.7 ms 128.5 ms 57.9 ms 207.6 ms
0.8.0 (ef_search=200, iterative_scan=relaxed_order) 30.7 ms 83.5 ms 85.7 ms 70.7 ms 160.3 ms

이 상세한 분석은 다양한 조합이 쿼리 유형별 성능에 어떻게 영향을 미치는지 보여줍니다. 단순한 쿼리(A)의 경우, relaxed_order와 함께 낮은 ef_search가 최고의 성능을 제공합니다. 복잡한 필터링된 쿼리(C, D)와 대용량 결과 세트(B, E)의 경우, relaxed_order와 함께 높은 ef_search 값이 일반적으로 성능과 완전성의 최적 균형을 제공합니다. relaxed_order 모드는 여전히 완전한 결과 세트를 제공하면서 대부분의 쿼리 유형에 대해 상당히 더 나은 성능을 제공합니다. 정확한 거리 순서가 덜 중요한 애플리케이션(제품 추천 등)의 경우, 이 모드는 성능과 결과 품질의 우수한 균형을 제공합니다.

향상된 비용 추정 및 쿼리 계획

PostgreSQL의 비용 추정은 데이터베이스가 쿼리를 실행하는 데 필요한 계산 리소스(주로 CPU 시간과 메모리)를 예측하는 방법을 의미합니다. 쿼리 플래너는 이러한 비용 추정을 사용하여 가장 효율적인 실행 경로를 결정합니다. pgvector 0.8.0의 쿼리 계획은 비용 추정 정확도와 계획 결정에서 상당한 개선을 보여줍니다. 이러한 개선사항은 PostgreSQL이 벡터 인덱스와 순차 스캔 중 언제 사용할지에 대해 더 스마트한 선택을 할 수 있게 하여, 특히 벡터 유사성을 전통적인 필터와 결합하는 복잡한 쿼리에서 더 빠른 쿼리 실행을 가능하게 합니다. 이를 설명하기 위해, 두 버전의 필터링된 쿼리(쿼리 C)에 대한 EXPLAIN 출력을 살펴보겠습니다. 다음 코드는 pgvector 0.7.4 쿼리 계획(카테고리 필터)입니다:

Limit  (cost=116.84..217.48 rows=10 width=90) (actual time=3.919..4.088 rows=6 loops=1)
  ->  Index Scan using products_embedding_idx on products  (cost=116.84..10127632.67 rows=987333 width=90) (actual time=3.919..4.087 rows=6 loops=1)
        Order By: (embedding <=> '[vector]'::vector)
        Filter: (category = 'category1'::text)
        Rows Removed by Filter: 494
Planning Time: 0.170 ms
Execution Time: 4.105 ms

다음 코드는 iterative_scan=relaxed_order를 사용한 pgvector 0.8.0 쿼리 계획입니다:

Limit  (cost=7224.63..7423.21 rows=10 width=90) (actual time=5.554..7.506 rows=10 loops=1)
  ->  Index Scan using products_embedding_idx on products  (cost=7224.63..20202546.50 rows=1017000 width=90) (actual time=5.554..7.504 rows=10 loops=1)
        Order By: (embedding <=> '[vector]'::vector)
        Filter: (category = 'category1'::text)
        Rows Removed by Filter: 570
Planning Time: 0.177 ms
Execution Time: 7.524 ms

이러한 쿼리 계획은 0.8.0에서 여러 주요 개선사항을 보여줍니다:

참고: PostgreSQL 비용 단위는 추정된 CPU 및 I/O 워크로드를 나타내는 임의의 내부 측정값입니다. 이들은 밀리초나 다른 표준 단위로 직접 변환되지 않지만, 더 높은 값은 플래너가 더 리소스 집약적인 작업을 예상한다는 것을 나타냅니다.

  • 더 현실적인 시작 비용 – 0.8.0 플래너는 0.7.4의 116.84 비용 단위에 비해 7,224.63 비용 단위의 시작 비용을 추정하며, 이는 벡터 연산의 실제 계산 복잡성을 훨씬 더 잘 반영합니다
  • 더 나은 행 추정 – 0.8.0 플래너는 0.7.4의 987,333개에 비해 1,017,000개의 필터링된 행을 추정하여, 필터의 선택성에 대한 더 정확한 평가를 보여줍니다
  • 완전한 결과 – 가장 중요한 것은, 0.8.0이 요청된 10개 행을 반환하는 반면 0.7.4는 6개만 찾았다는 것입니다
  • 인덱스의 효율적 사용 – 카테고리 인덱스의 추가로 두 버전 모두 결과를 효율적으로 필터링할 수 있지만, 0.8.0은 반복적 스캔으로 인해 인덱스 순회에서 더 철저합니다

복잡한 필터(쿼리 D)의 경우, 차이점이 더욱 뚜렷합니다. 다음 코드는 pgvector 0.7.4 쿼리 계획(복잡한 필터)입니다:

Limit  (cost=116.84..455.46 rows=100 width=100) (actual time=4.130..4.669 rows=39 loops=1)
  ->  Index Scan using products_embedding_idx on products  (cost=116.84..10170125.25 rows=2993034 width=100) (actual time=4.129..4.665 rows=39 loops=1)
        Order By: (embedding <=> '[vector]'::vector)
        Filter: ((title ~~* '%smart%'::text) AND (category = ANY ('{category1,category2,category3}'::text[])))
        Rows Removed by Filter: 461
Planning Time: 1.378 ms
Execution Time: 4.692 ms

다음 코드는 iterative_scan=relaxed_order를 사용한 pgvector 0.8.0 쿼리 계획입니다:

Limit  (cost=7224.63..7884.49 rows=100 width=100) (actual time=2.909..3.060 rows=100 loops=1)
  ->  Index Scan using products_embedding_idx on products  (cost=7224.63..20245171.57 rows=3067027 width=100) (actual time=2.909..3.053 rows=100 loops=1)
        Order By: (embedding <=> '[vector]'::vector)
        Filter: ((title ~~* '%smart%'::text) AND (category = ANY ('{category1,category2,category3}'::text[])))
        Rows Removed by Filter: 1
Planning Time: 1.508 ms
Execution Time: 3.083 ms

여기서 주요 차이점은 0.7.4가 (100개를 요청했음에도 불구하고) 39개 행만 찾은 후 중단하는 반면, 반복적 스캔을 사용하는 0.8.0 플래너는 요청된 100개 행을 찾을 때까지 검색을 계속하며, 심지어 더 나은 런타임을 제공한다는 것입니다. 이러한 예제들은 pgvector 0.8.0의 개선된 비용 추정이 어떻게 더 나은 실행 전략으로 이어지는지, 특히 벡터 검색을 전통적인 데이터베이스 필터와 결합할 때 보여줍니다. 더 정확한 비용 모델은 PostgreSQL 옵티마이저가 실행 경로에 대해 더 스마트한 결정을 내리는 데 도움이 되어, 더 나은 성능과 완전한 결과 세트 모두를 제공합니다.

프로덕션 워크로드로의 확장

Amazon Aurora I/O-Optimized 클러스터 구성은 전자상거래 서비스, 결제 처리 시스템, 추천 시스템, RAG 애플리케이션을 포함한 I/O 집약적 워크로드에 대해 향상된 가격 대비 성능과 예측 가능한 가격을 제공합니다. 이 구성은 향상된 버퍼 캐시 관리를 통한 Aurora Optimized Reads로 I/O 성능을 개선하여 쓰기 처리량을 증가시키고 지연 시간을 낮춥니다. 동적이거나 가변적인 워크로드의 경우, Amazon Aurora Serverless v2는 세밀한 증분 단위로 용량을 조정하는 프로덕션 준비된 자동 확장 옵션을 제공하여 성능이나 가용성을 희생하지 않으면서 빠른 시작과 탄력적 확장에 이상적입니다.

읽기 복제본을 통한 Aurora PostgreSQL-Compatible의 읽기 용량 확장 능력은 pgvector 0.8.0의 더 효율적인 쿼리 처리와 결합되어 엔터프라이즈 규모의 전자상거래 애플리케이션을 위한 견고한 기반을 제공합니다. 이제 기업들은 제품 카탈로그가 수백만 또는 수십억 개의 벡터로 성장하더라도 높은 성능과 결과 품질을 유지하는 시맨틱 검색, 추천 시스템, RAG 애플리케이션을 자신 있게 구축할 수 있습니다.

시맨틱 검색 시스템

시맨틱 검색 사용 사례에는 제품 검색, 문서 검색, 콘텐츠 추천이 포함될 수 있습니다. 0.8.0은 다음과 같은 방식으로 뛰어난 성능을 발휘합니다:

  • 눈에 띄는 속도 개선(기본 쿼리에 대해 최대 9.4배 빠름)으로 실시간 검색 경험을 가능하게 합니다
  • relaxed_order 모드는 결과 순서의 약간의 변화가 사용자에게 감지되지 않는 검색 인터페이스에 이상적입니다
  • 개선된 필터링된 쿼리(쿼리 C와 D)는 패싯 또는 카테고리 필터링된 검색 구현을 향상시킵니다
  • 완전한 결과 세트는 주요 결과를 종종 놓쳤던 0.7.4와 달리 사용자가 가장 관련성 높은 항목을 볼 수 있도록 보장합니다

구현 예시로는 사용자가 제품 속성별 필터링과 함께 1초 미만의 결과를 기대하는 전자상거래 제품 검색이 있을 수 있습니다.

대규모 추천 시스템

추천 사용 사례에는 콘텐츠 추천, “유사한 항목” 기능, 개인화가 포함될 수 있습니다. 0.8.0은 다음과 같은 이점을 제공합니다:

  • 대용량 결과 세트의 훨씬 빠른 검색(쿼리 B와 E)으로 시스템이 후처리를 위해 더 많은 후보를 가져올 수 있도록 합니다.
  • 낮은 지연 시간으로 트래픽이 많은 시스템에서 실시간 추천을 가능하게 합니다.
  • 필터링된 쿼리의 성능으로 상황별 추천(예: “이 카테고리의 유사한 제품”)을 지원합니다.
  • 더 나은 재현율로 추천의 다양성을 제공합니다.

구현 예시로는 수백만 개의 카탈로그에서 수천 개의 콘텐츠 항목을 실시간으로 추천해야 하는 미디어 스트리밍 서비스가 있을 수 있습니다.

RAG 애플리케이션

RAG 사용 사례에는 응답을 생성하기 전에 관련 컨텍스트를 검색하는 AI 시스템이 포함될 수 있습니다. 0.8.0은 다음과 같은 개선사항을 제공합니다:

  • 낮은 지연 시간으로 AI 시스템의 종단 간 응답 시간을 개선합니다
  • 필터링된 쿼리의 더 나은 성능으로 도메인별 검색을 가능하게 합니다
  • 완전한 결과 세트로 AI가 관련 컨텍스트에 액세스할 수 있도록 보장합니다
  • RAG는 일반적으로 정확한 순서가 중요하지 않은 top-k 검색을 사용하기 때문에 relaxed 순서가 이상적입니다

구현 예시로는 사용자 질문에 답하기 위해 회사 지식 베이스를 쿼리해야 하는 기업 AI 어시스턴트가 있을 수 있습니다.

Aurora PostgreSQL-Compatible에서 pgvector 0.8.0 시작하기

pgvector 0.8.0 사용을 시작하려면 다음 단계를 완료합니다:

  1. 버전 17.4, 16.8, 15.12, 14.17 또는 13.20 이상을 실행하는 새로운 Aurora PostgreSQL 클러스터를 시작합니다.
  2. DB 클러스터에 연결합니다.
  3. 데이터베이스에 연결한 후, 확장을 활성화합니다:

CREATE EXTENSION IF NOT EXISTS vector;

  1. pgvector의 최신 버전을 실행하고 있는지 확인합니다:
postgres=> SELECT extversion FROM pg_extension WHERE extname = 'vector';
 extversion 
------------
 0.8.0
(1 row)

Aurora PostgreSQL-Compatible에서 pgvector 0.8.0을 위한 모범 사례

프로덕션에서 pgvector 0.8.0을 배포할 때, 성능, 재현율, 그리고 필터링 정확도의 균형을 맞추기 위해 다음과 같은 모범 사례를 고려합니다:

  1. 벡터 인덱스가 필요하지 않다면 사용하지 않습니다 – 작은 데이터셋에서 100% 재현율과 좋은 성능을 위해서는 벡터 인덱스보다 순차 스캔이 더 적절할 수 있습니다. 대용량 데이터셋에 대한 성능 이점이 필요할 때만 벡터 인덱스를 사용합니다.

예를 들어, 10,000개의 제품 임베딩만 있는 테이블이 있다면, 순차 스캔이 실제로 벡터 인덱스를 사용하는 것보다 빠를 수 있습니다:

-- 작은 테이블의 경우, 단순한 순차 스캔이 종종 더 나은 성능을 보입니다
-- 특히 재현율이 중요한 경우 인덱스를 유지하는 것보다 낫습니다
-- 이 쿼리는 코사인 거리로 순차 스캔을 수행합니다
SELECT
    id,
    title,
    embedding <=> '[query vector]'::vector AS distance
FROM small_products_table
ORDER BY distance
LIMIT 20;
-- 이것과 인덱스를 사용한 동일한 쿼리의 EXPLAIN ANALYZE 출력을 비교합니다
-- 작은 테이블(10K-50K 벡터)의 경우, 순차 스캔이 최소한의 성능 영향으로
-- 더 나은 재현율을 제공한다는 것을 종종 발견할 것입니다

벡터 인덱스를 생성하는 것은 유지 관리와 저장을 위한 오버헤드를 추가하며, 이는 데이터셋이 순차 스캔이 현실적으로 불가능할 만큼 느려질 때 만큼 충분히 클 때만 가치가 있습니다.

  1. 인덱싱 권장사항
    • 높은 검색 품질과 효율적인 인덱스 구성을 보장하기 위해 권장 매개변수와 함께 HNSW를 사용합니다:
CREATE INDEX my_table_embedding_hnsw_idx
  ON my_table USING hnsw (embedding vector_cosine_ops)
  WITH (
      ef_construction = 128,
      m = 16
  );
  • 일반적으로 필터링되는 메타데이터 컬럼(예: category, status, org_id)에 추가 인덱스를 생성하여 벡터 후 필터링의 성능을 향상시킵니다:
CREATE INDEX my_table_category_idx ON my_table(category);
  1. 쿼리 시간 튜닝 (검색 매개변수)

사용 사례에 따라 재현율 또는 성능에 최적화하도록 이러한 매개변수를 조정합니다:

  • 필터링과 함께 최대 재현율을 위해 (예: 엄격한 규정 준수 또는 분석 사용 사례):
SET hnsw.iterative_scan = 'strict_order';  -- 완전성 보장
SET hnsw.ef_search = 200;                  -- 더 깊은 그래프 순회
  • 최고 성능을 위해 (예: 대화형 또는 지연 시간에 민감한 워크로드):
SET hnsw.iterative_scan = 'relaxed_order'; -- 결과를 더 빠르게 반환
SET hnsw.ef_search = 40;                   -- 더 빠른 순회
  • 균형 잡힌 시나리오를 위해 (예: 범용 검색):
SET hnsw.iterative_scan = 'relaxed_order'; -- 반복적 스캔 폴백 활성화
SET hnsw.ef_search = 200;                  -- 쿼리 복잡성에 따라 구성 가능

이러한 권장사항은 도메인에 구애받지 않으며 워크로드에 맞게 조정되어야 합니다. 일반적인 규칙으로:

  • 완전성이 중요할 때는 strict_order를 사용합니다.
  • 지연 시간이 재현율보다 중요할 때는 relaxed_order를 사용합니다.
  • 복잡한 필터링이나 더 큰 그래프에 대해서는 ef_search를 더 높게 조정합니다.

또한 다음과 같은 운영 모범 사례를 고려합니다:

  • Graviton4 기반 인스턴스 (R8g 시리즈) – 이러한 인스턴스들은 우수한 벡터 연산 성능을 보여줍니다. 개발과 테스트에는 r8g.large로 시작하고, 프로덕션 워크로드에는 r8g.2xlarge 또는 4xlarge로 확장합니다.
  • 메모리와 성능의 균형 – hnsw.ef_search의 높은 값은 더 정확한 결과를 제공하지만 더 많은 메모리를 소비합니다.
  • 필터 컬럼에 인덱스 생성 – WHERE 절에 사용되는 컬럼에 표준 PostgreSQL 인덱스를 생성합니다.
  • 모니터링 및 튜닝Amazon CloudWatch Database Insights를 사용하여 느린 벡터 쿼리를 식별하고 최적화합니다.
  • 매우 큰 테이블에 대한 파티셔닝 고려 – 수십억 개의 벡터의 경우, 테이블 파티셔닝이 쿼리 성능과 관리 용이성을 모두 향상시킬 수 있습니다.
  • 반복적 스캔을 적절히 구성 – relaxed_order로 시작하고 애플리케이션의 필요에 따라 임계값을 조정합니다.

결론

Aurora PostgreSQL-Compatible의 pgvector 0.8.0은 프로덕션 규모의 AI 애플리케이션을 구축하는 조직들에게 중요한 발전을 의미합니다. 반복적 인덱스 스캔의 도입은 벡터 검색에서 가장 어려운 문제 중 하나를 해결하며, 전반적인 성능 개선으로 Aurora PostgreSQL-Compatible를 벡터 저장 및 검색을 위한 더욱 매력적인 옵션으로 만들었습니다. 벡터 데이터가 수천 개에서 수백만 또는 수십억 개의 임베딩으로 증가함에 따라, 이러한 최적화는 애플리케이션이 반응성, 정확성, 그리고 비용 효율성을 유지하도록 보장합니다.

Amazon Aurora 리소스 또는 Amazon Aurora 사용자 가이드를 참조하여 애플리케이션에 pgvector 0.8.0을 통합하는 방법에 대해 자세히 알아봅니다.

Lee Youngdong

Lee Youngdong

이영동 클라우드 서포트 엔지니어는 데이터베이스와 데이터 엔지니어링 경험을 기반으로, Amazon Database 서비스에 대한 고객사의 기술 문의 및 이슈를 분석하여 데이터베이스가 안정적으로 운영될 수 있도록 노력하고 있습니다.

Sanghyun Ahn

Sanghyun Ahn

안상현은 AWS Cloud Support Engineering 소속 데이터베이스 전문 엔지니어로, Amazon Aurora와 Amazon RDS 서비스를 담당하고 있습니다. 고객의 기술적 문의 대응부터 장애 분석에 이르기까지 데이터베이스 운영 전반에 걸친 기술 지원을 제공하며, 고객이 AWS 데이터베이스 서비스를 안정적으로 활용할 수 있도록 돕고 있습니다.