SpringBoot에서 gte-base-zh 모델을 활용한 중국어 의미 검색 시스템 구현

기본 개념과 사용 사례

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를 활용한 의미 검색 시스템은 단순한 키워드 매칭을 넘어, 사용자의 진짜 의도를 파악할 수 있는 핵심 기능입니다. 초기 구현은 간단하지만, 캐싱, 전용 벡터 스토리지, 비동기 처리 등을 적용하면 생산 환경에서도 안정적이고 고성능으로 운영할 수 있습니다. 향후에는 영역 맞춤형 미세 조정을 통해 더 정교한 성능을 달성할 수 있습니다.

태그: SpringBoot gte-base-zh 벡터 검색 pgvector Caffeine

5월 26일 16:05에 게시됨