|
| 1 | +# 벡터 검색 인프라 구축 (#35) |
| 2 | + |
| 3 | +## 1. OpenSQL + pgvector 구성 |
| 4 | + |
| 5 | +### 배경 |
| 6 | + |
| 7 | +벡터 검색 기능 구현에 pgvector 확장이 필요하다. |
| 8 | +공식 `pgvector/pgvector` Docker 이미지가 있지만, 대회 요구사항이 **OpenSQL(tmaxopensql) 기반**이라 교체할 수 없었다. |
| 9 | +`tmaxopensql/postgres:14.6` 이미지에는 pgvector가 포함되어 있지 않아서 직접 컴파일해서 설치하는 방식을 택했다. |
| 10 | + |
| 11 | +### 해결 방법 |
| 12 | + |
| 13 | +`docker/opensql/Dockerfile`을 새로 만들어 `tmaxopensql/postgres:14.6` 위에 pgvector 0.8.0을 소스에서 빌드해 설치했다. |
| 14 | +기존 이미지 안에 `gcc`, `make`, `pg_config`가 모두 있어서 별도 도구 설치 없이 컴파일이 가능했다. |
| 15 | + |
| 16 | +```dockerfile |
| 17 | +FROM tmaxopensql/postgres:14.6 |
| 18 | + |
| 19 | +USER root |
| 20 | + |
| 21 | +RUN curl -L https://github.com/pgvector/pgvector/archive/refs/tags/v0.8.0.tar.gz \ |
| 22 | + -o /tmp/pgvector.tar.gz \ |
| 23 | + && tar -xzf /tmp/pgvector.tar.gz -C /tmp \ |
| 24 | + && cd /tmp/pgvector-0.8.0 \ |
| 25 | + && make PG_CONFIG=/usr/pgsql-14/bin/pg_config \ |
| 26 | + && make install PG_CONFIG=/usr/pgsql-14/bin/pg_config \ |
| 27 | + && rm -rf /tmp/pgvector.tar.gz /tmp/pgvector-0.8.0 |
| 28 | + |
| 29 | +COPY init-and-start.sh /usr/local/bin/init-and-start.sh |
| 30 | +RUN chmod +x /usr/local/bin/init-and-start.sh |
| 31 | + |
| 32 | +CMD ["/usr/local/bin/init-and-start.sh"] |
| 33 | +``` |
| 34 | + |
| 35 | +### init-and-start.sh |
| 36 | + |
| 37 | +OpenSQL entrypoint는 Ansible을 실행해 PostgreSQL을 초기화한다. |
| 38 | +Ansible 완료 후 CMD로 지정한 `init-and-start.sh`가 실행되며 아래 작업을 수행한다. |
| 39 | + |
| 40 | +1. postgres를 로컬 소켓으로 임시 시작 |
| 41 | +2. `docgrid` DB 생성 (없으면) |
| 42 | +3. `docgrid` DB에 `vector` 확장 활성화 |
| 43 | +4. 외부 TCP 접속 허용을 위해 `pg_hba.conf`에 항목 추가 |
| 44 | +5. postgres 정지 후 foreground로 재시작 |
| 45 | + |
| 46 | +```bash |
| 47 | +pg_ctl start -D "$PGDATA" -l /tmp/pg_init.log -o "-h ''" -w |
| 48 | + |
| 49 | +psql -d postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'docgrid'" | grep -q 1 \ |
| 50 | + || psql -d postgres -c "CREATE DATABASE docgrid OWNER docgrid;" |
| 51 | + |
| 52 | +psql -d docgrid -c "CREATE EXTENSION IF NOT EXISTS vector;" |
| 53 | + |
| 54 | +grep -qxF "host all all 0.0.0.0/0 trust" "$PGDATA/pg_hba.conf" \ |
| 55 | + || echo "host all all 0.0.0.0/0 trust" >> "$PGDATA/pg_hba.conf" |
| 56 | + |
| 57 | +pg_ctl stop -D "$PGDATA" -m fast -w |
| 58 | +exec postgres |
| 59 | +``` |
| 60 | + |
| 61 | +### 트레이드오프 — pg_hba.conf 규칙 범위 |
| 62 | + |
| 63 | +현재 `host all all 0.0.0.0/0 trust`로 설정되어 있다. |
| 64 | +로컬 개발 환경에서는 문제없지만, 포트가 외부 네트워크에 노출되는 상황(공용 와이파이, 시연 환경 등)에서는 인증 없이 접속이 가능해질 수 있다. |
| 65 | +시연 전에는 `192.168.65.1/32` 또는 실제 Docker 브리지 대역으로 범위를 좁히는 것을 권장한다. |
| 66 | + |
| 67 | +### 참고 — Ansible 재실행 동작 |
| 68 | + |
| 69 | +OpenSQL entrypoint는 컨테이너가 새로 생성될 때마다 Ansible을 실행한다 (볼륨 유지 여부와 무관). |
| 70 | +이미 초기화된 경우 대부분의 태스크가 skip되지만 전체 플레이가 돌기 때문에 약 5분이 소요된다. |
| 71 | +`docker compose build` 후 재시작 시에도 동일하게 발생하므로, Dockerfile 변경은 확실한 사항만 모아서 한 번에 반영하는 것이 좋다. |
| 72 | + |
| 73 | +--- |
| 74 | + |
| 75 | +## 2. 임베딩 서버 구축 |
| 76 | + |
| 77 | +### 설계 배경 |
| 78 | + |
| 79 | +pgvector 기반 벡터 검색을 위해 텍스트를 1024차원 벡터로 변환하는 임베딩 서버가 필요했다. |
| 80 | +Spring Boot에서 직접 모델을 실행하기 어렵기 때문에 Python 서버를 별도 서비스로 분리하고 HTTP로 통신하는 구조를 채택했다. |
| 81 | + |
| 82 | +모델은 **BAAI/bge-m3**를 사용한다. 다국어(한국어 포함) 지원, 1024차원 dense vector 출력, pgvector HNSW 인덱스와의 궁합이 선택 이유다. |
| 83 | + |
| 84 | +### 구현 구조 |
| 85 | + |
| 86 | +``` |
| 87 | +embedding-server/ |
| 88 | +├── main.py # FastAPI 앱 |
| 89 | +├── requirements.txt # 의존성 (버전 고정) |
| 90 | +└── Dockerfile # python:3.11-slim 기반 |
| 91 | +``` |
| 92 | + |
| 93 | +Spring Boot와 같은 `docgrid-local` Docker 네트워크에 올라가며, `http://docgrid-embedding:8000`으로 통신한다. |
| 94 | + |
| 95 | +### API 명세 |
| 96 | + |
| 97 | +#### GET /health |
| 98 | + |
| 99 | +서버 및 모델 로드 상태 확인. |
| 100 | + |
| 101 | +**Response 200** |
| 102 | +```json |
| 103 | +{ "status": "ok" } |
| 104 | +``` |
| 105 | + |
| 106 | +**Response 503** — 모델 미로드 시 |
| 107 | +```json |
| 108 | +{ "detail": "Model not loaded" } |
| 109 | +``` |
| 110 | + |
| 111 | +#### POST /embed |
| 112 | + |
| 113 | +텍스트를 1024차원 벡터로 변환. |
| 114 | + |
| 115 | +**Request** |
| 116 | +```json |
| 117 | +{ "text": "검색할 텍스트" } |
| 118 | +``` |
| 119 | + |
| 120 | +**Response 200** |
| 121 | +```json |
| 122 | +{ "vector": [0.012, -0.034, ..., 0.087] } |
| 123 | +``` |
| 124 | + |
| 125 | +**Response 503** — 모델 미로드 시 |
| 126 | +```json |
| 127 | +{ "detail": "Model not loaded" } |
| 128 | +``` |
| 129 | + |
| 130 | +### 주요 설계 결정 |
| 131 | + |
| 132 | +#### FastAPI 선택 |
| 133 | + |
| 134 | +async 기반으로 Spring Boot에서 동시 요청이 들어올 때 안정적으로 처리한다. |
| 135 | +Swagger UI가 자동 생성되어 별도 설정 없이 `http://localhost:8000/docs`에서 확인 가능하다. |
| 136 | + |
| 137 | +#### 모델 런타임 다운로드 + 볼륨 캐시 |
| 138 | + |
| 139 | +빌드 시 모델을 이미지에 포함하면 이미지 크기가 3GB 이상 커진다. |
| 140 | +대신 첫 컨테이너 시작 시 HuggingFace에서 다운로드하고 `huggingface-cache` Docker 볼륨에 캐시한다. |
| 141 | +두 번째 실행부터는 볼륨에서 로드하므로 다운로드 없이 빠르게 뜬다. |
| 142 | + |
| 143 | +```yaml |
| 144 | +volumes: |
| 145 | + - huggingface-cache:/root/.cache/huggingface |
| 146 | +``` |
| 147 | +
|
| 148 | +#### 의존성 버전 고정 |
| 149 | +
|
| 150 | +`FlagEmbedding==1.2.11`이 최신 `transformers`(4.47+)를 끌어오면 `torch 2.4.1`과 DTensor import 충돌이 발생한다. |
| 151 | +`transformers==4.44.2`로 고정해서 해결했다. |
| 152 | + |
| 153 | +`peft` 패키지는 `FlagEmbedding`의 reranker 모듈이 의존하지만 자동 설치되지 않아 명시적으로 추가했다. |
| 154 | + |
| 155 | +``` |
| 156 | +FlagEmbedding==1.2.11 |
| 157 | +torch==2.4.1 |
| 158 | +transformers==4.44.2 |
| 159 | +peft==0.12.0 |
| 160 | +``` |
| 161 | +
|
| 162 | +#### healthcheck — curl 대신 python3 urllib |
| 163 | +
|
| 164 | +`python:3.11-slim`에는 `curl`이 없다. |
| 165 | +별도 패키지 설치 없이 Python 표준 라이브러리로 healthcheck를 구현했다. |
| 166 | +
|
| 167 | +```yaml |
| 168 | +test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] |
| 169 | +``` |
| 170 | + |
| 171 | +### 로컬 실행 방법 |
| 172 | + |
| 173 | +```bash |
| 174 | +docker compose build embedding-server |
| 175 | +docker compose up -d embedding-server |
| 176 | +``` |
| 177 | + |
| 178 | +- 첫 실행 시 bge-m3 모델 다운로드로 약 10~15분 소요 (약 3GB) |
| 179 | +- `docker logs -f docgrid-embedding` 으로 진행 상태 확인 |
| 180 | +- `Uvicorn running on http://0.0.0.0:8000` 로그가 뜨면 준비 완료 |
| 181 | +- 이후 재시작은 볼륨 캐시에서 로드하므로 빠름 |
0 commit comments