티스토리 뷰
이전 글에서 사용했던 Cosine Search는 dense vector 하나만을 이용한 방법이었다.
그래서 내가 조금 더 다양한 정보를 갖고 있을 때, 이를 전부 활용하기 어렵다는 단점이 있다.
예를 들어, 내가 알고 있는게 게시물의 제목, 내용, 작성자, 작성 시간 등 몇 가지의 정보가 있음에도
게시물의 제목만 활용할 수 있다는 것이다.
모든 내용을 한문장에 섞어서 하나의 벡터화를 할 수도 있겠지만, 이럴 경우 엄밀히 말하면 각각을 비교한 결과가 아니게 된다.
이렇게 다양한 정보를 활용하기 위해서 Elasticsearch에서는 K-NN search 기능을 제공한다.
이를 이용해 검색 기능을 Springboot로 구현해봤다.
대략적인 구현은 cosine search에서 구현한 방식을 따라가려고 한다.
그 전에 K-NN Search에 대해 짚고 넘어가자
K-NN Search이란?
CS를 공부했다면, 대략적으로 알만한 내용이다.
간단히 설명하면, 검색하고자 하는 예시와 가장 가까운 k를 뽑아주는 검색 방법이다.
여기서 예시가 2개 3개 4개... 계속 늘어나게 되면 차원수가 계속 늘어나게 되어 비교 연산에 많은 시간을 소요하게 된다.
그럴 경우, 데이터를 특정 영역을 나눈 후 계산하는 대략적인(Approixmate) K-NN 방법을 사용할 것을 권장한다.
이 글에서도 비교하려는 데이터가 많아서 대략적인 K-NN 방법을 사용했다.
하나씩 알아보자.
구현
데이터 저장
@Data
@Builder
public class KnnDataInsertVo {
private String id;
private String title;
private String content;
private String creator;
private String createdDate;
@JsonProperty("title_vector")
private float[] titleVector;
@JsonProperty("content_vector")
private float[] contentVector;
@JsonProperty("creator_vector")
private float[] creatorVector;
@JsonProperty("created_vector")
private float[] createdVector;
}
cosine search와 다르게 검색하고자하는 요소가 많기 때문에 입력데이터도 많다.
id는 단순히 저장 위치를 지정하기 위해 받는 정보이다.
임베딩 데이터를 함께 갖고 있게 했다.
데이터 저장 매서드
저장하기 전에 임베딩을 해줬는데, 이 부분은 간단하게만 첨부한다.
EmbeddingResponseDto titleResponseDto = createEmbeddingIfNotNull(confluenceKnnSearchRequestDto.getTitle());
private EmbeddingResponseDto createEmbeddingIfNotNull(String text) {
if (!ObjectUtils.isEmpty(text)) {
return openAiService.createEmbedding(text);
} else {
return null;
}
}
코사인 검색 방법과 같이 임베딩은 openAI의 API를 이용했다.
추가적으로 입력 정보가 없을 경우를 대비해 null체크를 해줬다.
다음은 데이터 저장 매서드이다.
public ResultData insertKnnSearchData(KnnDataInsertVo knnDataInsertVo) {
String json = CommonUtils.objectToJsonString(knnDataInsertVo);
String endPoint = "/knn-test";
// space 별 index 생성
if(!indexExists(endPoint)) {
// dense_vector 컬럼 생성
requestElasticSearch("PUT", "/knn-test/_mapping", ElasticSearchIndexMapping.ES_KNN_SEARCH_MAPPING);
}
String docEndPoint = "/knn-test/_doc/" + knnDataInsertVo.getId();
String responseBody = requestElasticSearch("POST", docEndPoint, json);
return CommonUtils.JsonStringToObject(responseBody, ResultData.class);
}
여기서도 검색하고자하는 벡터들을 dense_vector로 지정해야한다.
때문에 인덱스를 만들 때 maaping 정보를 심어줬다.
public static final String ES_KNN_SEARCH_MAPPING = """
{
"mappings": {
"properties": {
"title_vector": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "l2_norm"
},
"content_vector": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "l2_norm"
},
"creator_vector": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "l2_norm"
},
"created_vector": {
"type": "dense_vector",
"dims": 1536,
"index": true,
"similarity": "l2_norm"
},
"title": {
"type": "text"
},
"content": {
"type": "text"
},
"creator": {
"type": "text"
},
"createdDate": {
"type": "date"
}
}
}
}
""";
데이터 검색 매서드
데이터가 많은만큼 검색 매서드가 조금 복잡하다.
검색 데이터
@Data
@NoArgsConstructor
@AllArgsConstructor
public class KnnSearchParameterVo {
private String title;
private String content;
private String createdDate;
private String creator;
}
요청 객체
@Data
public class KnnRequestDto {
private List<KnnQuery> knn;
private int size;
@Data
public static class KnnQuery {
private String field;
@JsonProperty("query_vector")
private float[] queryVector;
private int k;
@JsonProperty("num_candidates")
private int numCandidates;
private double boost;
}
}
검색하고자 하는 데이터들을 리스트로 만들어야한다.
다음은 검색 매서드이다.
public KnnSearchResultVo searchKnnSimilarityVersion3(KnnSearchParameterVo knnSearchParameterVo) {
KnnRequestDto knnRequestDto = new KnnRequestDto();
List<KnnRequestDto.KnnQuery> knnQueryList = new ArrayList<>();
addKnnQueryIfPresent(knnQueryList, knnSearchParameterVo.getText(), EmbeddingVectors.TITLE_VECTOR.getVectorName());
addKnnQueryIfPresent(knnQueryList, knnSearchParameterVo.getText(), EmbeddingVectors.CONTENT_VECTOR.getVectorName());
addKnnQueryIfPresent(knnQueryList, knnSearchParameterVo.getCreator(), EmbeddingVectors.CREATOR_VECTOR.getVectorName());
addKnnQueryIfPresent(knnQueryList, knnSearchParameterVo.getCreatedDate(), EmbeddingVectors.CREATED_VECTOR.getVectorName());
knnRequestDto.setKnn(knnQueryList);
knnRequestDto.setSize(5);
return elasticSearchService.searchKnnSimilarity(knnRequestDto);
}
여기서 size는 검색하고자 하는 이웃 갯수를 의미한다.
요청 쿼리를 만드는 과정이 조금 손이 간다.
private void addKnnQueryIfPresent(List<KnnRequestDto.KnnQuery> knnQueryList, String text, String vectorType) {
Optional<String> optionalText = Optional.ofNullable(text);
optionalText.ifPresent(value -> knnQueryList.add(openAiService.makeKnnQuery(value, vectorType)));
}
null 체크를 하면서 진행
public KnnRequestDto.KnnQuery makeKnnQuery(String message, String field) {
KnnRequestDto.KnnQuery knnQuery = new KnnRequestDto.KnnQuery();
EmbeddingResponseDto titleResponseDto = createEmbedding(message);
knnQuery.setField(field);
knnQuery.setK(5);
knnQuery.setBoost(0.5);
knnQuery.setNumCandidates(10);
knnQuery.setQueryVector(titleResponseDto.getData()
.get(0)
.getEmbedding());
return knnQuery;
}
요청 쿼리의 세부 설정은 여기서 처리한다.
num_candidates를 검색 파라미터로 사용하면 Approximate kNN(대략적인 최근접 이웃)을 사용하게 된다.
num_candidates는 approximate하게 비교하기 위해 아래 그림과 같이 데이터를 특정 영역으로 나누게 되는데, 나눈 영역을 샤드라고 부른다.
샤드에서 num_candidates만큼의 후보를 뽑고, 샤드 별로 뽑힌 후보 벡터 중에서 조회하고자 하는 데이터에 가까운 상위 k개를 선택한다.
(샤드 수 x k개) 가 선정되면 그 중에서 다시 가장 점수가 높은 k개를 선정해 return한다.
boost는 가중치를 의미하는데 0.5는 모든 데이터를 같은 비중으로 두고 검색하겠다는 의미이다.
public KnnSearchResultVo searchKnnSimilarity(KnnRequestDto knnRequestDto) {
String json = CommonUtils.objectToJsonString(knnRequestDto);
String responseBody = requestElasticSearch("POST", "/knn-test/_search", json);
return CommonUtils.JsonStringToObject(responseBody, KnnSearchResultVo.class);
}
이렇게 만들고 요청을 하면된다.
마지막이 찝찝하게 됐는데, 이 글을 쓰기 전에 데이터를 정리하면서 knn-test 인덱스를 날려서 결과를 확인할 수 없게 됐다.
기회가 되면 추가해서 테스트 해보겠다.
마치며
다양한 정보를 활용를 활용하는 다른 검색 알고리즘이 있을 수는 있지만 내가 사용해본게 K-NN이었다.
생각보다 좋은 대략적인(Approximate) K-NN의 경우에도 정확도가 크게 떨어지지는 않았다.
(명확했던 데이터의 영향일지 모른다.)
일단 Elasticsearch에서 사용해본 것들은 다 정리해본 것 같다.
알고리즘적으로나 내부 시스템적으로나 아직도 모르는부분이 많은 것 같지만, 당장 공부하긴 힘든 부분일 것 같다.
'개발 > Elasticsearch' 카테고리의 다른 글
Elasticsearch _reindex 429 error 해결하기 (0) | 2024.03.08 |
---|---|
Elasticsearch cloud에서 검색(search) 기능 사용하기 - 1 (cosine similarity search) (0) | 2023.08.17 |
스프링부트에서 Elasticsearch Cloud 연동하기 (0) | 2023.07.26 |
- Total
- Today
- Yesterday
- AWS EC2
- Kotlin
- Spring
- GIT
- lambda
- AOP
- S3
- 람다
- serverless
- Elastic cloud
- springboot
- AWS
- java
- terraform
- ChatGPT
- EKS
- CloudFront
- cache
- docker
- 스프링부트
- OpenAI
- JWT
- 티스토리챌린지
- Log
- 오블완
- 후쿠오카
- OpenFeign
- openAI API
- elasticsearch
- MySQL
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |