배경
현재 진행중인 의료 검색 서비스에서는 자연어 질의가 들어오면 관련있는 의료 표준 데이터를 찾는 과정이 있다.
의료 표준 데이터는 elasticsearch에 적재되어 있고 기본 알고리즘인 BM25를 사용해서
질의어와 저장되어 있는 문서가 얼마나 관련있는지 점수로 계산하고 점수순으로 문서들을 조회하고 있었다.
하지만 키워드 기반으로 점수를 계산하는 BM25를 사용하다보니
단어적인 관련성은 있지만 의미적 관련성은 떨어지는 데이터가 조회가 되었고
도메인 전문가 평가에서 감점을 받게 되었다.
이를 해결하기 위해 벡터를 사용한 의미적 유사도를 적용하게 되었다.
알고리즘
BM25
BM25는 검색어의 빈도, 희소성, 문서 길이를 고려해서 관련성 점수를 계산한다.
테스트를 위해 아래와 같은 인덱스를 정의하고 데이터를 넣어줬다.
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text)
private String name;
@Field(type = FieldType.Text)
private String description;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Integer)
private Integer stock;
}
[
{
"id": "product-1",
"name": "삼성 노트북",
"description": "고성능 노트북 컴퓨터",
"price": 1200000.0,
"category": "전자제품",
"stock": 10
},
{
"id": "product-2",
"name": "기계식 키보드",
"description": "타건감이 우수한 기계식 키보드",
"price": 120000.0,
"category": "전자제품",
"stock": 40
},
{
"id": "product-3",
"name": "게이밍 노트북",
"description": "고사양 그래픽 카드가 탑재된 게이밍 노트북",
"price": 1900000.0,
"category": "전자제품",
"stock": 7
}
]
description으로 노트북을 검색해보면
product-2의 경우는 노트북이라는 단어가 포함되어 있지 않기 때문에 검색 결과에서 제외되고
product-1과 product-3의 경우 검색어의 빈도와 희소성은 같기 때문에
description 길이가 더 짧은 product-1 문서가 점수가 더 높게 나온 것을 확인할 수 있다.
[
{
"_index": "products",
"_id": "product-1",
"_score": 1.132049,
"_source": {
"_class": "com.example.elasticsearchdemo.model.Product",
"id": "product-1",
"name": "삼성 노트북",
"description": "고성능 노트북 컴퓨터",
"price": 1200000.0,
"category": "전자제품",
"stock": 10
}
},
{
"_index": "products",
"_id": "product-3",
"_score": 0.8589227,
"_source": {
"_class": "com.example.elasticsearchdemo.model.Product",
"id": "product-4",
"name": "게이밍 노트북",
"description": "고사양 그래픽 카드가 탑재된 게이밍 노트북",
"price": 1900000.0,
"category": "전자제품",
"stock": 7
}
}
]
벡터 유사도
질의어와 문서를 벡터로 표현하고 코사인 유사도를 계산하는 방법이다.
코사인 유사도는 - 1~1 범위의 값을 가지고 1로 갈수록 유사하다.
description을 벡터로 표현하는 필드를 추가한다. 사용하는 임베딩 모델에 따라 차원수를 지정한다.
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text)
private String name;
@Field(type = FieldType.Text)
private String description;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Keyword)
private String category;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Dense_Vector, dims = 4096)
private List<Float> embedding;
}
벡터 유사도 적용
임베딩 필드 추가
문서는 160만개 정도 있었고
측정 결과 모든 문서에 임베딩 필드를 추가하는데 10일 정도 걸릴 것으로 예상되었다.
배치 프레임워크를 고민하는 과정에서
중간에 작업이 중단되어도 이미 처리한 문서는 건너뛰고
중단된 지점부터 재실행이 가능해야 했고 최종적으로 spring batch를 사용하게 되었다.
메타데이터 테이블인 BATCH_STEP_EXECUTION_CONTEXT 테이블을 통해 중단된 지점부터 재실행이 가능하다.
버전
기존에는 spring data elasticsearch는 5.2.1 버전을 사용하고 있었다.
하지만 해당 버전에서는 dense vector의 차원수가 2048까지만 지원되고 있었다.
4096 차원의 벡터를 사용해야 했기 때문에 문제가 발생했다.
아래는 spring data elasticsearch는 5.2.1 버전에서 차원수를 검증하는 부분이다.
if (type == FieldType.Dense_Vector) {
Assert.isTrue(dims >= 1 && dims <= 2048,
"Invalid required parameter! Dense_Vector value \"dims\" must be between 1 and 2048.");
}
spring data elasticsearch 5.2.7 버전 이상부터 dense vector의 차원수를
4096까지 지원하는 것을 확인했고 버전을 올려서 해결했다.
'프로젝트 > 회사' 카테고리의 다른 글
| MCP app에 대해 알아보자 (0) | 2026.08.16 |
|---|---|
| litellm 오픈소스에 기여해보기 (0) | 2026.08.02 |
| OpenClaw를 사용해서 Jira 이슈 관리하기 (0) | 2026.07.19 |