본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 29. 13:39

Amazon Aurora의 pgvector를 활용한 사용자 간 리뷰 그래프 구축하기

요약

Amazon Aurora의 pgvector를 사용하여 서로 다른 텍스트로 작성된 리뷰 데이터를 동일한 항목으로 매칭하는 시스템 구축 방법을 설명합니다. 1024차원 임베딩과 HNSW 인덱스를 활용해 유사성 문제를 해결하고 데이터 모델을 설계하는 과정을 다룹니다.

핵심 포인트

  • pgvector를 활용한 코사인 유사도 기반의 항목 매칭 구현
  • HNSW 인덱스를 통한 효율적인 근사 최근접 이웃(ANN) 검색
  • user_items와 canonical_items 테이블을 통한 데이터 모델링
  • 비차단(non-blocking) 방식의 매칭 프로세스 설계

모든 리뷰 앱은 고립되어 있습니다. Yelp 리뷰는 장소를, Amazon 리뷰는 자체 카탈로그를, Letterboxd 리뷰는 영화를 다룹니다. 저는 그 반대의 것을 만들고 싶었습니다. 볼펜, 버거, 크루즈 등 무엇이든 기록하고 별점을 매길 수 있으며, 나의 리뷰가 다른 모든 사람의 리뷰와 하나로 합쳐질 수 있는 단 한 곳 말입니다.

어려운 점은 입력 양식이 아닙니다. 바로 이것입니다: 내가 "In-N-Out Double-Double"이라고 기록하고, 당신이 "in n out double double burger"라고 기록했을 때, 우리의 평점이 합산될 수 있도록 이 두 기록이 동일한 것이 되어야 한다는 점입니다. 공유된 제품 ID도, 바코드도, 철자에 대한 합의도 없습니다. 그저 두 명의 인간이 동일한 항목을 서로 다르게 설명하고 있을 뿐입니다.

이것은 유사성 문제(similarity problem)이며, 저는 이를 **Amazon Aurora PostgreSQL (Serverless v2) 내에서 실행되는 pgvector**를 통해 해결했습니다. 전체 앱은 opinlog.com에서 Vercel을 통해 배포되었습니다.

데이터 모델이 곧 제품이다

두 개의 테이블이 전체 아이디어를 담고 있습니다:

  • user_items — "내가 이것을 기록했고, $X를 지불했으며, 별점 4개를 주었다." 기록 이벤트당 하나의 행이 생성됩니다. 개인적인 데이터입니다.
  • canonical_items — "모든 사람의 행이 가리키는 하나의 공유된 항목." 중복이 제거되어 있으며, 모든 사람의 평점을 집계합니다.

매처(matcher)의 유일한 임무는 user_item을 올바른 canonical_item에 연결하는 것입니다. 그 연결(edge)이 바로 제품입니다.

모든 항목은 1024차원의 임베딩(embedding)을 가집니다. canonical 행의 임베딩은 코사인 ANN(Approximate Nearest Neighbour) 검색을 위해 HNSW로 인덱싱된 vector(1024)입니다:

// db/schema.ts (Drizzle)
embedding: vector("embedding", { dimensions: 1024 }).notNull(),
// ...
...

저는 의도적으로 canonical 임베딩을 NOT NULL로 설정했습니다. canonical 항목은 항상 매칭 가능해야 하므로, 모든 생성 경로에서 벡터를 제공해야 하며, 재계산(recompute) 로직은 이를 NULL로 평균화하는 대신 보존해야 하기 때문입니다.

"이것이 이것 중 하나인가?" — 매처(matcher)

항목을 추가할 때마다, 저는 새 항목을 임베딩하고 canonical 카탈로그에 대해 근사 최근접 이웃(approximate-nearest-neighbour) 검색을 실행합니다. Drizzle은 벡터 연산자를 모델링하지 않으므로, 쿼리는 원시 sql 템플릿을 사용합니다:

SELECT id, name, photo_url, rating_avg, rating_count,
       1 - (embedding <=> :q) AS similarity
FROM canonical_items
...

<=>는 pgvector의 코사인 거리 (cosine distance)이며, 1 - 거리는 유사도 (similarity)를 의미합니다. 신뢰도가 높은 결과는 기본값으로 자동 제안되지만 ("이것이 맞을 것으로 생각됩니다"), 그렇지 않은 경우 사용자는 상위 후보군과 함께 "이 중에 없음 — 새로 만들기"라는 탈출구 (escape hatch)를 보게 됩니다. 매칭은 선택 사항이며 비차단 방식 (non-blocking)입니다: user_itemcanonical_item_id = NULL 상태로 즉시 저장되며, 매칭 결과는 업로드 시점이나 나중에 큐 (queue)를 통해 해당 필드를 채우기만 하면 됩니다. 기본 루프 (자신의 항목 기록하기)에는 어떠한 마찰도 발생하지 않습니다.

스스로 정교해지는 카탈로그 (The self-sharpening catalog)

이 부분이 제가 가장 자랑스럽게 생각하는 지점입니다. 정규 항목 (canonical item)의 임베딩 (embedding)은 연결된 멤버들의 임베딩에 대한 **실시간 중심점 (running centroid)**입니다. 당신의 버거 기록을 제 기록과 연결하면, 정규 벡터 (canonical vector)는 두 벡터의 평균값으로 이동합니다. 더 많은 사람이 동일한 항목을 기록할수록, 그 벡터는 더욱 정교하고 대표성 있게 변하며, 즉 카탈로그가 사용됨에 따라 스스로 개선됩니다.

또한 "평점순 정렬"이 대규모 환경에서도 빠르게 유지되어야 하므로, 평점 집계 데이터는 **정규 행 (canonical row)에 비정규화 (denormalized)**되어 있으며, rating_avg는 저장된 생성 열 (stored generated column)로 관리됩니다:

rating_avg real GENERATED ALWAYS AS
  (CASE WHEN rating_count > 0 THEN rating_sum::real / rating_count END) STORED

따라서 정규 페이지 — 즉, 결과 화면 — 는 렌더링 시점에 집계 쿼리를 실행할 필요 없이, 단 하나의 행만 읽어서 "낯선 이들의 리뷰 11개 기준 4.6★"과 같은 정보를 보여줍니다.

왜 특별히 Aurora PostgreSQL인가

자격을 갖춘 세 가지 AWS 데이터베이스를 평가했습니다:

  • DynamoDB — 확장성은 뛰어나지만, 네이티브 벡터 검색 기능이 없습니다. OpenSearch를 별도로 결합해야 합니다.
  • Aurora DSQL — 분산형 Postgres이지만, pgvectorPostGIS 확장이 부족하여 핵심 매칭 기능을 수행할 수 없습니다.
  • Aurora PostgreSQL하나의 엔진에서 벡터 (pgvector), 지리 정보 (PostGIS), 그리고 관계형 데이터를 모두 처리합니다. 하나의 연결, 하나의 쿼리 플래너 (query planner)를 통해 "벡터 매칭"과 "평점 집계" 사이의 트랜잭션 조인 (transactional joins)을 수행할 수 있습니다.

마지막으로 언급한 점이 바로 이 프로젝트를 한 사람이 마감 기한 내에 구축할 수 있었던 핵심 이유입니다. 매칭 결과와 관계형 집계 (relational aggregation)가 동일한 데이터베이스 내에 존재하기 때문에, 아이템을 연결하고 해당 아이템의 정준 중심점 (canonical's centroid)과 평점을 재계산하는 과정이 단일 트랜잭션 (single transaction)으로 처리됩니다. 즉, 벡터 스토어 (vector store)와 RDBMS 사이를 오가는 복잡한 과정이 필요하지 않습니다.

기술 스택 (The stack)

Vercel 기반의 Next.js 16 (App Router, Server Actions) → pg/Drizzle을 통한 Aurora PostgreSQL Serverless v2 (pgvector + PostGIS + pg_trgm 포함). 임베딩 (Embeddings)은 Amazon Bedrock에서 가져옵니다 (이에 대한 자세한 내용은 다음 포스트에서 다룹니다). 로컬 Docker Postgres를 Aurora로 교체하는 작업은 DATABASE_URL 한 줄만 변경하면 끝나는 작업이었습니다. 드라이버와 SQL이 동일하기 때문입니다.

그 결과, 말 그대로 무엇이든 적용 가능한 범용적인 사용자 간 리뷰 그래프 (cross-user review graph)가 완성되었으며, 그 마법은 단 한 줄의 SQL인 ORDER BY embedding <=> :q에 담겨 있습니다.

H0 해커톤 ("Hack the Zero Stack with Vercel and AWS Databases")을 위해 제작되었습니다. 본 콘텐츠는 해당 해커톤 참가를 목적으로 작성되었습니다. #H0Hackathon

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0