

Retrieval-Augmented Generation나 하이브리드 검색을 구현할 때, 단순 벡터 검색 외에 BM25 기반의 키워드 검색이 병행되어야 정확도가 올라갑니다.
하지만 PostgreSQL 기본 이미지에는 한국어 형태소 분석 기능이 없습니다.
본 포스팅에서는 pgvector 이미지를 베이스로 mecab-ko 엔진과 textsearch_ko 확장을 포함한 커스텀 Docker 이미지를 빌드하고, 발생 가능한 트러블슈팅 사례를 정리하였습니다.
빌드 과정에서 흔히 발생하는 네트워크 및 의존성 문제를 해결하기 위해 다음과 같이 Dockerfile.pgvector를 구성했습니다.
미러 서버 변경: 기본 Debian 미러에서 발생하는 400 Bad Request를 방지하기 위해 한국 미러(ftp.kr.debian.org)를 우선 사용합니다.
CA 인증서 포함: ca-certificates를 명시적으로 설치하여 HTTPS 통신시의 인증 오류를 차단합니다.
Mecab-ko 사전 경로 최적화: 빌드 후 mecabrc를 직접 수정하여 사전 경로 인식을 자동화했습니다.
FROM pgvector/pgvector:pg17
# 1. 필수 빌드 도구 설치 및 미러 서버 최적화
RUN set -e; \
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do \
[ -f "$f" ] && sed -i 's|http://deb\.debian\.org|http://ftp.kr.debian.org|g' "$f" 2>/dev/null || true; \
done; \
apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl make g++ patch git \
automake autoconf libtool \
postgresql-server-dev-17 \
&& rm -rf /var/lib/apt/lists/*
# 2. mecab-ko 엔진 설치
RUN curl -LO https://bitbucket.org/eunjeon/mecab-ko/downloads/mecab-0.996-ko-0.9.2.tar.gz \
&& tar zxf mecab-0.996-ko-0.9.2.tar.gz \
&& cd mecab-0.996-ko-0.9.2 \
&& ./configure && make && make install \
&& ldconfig
# 3. mecab-ko-dic 사전 설치 및 경로 설정
RUN curl -LO https://bitbucket.org/eunjeon/mecab-ko-dic/downloads/mecab-ko-dic-2.1.1-20180720.tar.gz \
&& tar zxf mecab-ko-dic-2.1.1-20180720.tar.gz \
&& cd mecab-ko-dic-2.1.1-20180720 \
&& ./autogen.sh && ./configure && make && make install \
&& echo "dicdir = /usr/local/lib/mecab/dic/mecab-ko-dic" > /usr/local/etc/mecabrc
# 4. textsearch_ko(한국어 형태소 분석 확장) 빌드
RUN git clone --depth 1 https://github.com/i0seph/textsearch_ko.git \
&& cd textsearch_ko \
&& make USE_PGXS=1 \
&& make USE_PGXS=1 install \
&& cd .. && rm -rf textsearch_ko
로컬 환경의 5432 포트 충돌을 방지하고 데이터 영속성을 보장합니다.
services:
pgvector:
build:
context: .
dockerfile: Dockerfile.pgvector
container_name: pgvector
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports:
- "127.0.0.1:5433:5432"
volumes:
- pgvector_data:/var/lib/postgresql/data
volumes:
pgvector_data:

많은 분이 겪는 "pgAdmin 컨테이너 환경에서의 접속 실패" 현상은 네트워크 가상화 차이 때문입니다.
127.0.0.1로 설정 시 접속 불가.127.0.0.1은 DB 컨테이너가 아닌 자기 자신을 가리킴.host.docker.internal로 변경하여 호스트 머신을 경유하도록 설정합니다. (포트는 5433 유지)컨테이너가 정상 구동되면 아래 명령어로 확장이 활성화되었는지 확인하십시오.
# 확장 모듈 활성화 SQL
docker exec pgvector psql -U postgres -c "
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS textsearch_ko;
SELECT extname, extversion FROM pg_extension WHERE extname IN ('vector','textsearch_ko');
"
textsearch_ko 확장이 설치되었다면, 이제 이를 활용해 한국어 형태소 분석 기반의 검색 시스템을 구축할 수 있습니다.
검색 대상이 될 제목(title)과 본문(content)을 포함한 간단한 게시판 테이블을 만듭니다.
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
-- 검색 속도 최적화를 위한 텍스트 벡터 컬럼 추가
tsv_content tsvector
);
한국어 형태소 분석기를 사용하여 검색 전용 인덱스인 Generalized Inverted Index을 생성합니다.
-- 1. 본문 내용을 기반으로 GIN 인덱스 생성
CREATE INDEX idx_documents_content_fts ON documents USING GIN (to_tsvector('korean', content));
-- 2. (권장) 성능 향상을 위해 미리 계산된 컬럼(tsv_content)에 인덱스 생성
CREATE INDEX idx_documents_tsv ON documents USING GIN (tsv_content);
단순히 "포함 여부"만 체크하는 것이 아니라, 검색어와의 연관성에 따라 정렬하여 상위 결과를 가져오는 쿼리입니다.
-- '데이터베이스'라는 키워드로 검색하고 관련성 높은 순으로 정렬
SELECT
title,
ts_rank(to_tsvector('korean', content), query) AS rank
FROM
documents,
to_tsquery('korean', '데이터베이스') query
WHERE
to_tsvector('korean', content) @@ query
ORDER BY
rank DESC;
매번 INSERT나 UPDATE를 할 때마다 tsvector를 수동으로 갱신하는 것은 비효율적입니다. DB 레벨에서 트리거를 설정해두면 관리 포인트가 줄어듭니다.
-- 데이터가 삽입/수정될 때마다 tsv_content 컬럼을 자동으로 업데이트하는 함수/트리거
CREATE FUNCTION documents_trigger() RETURNS trigger AS $$
begin
new.tsv_content := to_tsvector('korean', coalesce(new.title,'') || ' ' || coalesce(new.content,''));
return new;
end
$$ LANGUAGE plpgsql;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE
ON documents FOR EACH ROW EXECUTE FUNCTION documents_trigger();
원하는 대로 단어가 잘리고 있는지 궁금하다면 ts_debug 함수를 활용 하면됩니다.
SELECT * FROM ts_debug('korean', '형태소 분석이 잘 되는지 확인합니다.');
현재 구성된 BM25(키워드 검색)와 이전에 설치한 pgvector(벡터 검색)를 결합하여 Reciprocal Rank Fusion(RRF) 알고리즘 등을 적용하면, 훨씬 더 강력한 RAG 시스템을 구축할 수 있습니다.
단순히 검색 결과에 포함되는 것을 넘어, "사용자가 의도한 결과가 최상단에 오게 하는 법"에 대한 기술적 해결책입니다.
검색어가 '제목'에 포함된 문서가 '본문'에 포함된 문서보다 더 중요할 가능성이 높습니다. PostgreSQL은 A, B, C, D 네 단계의 가중치를 정의했습니다.
-- 제목(A)과 본문(B)에 차등 가중치를 부여하여 tsvector 생성
-- (coalesce는 NULL 값을 빈 문자열로 처리하기 위함)
UPDATE documents
SET tsv_content =
setweight(to_tsvector('korean', coalesce(title, '')), 'A') ||
setweight(to_tsvector('korean', coalesce(content, '')), 'B');
이렇게 설정된 가중치는 ts_rank 계산 시 자동으로 반영되어, 제목에 키워드가 있는 문서가 검색 결과 상단에 배치됩니다.
가장 트렌디한 방식입니다.
벡터 검색의 의미론적 결과와 textsearch_ko의 키워드 결과 점수를 합산하여 최종 순위를 결정합니다.
-- RRF 하이브리드 검색 예시 (Pseudo-code 기반)
WITH vector_search AS (
-- 1. 벡터 검색 결과 (상위 50개)
SELECT id, row_number() OVER (ORDER BY embedding <=> '[0.1, 0.2, ...]') as rank
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]' LIMIT 50
),
keyword_search AS (
-- 2. BM25 키워드 검색 결과 (상위 50개)
SELECT id, row_number() OVER (ORDER BY ts_rank(tsv_content, to_tsquery('korean', '검색어')) DESC) as rank
FROM documents
WHERE tsv_content @@ to_tsquery('korean', '검색어')
ORDER BY rank LIMIT 50
)
-- 3. 두 결과의 순위를 역수로 합산 (RRF)
SELECT
COALESCE(v.id, k.id) AS id,
(COALESCE(1.0 / (60 + v.rank), 0) + COALESCE(1.0 / (60 + k.rank), 0)) AS rrf_score
FROM vector_search v
FULL OUTER JOIN keyword_search k ON v.id = k.id
ORDER BY rrf_score DESC;
RRF의 분모에 들어가는 상수
60은 낮은 순위의 결과가 전체 점수를 왜곡하는 것을 방지하는 완충 역할을 합니다.
실제 서비스 데이터에 맞춰 조정하며 최적의 값을 찾는 과정이 필요합니다.
이론적으로 완벽한 쿼리도 데이터가 쌓이면 느려질 수 있습니다.
PostgreSQL의 실행 계획을 분석하여 병목 지점을 찾는 방법입니다.
우리가 만든 GIN 인덱스가 제대로 작동하는지 확인하려면 쿼리 앞에 EXPLAIN ANALYZE를 붙여 실행합니다.
EXPLAIN ANALYZE
SELECT title
FROM documents
WHERE tsv_content @@ to_tsquery('korean', '데이터베이스');
체크 포인트:
RRF 쿼리는 공통 테이블 표현식을 사용하므로 각 단계별 소요 시간을 확인하는 것이 중요합니다.
EXPLAIN (ANALYZE, BUFFERS)
WITH vector_search AS (...) -- 생략
SELECT ...
단순히 "기능이 돌아가게 만드는 것"과 "시행착오 없이 쓸 수 있는 환경을 만드는 것"의 차이를 다시 한번 체감했습니다.
처음에는 textsearch_ko 확장을 설치하는 단순한 빌드 과정이라 생각했지만, 실제 환경에서는 네트워크 미러 서버의 불안정성이나 Docker 컨테이너 간의 네트워크 루프백(127.0.0.1 이슈) 같은 실무적인 복병들이 많았습니다.
이러한 디테일한 기록이 모여 블로그의 수준을 높이고, 시간을 아껴줄 수 있다고 믿습니다.
특히 벡터 검색과 전통적인 키워드 검색의 결합은 현대 검색 아키텍처의 핵심인 만큼, 서비스의 검색 품질을 한 단계 끌어올리는 밑거름이 되었으면 합니다.