Nori to Seunjeon
{
"analysis": {
"analyzer": {
"facilities_analyzer": {
"char_filter": [],
"tokenizer": "nori_tokenizer",
"filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
}
}
}
}💡
nori_analyzer 구성을 직접 명시한 모습
엘라스틱서치에서 한국어 형태소 분석기를 nori_analyzer 로 사용했습니다.

GET _cat/plugins?v
그런데 analysis-nori 플러그인은 존재하지 않았고 대신에 analysis-seunjeon 로 사용해야합니다. (은전한닢)
{
"analysis": {
"tokenizer": {
"korean_seunjeon_tokenizer": {
"type": "seunjeon_tokenizer",
"index_eojeol": false,
"pos_tagging": false
}
},
"analyzer": {
"facilities_analyzer": {
"type": "custom",
"tokenizer": "korean_seunjeon_tokenizer",
"filter": [
"lowercase"
]
}
}
}
}"index_eojeol": false: 어절(띄어쓰기 단위) 그대로 저장하지 않고, 형태소 단위로 쪼개서 저장(검색에 유리)- 플러그인 자체에서 조사/어미 등을 분리해서 처리하므로 별도의 pos 필터 제외
💡
참고로 nori 와 은전한닢은 같은 사전으로 학습한 형태소 분석기이나 nori 가 훨씬 가볍다.
당장은 용량에 관한 걱정이 없으니 nori 를 설치하는 대신에 은전한닢으로 교체한 모습입니다.
모든것을 바꿔야하나?
AWS opensearch 는 엘라스틱서치가 오픈소스로 푼 7버전(?)을 개량해서 자체적으로 개발한버전입니다.
따라서 사용할 라이브러리부터 다릅니다.
import co.elastic.clients.elasticsearch._types.GeoDistanceType;
import co.elastic.clients.elasticsearch._types.GeoLocation;
import co.elastic.clients.elasticsearch._types.LatLonGeoLocation;
import co.elastic.clients.elasticsearch._types.query_dsl.*;그런데 opensearch 사용법을 찾아보는데 자료가 너무 부족했고 각 의존성이 opensearch에서는 어떻게 사용해야할지 막막했습니다.
이거저거 찾다가 AWS CloudSearch 에 대해서도 알게되었는데 버전이 낮은대신 aws가 자체적으로 간리하는 친구입니다. 해당 서비스에 내 데이터를 형식에 맞게 세팅해서 넣고 rest api 로 통신이 가능했습니다. 다만 비용이 조금 비싸다고 알고있어서 비용 때문에 바꾸려고하는 제 상황과는 맞지 않았습니다.
생각해보기
CloudSearch 가 Rest API 로 통신하는구나… 맞다 생각해보니 어플리케이션을 실행하면, 엘라스틱서치와 연결이 제대로 되었나 ping pong 하는 부분이 있습니다.

실제로 https 로 요청을 보냈다는 로그가 찍혔습니다. 그렇다면 이 RestClient의 빈을 재정의해서 opensearch 로 우회하는 방식으로 사용할 수 있지 않을까?


일단 opensearch 도메인을 생성할 때 마스터 사용자의 이름과 암호를 입력하는걸로 봐서는 디폴트로 HTTP 기본 인증을 사용하는 것 같습니다.

세분화된 엑세스 제어 항목에서 다른 인증항목들이 있는걸로 보아 더욱 확신됩니다.
코드
public ElasticsearchClient elasticsearchClient(ObjectMapper objectMapper) {
// 2. 인증 설정 (Basic Auth)
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));
// 3. RestClient 빌더 생성
RestClient restClient = RestClient.builder(new HttpHost(uris, 443, "https"))
.setDefaultHeaders(new Header[]{
new BasicHeader("Content-Type", "application/json")
})
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
.setDefaultCredentialsProvider(credentialsProvider)
// ★ 핵심: AWS OpenSearch가 안 보내주는 헤더를 강제로 주입하여 속임
.addInterceptorLast((HttpResponseInterceptor) (response, context) ->
response.addHeader("X-Elastic-Product", "Elasticsearch"))
)
.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
.setConnectTimeout(5000)
.setSocketTimeout(120000)
)
.build();
// 4. Transport 생성
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper(objectMapper));
// 5. Client 리턴 (기존 코드에서 쓰던 그 클라이언트 객체)
return new ElasticsearchClient(transport);
}인증 설정
CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));저수준 HTTP 통신을 담당하는 Apache HttpClient 라이브러리의 표준적인 인증 처리 방식에 기반합니다.
HTTP 요청 헤더에 Authorization: Basic [Base64 인코딩된 ID:PW]를 넣기 위한 표준적인 준비 과정
| 클래스 | 역할 | 설명 |
|---|---|---|
UsernamePasswordCredentials |
인증 정보 저장소 | 말 그대로 사용자 이름(username)과 비밀번호(password) 쌍을 저장하는 객체입니다. HTTP Basic 인증의 원료입니다. |
BasicCredentialsProvider |
인증 정보 제공자 | UsernamePasswordCredentials를 받아서 저장하고 관리합니다. 이 객체는 RestClient가 요청을 보낼 때 "요청에 필요한 인증 정보를 제공해주는 역할"을 수행합니다. |
AuthScope |
인증 범위 지정 | 이 자격 증명(ID/PW)이 어떤 요청에 유효한지 그 범위를 지정합니다. AuthScope.ANY는 "어떤 호스트, 어떤 포트, 어떤 프로토콜에도 이 ID/PW를 사용하라"는 의미로 가장 넓은 범위를 지정합니다. |
RestClient 생성
.setDefaultHeaders(new Header[]{
new BasicHeader("Content-Type", "application/json")
})헤더에 application/json 을 명시해줘야합니다.
Caused by: org.elasticsearch.client.ResponseException: method [PUT], host [https://search-gh-opensearch-e7rsx6oyqucfsztn7uwlrbz6sm.ap-northeast-2.es.amazonaws.com:443], URI [/facilities], status line [HTTP/1.1 406 Not Acceptable]
{"error":"Content-Type header [application/vnd.elasticsearch+json; compatible-with=8] is not supported","status":406}최종적으로 ElasticsearchClient 를 생성해서 통신하는데, 이 친구는 application/vnd.elasticsearch+json 헤더를 통해 통신하기 때문입니다.
- AWS OpenSearch는 Elasticsearch 7.10 버전이라서, 최신 8.x 전용 헤더를 이해하지 못하기 때문입니다.
.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
.setDefaultCredentialsProvider(credentialsProvider)
)인증 주입: credentialsProvider에 저장된 사용자 이름과 비밀번호를 HTTP 클라이언트에 주입하여, 모든 요청에 Basic Authentication 헤더(Authorization: Basic ...)가 자동으로 포함되도록 설정합니다.
.addInterceptorLast((HttpResponseInterceptor) (response, context) ->
response.addHeader("X-Elastic-Product", "Elasticsearch"))해당부분을 추가하지 않으면
Caused by: co.elastic.clients.transport.TransportException: node:
https://search-gh-opensearch-e7rsx6oyqucfsztn7uwlrbz6sm.ap-northeast-2.es.amazonaws.com:443/,
status: 200, [es/indices.exists] Missing [X-Elastic-Product] header.
Please check that you are connecting to an Elasticsearch instance,
and that any networking filters are preserving that header.이런 오류가 발생한데 오류내용으로는 X-Elastic-Product 헤더가 없다는 내용이다. contentType과 달리 다른 메서드에 추가를 해줘야하는데 이는 헤더의 역할과 HTTP 통신 과정 중 헤더를 주입하는 시점이 다르기 때문이다.
💡
setDefaultHeaders
- 헤더 타입: 요청 헤더 (Request Header)
- 추가 시점: HTTP 요청(Request)이 OpenSearch 서버로 나가기 직전.
addInterceptorLast
- 헤더 타입: 응답 헤더 (Response Header)를 조작
- 추가 시점: 서버로부터 HTTP 응답(Response)이 클라이언트(Java 애플리케이션)에 도착한 직후
ElasticSearch 와 다르게 OpenSearch 는 응답시 위 헤더를 안주기 때문에 라이브러리 입장에서는 우리 ElasticSearch에서 준게 아닌데? 라고 생각해서 연결을 끊어버립니다.
.setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder
.setConnectTimeout(5000)
.setSocketTimeout(120000)
)스프링 배치로 데이터를 적재하는 과정에서 타임아웃이 발생하여 시간을 넉넉하게 확보한 모습입니다.
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper(objectMapper));transport : 통역사
RestClient와 Mapper 를 하나로 묶어주는 코드입니다.
(objectMapper를 넘겨주는 이유는, 스프링 부트가 이미 가지고 있는 JSON 설정(날짜 포맷, 스네이크 케이스 등)을 그대로 따르기 위해서)
현재 FacilityDocument 로 그대로 DB에 넣듯이 쓰기 위해서 필요한 과정입니다. 이게 없으면 모든 데이터를
직접 JSON 문자열("{\"name\":...}")로 짜서 보내야 합니다.
법적인 문제는 없을까?
| 주체 | 고려 사항 | 결론 (사용자님의 책임) |
|---|---|---|
| 서버 (AWS OpenSearch) | 이 서버를 사용할 수 있는 라이선스가 있는가? | YES. Apache 2.0 라이선스를 기반으로 하므로 자유롭게 사용 가능합니다. |
| 라이브러리 (Elastic Client) | 이 라이브러리의 사용 조건을 위반했는가? | NO. 라이브러리의 라이선스(ELv2)는 사용자가 돈을 내는 서비스를 만들 때 적용되는 경우가 많으며, 연결 제한은 라이선스 위반이 아닌 기술적 제한입니다. |
제미나이 피셜 법적 판단 여부는 연결하는 쪽에서만 고려하면 된다고합니다.
추후에 연결오류가 생긴다면 그때가서 opensearch-java 기반으로 전환하든 고려하면 될 것 같습니다.
신경 쓸 포인트
결국에는 사용환경에 맞지않는 라이브러리를 사용하므로 비정상적으로 동작하지 않는것을 항상 고려해야합니다.
elasticsearchClient.search(s->s.knn())
Failed to parse list: expecting START_ARRAY but got VALUE_STRING예를들어 knn 메서드를 사용하면 오류가 발생합니다. 에러로그를 살펴보면 Float 형이 직렬화되면서 문자열로 변경되는 듯 합니다.
SearchResponse<Map> response = elasticsearchClient.search(s -> s
.index("item_recommendation_index")
.size(5)
.query(q -> q
._custom("knn", knnField)
), Map.class
);해당 문제는 _custom 메서드를 사용하여 'knn'이라는 이름의 쿼리로 전달하는 방식으로 해결했습니다.
'기타' 카테고리의 다른 글
| Opensearch 데이터/설정/인덱스 초기화 이슈 (0) | 2026.02.26 |
|---|---|
| AWS Cognito 로 OpenSearch 대시보드 접근하기 (0) | 2026.02.12 |