Team과 Member의 관계가 있다고 가정해보자.
Team을 검색할 때 Member에 대한 정보를 같이 나오게 하려면 어떻게 해야할까?
좀 더 복잡한 엔티티를 구성하기 전에 ElasticSearch에서 다루는 Nested 타입과 Object 타입의 차이를 확인하자.
추가 참고 : https://esbook.kimjmin.net/07-settings-and-mappings/7.2-mappings/7.2.5-object-nested
둘의 차이를 비교하기 위해서 Document로 Team과 Member를 담을 것이다.
구성과 데이터는 다음과 같다.
Team: ID, 이름, 구성멤버(
List<Member>
)
Member: ID, 이름, 역할, 나이
object 필드로 Team에 Member 필드를 넣어주었다.
인덱스 명은 적당한 이름으로 설정해주면 된다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = Indices.TEAM_OBJ_INDEX)
public class TeamObj {
@Id
private long id;
private String name;
@Field(type = FieldType.Object) // 중요
private List<MemberObj> members;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberObj {
private long id;
private String name;
private String role;
private int age;
}
members
필드가 Nested
라는 점을 제외하고 Object 구성이 같다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = Indices.TEAM_NESTED_INDEX)
public class TeamNested {
...
@Field(type = FieldType.Nested) // 중요
private List<MemberNested> members;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberNested {
...
}
이제 다음 데이터를 두 인덱스에 넣고 검색을 진행해보자.
- 회계팀 : 김현우 매니저(54세)
- 개발팀 : 김진수 매니저(42세), 김현우 엔지니어(24세)
- 인사팀 : 김민수 매니저(57세)
나는 김현우
라는 이름의 매니저
가 속한 팀의 정보를 알고 싶다.
즉 현재 데이터에서 회계팀
에 대한 정보를 찾고 싶은 것이다.
이 쿼리를 위해서 다음과 같이 두 Repository를 구성했다.
// 저의 환경은 java 17이기 때문에 """를 사용할 수 있습니다.
@Repository
public interface TeamObjSearchRepository extends ElasticsearchRepository<TeamObj, Long> {
@Query("""
{
"bool": {
"must": [
{"match": {"members.name": "?0"}},
{"match": {"members.role": "?1"}}
]
}
}
""")
List<TeamObj> searchTeam(String name, String role);
}
@Repository
public interface TeamNestedSearchRepository extends ElasticsearchRepository<TeamNested, Long> {
@Query("""
{
"bool": {
"must": [
{"match": {"members.name": "?0"}},
{"match": {"members.role": "?1"}}
]
}
}
""")
List<TeamNested> searchTeam(String name, String role);
}
결과는 다음과 같다.
TeamObj 결과
TeamObj(id=1, name=회계팀, members=[(id=1, name=김현우, role=MANAGER, age=54)])
TeamObj(id=2, name=개발팀, members=[..., (id=3, name=김현우, role=ENGINEER, age=24)])
TeamNested 결과
없음.
두 결과 다 원한 결과가 아니다.
TeamObj의 경우 역인덱스 관점에서 바라봐야 한다.
정말 간단히 말하면 문서에 있는 단어들을 토큰화하고 (ex - 저는 김현우 입니다. -> 저는/김현우/입니다)
문서의 ID를 기록해두어서 빠르게 원하는 단어를 검색해서 찾을 수 있도록 만든 것이다.
역인덱스에 관한 설명이 부족하다고 느낀다면 다음 글을 보고 오자.
https://esbook.kimjmin.net/06-text-analysis/6.1-indexing-data
다시 Team-Member 관계로 돌아와보자.
김현우
라는 데이터는 어느 문서에 있을까?
동명이인인 김현우는 회계팀
, 개발팀
에 소속돼 있다.
그렇다면 MANAGER
는 어느 문서에 있을까?
MANAGER
가 있는 문서는 회계팀
, 개발팀
, 인사팀
에 모두 있다.
단어 ID 김현우 1(회계팀), 2(개발팀) MANAGER 1(회계팀), 2(개발팀), 3(인사팀) ... ...
이 둘의 교집합은 회계팀
, 개발팀
으로 두가지 팀이 결과로 나오게 되는 것이다.
Nested 필드를 검색할 때는 Object와 다르게 nested
라는 쿼리를 사용해야 한다.
nested 쿼리 안에는 path 라는 옵션으로 nested로 정의된 필드를 먼저 명시하고 그 안에 다시 쿼리를 넣으면 된다.
Nested Repository에 다음 쿼리를 추가해주자.
@Query("""
{
"nested": {
"path": "members",
"query": {
"bool": {
"must": [
{"match": {"members.name": "?0"}},
{"match": {"members.role": "?1"}}
]
}
}
}
}
""")
List<TeamNested> searchNested(String name, String role);
이제 정상적인 결과가 나오는 것을 확인할 수 있다.
TeamNested(id=1, name=회계팀, members=[(id=1, name=김현우, role=MANAGER, age=54)])
Object 필드의 경우 하위 문서에 대한 복잡한 쿼리가 없을 경우 사용하면 된다.
Nested 필드의 경우 하위 문서에 대한 복잡한 쿼리를 사용할 필요가 있을 때 사용하면 된다.
Object 필드는 하나의 문서에 모든 내용이 같이 저장되는 것이고,
Nested 필드의 경우 숨겨진(hidden) 하위 문서가 생기고 상위 문서와 하위 문서가 각자의 역인덱스를 가지게 되는 것이다.
공식문서 : https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html
https://techblog.woowahan.com/7425/
이 기술블로그의 글에서 운영 환경에서 잦은 부모 문서 업데이트로 발생하는 nested 문서의 다시 쓰기로 인한 처리 병목을 파악했다.
병목을 해결하기 위해서 nested 문서를 document로 분리하고 단계별로 쿼리를 남기는 식으로 처리했다.
그리고 무조건 nested를 쓰기보다는 복합적으로 파악해보고, document를 아예 분리하고
단계별로 쿼리를 날려서 가져오는 방식이 더 효율적일 수 있다는 점을 말해주고 있다.