배경
회사에서 AI를 실제 서비스에 활용해보라는 과제를 받았습니다.
막연히 "AI로 뭘 해볼까" 고민하다가, 우리 패션 이커머스 플랫폼의 검색 기능을 자연어 기반으로 개선해보기로 했습니다. 기존 키워드 검색의 한계는 명확했습니다.
- "린넨 소재 셔츠"는 찾지만 "시원한 여름 셔츠"는 못 찾음
- "드라이클리닝 제외" 같은 부정 조건을 표현할 방법이 없음
- "오피스룩"이라는 표현이 셔츠·재킷·블라우스 중 뭘 의미하는지 시스템이 모름
- 개인화 추천이 아예 없어서, 모든 사용자에게 같은 인기 상품만 노출됨
사용자가 "드라이클리닝 안 해도 되는 루즈한 자켓", "오늘 뭐 입지?" 처럼 자연스럽게 검색하고, 시스템이 의도를 파악해 검색·추천·주문조회까지 알아서 처리하는 걸 목표로 잡았습니다.
기술 스택 선정
새로 배우는 스택이 많았습니다. 정리하면 이렇습니다.
| Language / Framework | Java 17, Spring Boot 3.3.2 | 웹 서버 |
| AI 통합 | Spring AI 1.0.0-M1 | LLM·임베딩 연동 |
| LLM | OpenAI GPT-4o-mini | 의도 분류, 키워드 추출, 응답 생성 |
| Embedding | text-embedding-3-small (1536차원) | 텍스트 벡터화 |
| Search | OpenSearch 2.12 | 벡터 검색(kNN/HNSW) + BM25 |
| DB | PostgreSQL | 상품 메타데이터, 대화 히스토리 |
| Reactive | Spring WebFlux (Reactor) | SSE 스트리밍 |
Spring AI는 M1(마일스톤) 버전이라 정식 릴리즈 대비 문서가 부족한 부분이 있었는데, 이 부분은 다음 편에서 실제로 부딪혔던 문제들과 함께 다루겠습니다.
전체 아키텍처
사용자 요청이 들어오면, 먼저 의도(Intent) 를 분류하고 그에 맞는 파이프라인으로 분기하는 구조로 설계했습니다.
사용자 요청 (자연어 질의)
│
▼
검색 API (단일 응답 / SSE 스트리밍)
│
▼
자연어 검색 서비스
│
① 의도 분류 ──→ Intent 판단
│
├── 상품 검색 ──→ ② 키워드/태그 추출 → ③ 임베딩 생성 → ④ 하이브리드 검색
│
├── 개인화 추천 ──→ ⑤ 추천 서비스 (주문/찜/최근 본 상품 기반)
│
├── 주문 조회 ──→ ⑥ 주문 서비스
│
└── 그 외 (인사/질문 등) ──→ ⑦ 일반 응답 서비스
│
┌─────┴──────┐
▼ ▼
OpenSearch PostgreSQL
(벡터 검색) (메타데이터, 대화 히스토리)
핵심은 "모든 요청을 검색으로 처리하지 않는다"는 점입니다. 사용자가 "주문 확인하고 싶어요"라고 하면 검색 로직을 태우지 않고 바로 주문 조회로 보내야 하고, 단순 인사말에 굳이 벡터 검색을 돌릴 필요도 없습니다.
그래서 첫 관문으로 의도 분류를 두고, 이후 단계를 의도별로 분기하는 구조를 잡았습니다.
실제 검색 요청이 처리되는 흐름 (예시)
"드라이클리닝 안해도 되는 루즈한 자켓"이라는 질의가 들어왔을 때 내부적으로 벌어지는 일을 순서대로 정리하면 이렇습니다.
[1] 의도 분류
→ "상품 검색" 의도로 확신도 0.95로 분류
[2] 키워드/태그 추출
→ 검색용 문장: "가정에서 세탁 가능한 편안한 루즈핏 오버사이즈 자켓"
→ 핵심 키워드: 자켓, 루즈핏
→ 부정 조건: 드라이클리닝 (세탁방법 태그에서 제외 처리)
[3] 임베딩 생성
→ 위 문장을 1536차원 벡터로 변환
[4] 검색 가중치 계산
→ 추출된 태그가 3개 이상이면 키워드 검색 비중을 높임
[5] 벡터 검색 + 키워드 검색 동시 수행
[6] 두 결과를 RRF(Reciprocal Rank Fusion)로 결합
[7] 검색 결과 + 대화 맥락을 반영해 자연스러운 응답 문장 생성
이 흐름에서 가장 시간을 많이 쓴 부분은 2번(키워드/태그 추출)과 6번(하이브리드 결합)이었는데, 각각 다음 편들에서 자세히 다루겠습니다.
오늘의 정리
이번 편에서는 왜 이 프로젝트를 시작했고, 어떤 스택으로 어떤 구조를 잡았는지만 정리했습니다. 실제로 부딪힌 문제들은 다음 편부터 하나씩 풀어보겠습니다.
- 2편: LLM에게 의도를 분류시키고, 검색용 키워드/태그를 추출하게 만드는 프롬프트 설계 과정 (temperature 설정, 구조화된 출력 받기)
- 3편: 벡터 검색과 키워드 검색을 어떻게 결합했는지 (RRF, 동적 가중치, OpenSearch 인덱스 설계)
- 4편: SSE로 응답을 스트리밍한 이유, OpenAI 호출 없이 개인화 추천을 구현한 방법, 이미지 태깅
다음 편에서 이어가겠습니다.
'ELK' 카테고리의 다른 글
| [자연어 검색 개발기 #3] 벡터 검색과 키워드 검색, 뭘 더 믿어야 할까 (0) | 2026.07.12 |
|---|---|
| [자연어 검색 개발기 #2] LLM에게 "의도"부터 물어보게 만들기 (1) | 2026.07.11 |
| Elasticsearch CircuitBreaker GC 관련 (3) | 2023.10.26 |
| Elasticsearch Essential 트러블 슈팅 사례 (0) | 2023.10.26 |
| Elasticsearch 커스텀 분석기 만들기 (1) (0) | 2023.09.25 |
