[디프만] 임베딩으로 검색 기능 구현하기

2025. 11. 21. 22:33·사이드 프로젝트

디프만 활동에서 "밥토리" 프로젝트에 진행한 내용입니다.

밥토리

디프만에서 혼밥 정보 서비스 "밥토리"에서 백엔드 개발자로 참여하였다. "밥토리" 서비스 기능 중 검색 기능을 임베딩 벡터를 통해 구현하였다.

검색

검색은 어려운 기능이다. 사이드 프로젝트에서 검색 기능은 때때로 피하기 어려운 기능이다. 사이드 프로젝트 단에서 검색 기능을 구현하기 어렵다고 생각하는 이유는 사용가능한 리소스가 적고, 빠른 시간안에 개발 할 수 있어야하기 때문이다. 디프만 활동 당시에도 검색에 대한 요구사항이 생겼다. 구체적으로는 식당 이름, 또는 메뉴에 대한 검색 기능을 제공할 수 있어야 했다. 더 나아가 여러 검색어가 호환될 수 있으면 더 좋다. 이에 따라검색 기능을 구현할 수 있는 방법을 모색해보았다.

Like

가장 빠른 시간에 개발할 수 있는 방법이다. 검색어 기준으로 Like 함수를 통해 동일한 식당 또는 메뉴를 검색하여 반환할 수 있다.

SELECT *
FROM menu
WHERE name LIKE '%고기%';

 

이 경우 해당 검색어를 포함한 단어만을 검색할 수 있기 때문에 일부 단어에 대해서 제약이 존재한다.

Full Text Index

단어를 토큰화하여 Index Scan을 지원하는 방법이다.

SELECT *
FROM menu
WHERE to_tsvector('simple', name) @@ to_tsquery('고기');

그러나 Full Text Index의 경우, Like에서의 제약을 해결할 수 없고, 한국어의 경우 공백 기준으로 토큰화하기 때문에 단어의 길이가 짧은 식당, 메뉴 검색 용도로 적합하지 않다.

외부 검색 엔진

Elasticsearch, OpenSearch와 같은 전용 검색 엔진을 사용하는 방법이다.

Elasticsearch에서 지원하는 역색인 기술을 사용한다면 앞선 문제들을 해결할 수 있다. 결론적으로 외부 검색 엔진을 도입할 수 없었다. 현재 밥토리 팀에서는 최소한의 인프라 자원을 사용하고 있었기 때문에, 외부 검색엔진에 대한 시스템 리소스를 제공할 수 없었다.

임베딩

임베딩은 다차원의 데이터를 보다 낮은 차원의 공간으로 표현하는 방법이다. 데이터를 벡터 형태로 변환할 수 있는데, 벡터에는 의미적 유사성을 가지게 된다. 임베딩 벡터를 통해 식당을 검색할 수 있다면, 사용자에게 더 좋은 검색 경험을 줄 수 있다고 판단했다.

https://api.ncloud-docs.com/docs/clovastudio-embeddingv2

 

임베딩 v2

 

api.ncloud-docs.com

또한 현재 밥토리에서 사용하고 있는 NCP에서 임베딩 관련 API를 제공함을 확인했다.

시스템 오버 헤드와 퍼포먼스를 고려했을때, 현재 밥토리 팀에서 사용하기 가장 적합한 기술이라고 판단하였다.

PGVector

현재 밥토리팀은 PSQL을 사용하고 있다. 앞서 말했듯이 검색을 위한 추가 시스템 리소스를 제공할 수 없었기 때문에 PSQL에서 지원하는 PGVector Extension을 추가하여 사용하였다.

https://github.com/pgvector/pgvector

 

GitHub - pgvector/pgvector: Open-source vector similarity search for Postgres

Open-source vector similarity search for Postgres. Contribute to pgvector/pgvector development by creating an account on GitHub.

github.com

 

검색 기능 구현하기

검색 기능을 구현해보겠다.

public class StoreEmbedding {

    ##중략
    
    private float[] embedding;

    @OneToOne(fetch = FetchType.LAZY)
    private Store store;
}

NCP에서 제공하는 임베딩 벡터가 1024차원이기 때문에 데이터 크기를 고려하여 기존 식당 엔티티에 일대일 연관관계를 가지도록 StoreEmbedding 엔티티를 추가했다.

 

if (store.getName() != null) {
    sb.append("가게명: ").append(store.getName()).append("\n");
}

if (store.getDescription() != null) {
    sb.append("설명: ").append(store.getDescription()).append("\n");
}

각 식당에 대해 식당의 정보를 모두 임베딩에 표현할 수 있도록 가게명, 설명, 메뉴를 통해 일련의 정보 데이터를 생성하여 임베딩 벡터를 저장한다.

 

WITH se_dist AS (
  SELECT 
    se.*,
    se.embedding <=> :embedding AS distance
  FROM store_embedding se
)
SELECT se_dist.*, s.name
FROM se_dist
JOIN store s ON s.id = se_dist.store_id
WHERE se_dist.distance > :lastScore
ORDER BY se_dist.distance ASC, s.id ASC
LIMIT :limit;

검색 임베딩 벡터를 식당 임베딩 벡터와 비교하여 유사도 기준으로 정렬하여 반환한다.

 

검색 테스트

버거 검색

완성된 검색 API는 식당, 메뉴에 대한 검색은 올바르게 방어할 수 있었고, 이외에도 기존 검색어 비교를 통한 검색 로직 보다는 더 넓은 범위의 식당을 보완할 수 있었다. 런칭데이에서도 검색관련하여 좋은 피드백을 받을 수 있었는데, 만약 빠른 시간에 낮은 시스템 오버헤드을 요구하는 검색 기능을 구현해야한다면 검색 임베딩을 고려해볼것을 추천한다.

ETC

아래 내용은 해당 기능을 개발하면서 만난 문제이다.

CI Failed

PGVector를 사용함에 따라 기존 CI 테스트 일부가 실패하고 있었다.

테스트 결과

실패한 테스트 모두 통합 테스트로 테스트 환경 설정이 실패하는 것으로 확인 되었다. 문제 원인은 총 두가지 였다.

  1. PGVector가 CI환경에서 구성되어 있지 않다
  2. Hibernate가 PGVector를 제대로 인식하지 못한다.

기존에 사용중인 테스트 PSQL 이미지는 PSQL + PGIS 으로 PGVector는 포함되어 있지 않았다.

PSQL + PGIS + PQVector 에 대한 공식 이미지는 확인되지 않아, 직접 테스트 이미지를 생성하여 해결하였다.

FROM postgis/postgis:16-3.4

RUN apt-get update \
 && apt-get install -y postgresql-16-pgvector

COPY infra/docker/init.sql  /docker-entrypoint-initdb.d/init-pgvector.sql

pqvector를 설치하는 것 이외에도 PostgesSQL Server에서 "Create Extension Vector"를 통해 메타데이터를 등록해야한다. (init.sql에서 처리)

 

com.pgvector.pgvector → hibernate vector

https://docs.hibernate.org/orm/current/userguide/html_single/#vector-module

 

Hibernate ORM User Guide

Starting in 6.0, Hibernate allows to configure the default semantics of List without @OrderColumn via the hibernate.mapping.default_list_semantics setting. To switch to the more natural LIST semantics with an implicit order-column, set the setting to LIST.

docs.hibernate.org

 

vector 데이터를 원시 타입 이외에도 pgvector라는 래퍼 클래스를 제공한다. 해당 래퍼 클래스에 관한 이슈가 있는 것으로 확인된다. pgvector 문서에서도 float[] 사용을 권장하고 있다.

'사이드 프로젝트' 카테고리의 다른 글

[아워메뉴] K6를 사용한 부하 테스트 -1  (5) 2025.06.11
[아워메뉴] API 응답 필드 변경 대처하기  (0) 2025.05.27
[아워메뉴] 모니터링 서버 구축 -2 (Alloy, Loki)  (1) 2025.05.20
[아워메뉴] 모니터링 서버 구축 -1 (Grafana, Prometheus)  (0) 2025.05.19
[Github Action] 로컬환경에서 Github Action 테스트하기  (2) 2025.04.10
'사이드 프로젝트' 카테고리의 다른 글
  • [아워메뉴] K6를 사용한 부하 테스트 -1
  • [아워메뉴] API 응답 필드 변경 대처하기
  • [아워메뉴] 모니터링 서버 구축 -2 (Alloy, Loki)
  • [아워메뉴] 모니터링 서버 구축 -1 (Grafana, Prometheus)
bluesparrow
bluesparrow
개인 공부 목적으로 작성된 블로그 입니다.
  • bluesparrow
    Bluesparrow
    bluesparrow
  • 전체
    오늘
    어제
    • 분류 전체보기 (90)
      • 회고 (3)
      • CS (17)
        • 운영체제 (1)
        • 컴퓨터구조 (2)
        • 데이터베이스 (5)
        • 네트워크 (9)
      • PS (7)
        • 백준 (7)
      • 사이드 프로젝트 (12)
      • AI (6)
        • 강화학습 (0)
        • 기계학습 (3)
      • 보안 (13)
      • Java (11)
        • 스프링 부트 (6)
      • 인프라 (4)
        • 도커 (3)
        • AWS (4)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 회고
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    자바
    보안
    a
    도커
    SpringSecurity
    논문
    JPA
    이펙티브 자바
    게임이론
    회고
    강화학습
    조합론
    이분탐색
    Spring
    트러블슈팅
    BFS
    그래프
    그리디
    컴퓨터구조
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
bluesparrow
[디프만] 임베딩으로 검색 기능 구현하기
상단으로

티스토리툴바