기본 개념과 사용 사례
Java 백엔드 개발자라면 애플리케이션에 효과적인 의미 이해 기능을 도입하고자 할 때, gte-base-zh 모델은 강력한 선택지입니다. 예를 들어, 사용자가 "여름용 가벼운 코트"를 검색하면, 기존의 키워드 매칭 방식은 단어가 정확히 포함된 항목만 찾아낼 수 있지만, gte-base-zh는 "여름용 실내용 자외선 차단 룩업", "통기성 좋은 피부보호 의류"와 같은 표현도 의미적으로 유사하게 인식하여 보다 적절한 결과를 제공합니다.
프로젝트 초기 설정
먼저, gte-base-zh 모델을 실행하는 서버를 준비해야 합니다. 이는 자체 배포 또는 클라우드 서비스를 통해 가능합니다. 최종적으로는 http://your-model-server:port/encode 형식의 엔드포인트를 확보해야 하며, 이는 텍스트를 입력받아 벡터 배열을 반환합니다.
Spring Boot 프로젝트 생성 시 필요한 의존성은 다음과 같습니다:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
핵심 구성 요소 설계
모델 클라이언트 구현
모델 서버와 통신하기 위해 GteModelClient 클래스를 생성합니다. 이 클래스는 RestTemplate를 사용해 텍스트를 벡터로 변환합니다.
@Component
public class GteModelClient {
@Value("${model.api.url}")
private String modelApiUrl;
private final RestTemplate restTemplate;
public GteModelClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public List<Float> getTextEmbedding(String text) {
Map<String, String> requestBody = Map.of("text", text);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(
modelApiUrl + "/encode",
requestEntity,
Map.class
);
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
List<Double> rawVector = (List<Double>) response.getBody().get("embedding");
return rawVector.stream()
.map(Double::floatValue)
.collect(Collectors.toList());
} else {
throw new RuntimeException("벡터 생성 실패: " + response.getStatusCode());
}
}
}
벡터 저장 전략
벡터는 일반적으로 768차원이며, 데이터베이스에 TEXT 필드로 직렬화하여 저장할 수 있습니다. 그러나 성능상 제약이 있으므로, 실제 운영 환경에서는 pgvector 또는 Milvus 같은 전용 벡터 데이터베이스를 권장합니다.
엔티티 정의 예시:
@Entity
@Table(name = "document_embedding")
public class DocumentEmbedding {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(columnDefinition = "TEXT")
private String content;
@Column(columnDefinition = "TEXT")
private String vectorJson;
private String identifier;
private String category;
// getter/setter 및 메서드 생략
}
의미 검색 로직 구현
SemanticSearchService는 쿼리 벡터 생성, 데이터 조회, 유사도 계산을 담당합니다.
@Service
public class SemanticSearchService {
private final GteModelClient modelClient;
private final DocumentEmbeddingRepository repository;
public SemanticSearchService(GteModelClient modelClient, DocumentEmbeddingRepository repository) {
this.modelClient = modelClient;
this.repository = repository;
}
public List<SearchResult> search(String query, int topK, String category) {
List<Float> queryVector = modelClient.getTextEmbedding(query);
List<DocumentEmbedding> candidates = category != null ?
repository.findByCategory(category) : repository.findAll();
return candidates.stream()
.map(candidate -> {
List<Float> candidateVector = parseVector(candidate.getVectorJson());
float similarity = cosineSimilarity(queryVector, candidateVector);
return new SearchResult(candidate.getContent(), candidate.getIdentifier(), similarity);
})
.sorted((a, b) -> Float.compare(b.getScore(), a.getScore()))
.limit(topK)
.collect(Collectors.toList());
}
private float cosineSimilarity(List<Float> a, List<Float> b) {
double dotProduct = 0.0, normA = 0.0, normB = 0.0;
for (int i = 0; i < a.size(); i++) {
dotProduct += a.get(i) * b.get(i);
normA += Math.pow(a.get(i), 2);
normB += Math.pow(b.get(i), 2);
}
return (float) (dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)));
}
public static class SearchResult {
private String text;
private String id;
private float score;
public SearchResult(String text, String id, float score) {
this.text = text;
this.id = id;
this.score = score;
}
// getter
}
}
외부 API 노출
REST 컨트롤러를 통해 검색 기능을 공개합니다.
@RestController
@RequestMapping("/api/v1/search")
public class SearchController {
private final SemanticSearchService searchService;
public SearchController(SemanticSearchService searchService) {
this.searchService = searchService;
}
@GetMapping("/semantic")
public List<SemanticSearchService.SearchResult> semanticSearch(
@RequestParam String q,
@RequestParam(defaultValue = "10") int topK,
@RequestParam(required = false) String category
) {
return searchService.search(q, topK, category);
}
@PostMapping("/index")
public String indexDocument(@RequestBody IndexRequest request) {
// 벡터 생성 후 저장 로직
return "등록 완료";
}
static class IndexRequest {
private String text;
private String id;
private String category;
// getter/setter
}
}
고성능 및 확장성 개선
- 캐싱: Caffeine 또는 Redis를 사용해 자주 사용되는 쿼리 벡터를 메모리에 저장해 반복 호출 비용을 줄입니다.
- 벡터 데이터베이스 전환:
pgvector를 사용하면<=>연산자를 통해 효율적인 근접 검색이 가능하며, 대량 데이터 처리에 적합합니다. - 비동기 처리: 대량 인덱싱 작업은
@Async또는 메시지 큐를 통해 분리하여 응답 지연을 최소화합니다. - 장애 대응: 모델 서비스 장애 시, 키워드 기반 검색으로 전환하는 다중 경로 전략을 도입합니다.
결론
gte-base-zh를 활용한 의미 검색 시스템은 단순한 키워드 매칭을 넘어, 사용자의 진짜 의도를 파악할 수 있는 핵심 기능입니다. 초기 구현은 간단하지만, 캐싱, 전용 벡터 스토리지, 비동기 처리 등을 적용하면 생산 환경에서도 안정적이고 고성능으로 운영할 수 있습니다. 향후에는 영역 맞춤형 미세 조정을 통해 더 정교한 성능을 달성할 수 있습니다.