본문 바로가기
TIL/엘라스틱 서치

Elastic Search : Complete Guide to ElasticSearch - 관계형처럼 조인하기

by yeon_zoo 2024. 2. 5.


[조인 쿼리]

  • RDB보다는 몽고DB(조인을 아예 하지 않거나, 애플리케이션 수준에서 진행)같은 noSQL에서의 조인과 가깝다
  • 비정규화는 데이터 저장소를 더 많이 먹지 않는가?
    • ㅇㅇ 근데 es는 메인 저장소로 쓰지 않는 것을 추천하기 때문에 ㄱㅊ (검색만 빠르면 괜찮다 주의)
  • es에서 간단한 조인 기능들을 제공하기는 하지만, 비효율적이기 때문에 많은 도큐먼트를 다룰 때는 좋지 않다
  • mapping 옵션에서 join property를 줄 수 있다.
  • 아래 예시를 참고. (이때 department가 꼭 조인하는 인덱스의 명칭과 똑같을 필요는 없다.)
PUT /department
{
  "mappings": {
    "properties": {
      "join_field": {
        "type": "join",
        "relations": {
          "department": "employee"
        }
      }
    }
  }
}
  • 구조는 department 가 employee의 부모가 되는 형식이다. (하나의 department : 여러 employee)
  • department에 도큐먼트를 추가하고 싶을 때의 예제
    • 조인 필드를 사용해 관계를 위한 문서를 추가할 땐 해당 문서가 어떤 관계의 일부분인지를 명시해야 한다.
PUT /department2/_doc/1
{
  "name": "Development",
  "join_field": "department"
}
  • employee 인덱스에 도큐먼트를 추가하고 싶을 때의 예제
PUT /department2/_doc/3?routing=1  // 부모 문서의 라우팅 값이 꼭 들어가야 한다 -> 부모& 자식이 같은 샤드 내에 저장되어야 하기 때문에
{
  "name": "Yeonju Lee",
  "age": 25,
  "gender": "F",
  "join_field": {
    "name": "employee", // 이 문서가 employee의 문서라는 것 
    "parent": 1 
  }
}
  • 부모 도큐먼트 id에 기반해서 자식 도큐먼트를 반환하는 쿼리
GET /department2/_search
{
  "query": {
    "parent_id": {
      "type": "employee", -- 검색 결과의 타입
      "id": 1 -- 검색 대상이 되는 부모 문서(여기서는 department)의 id 값
    }
  }
}
  • 부모 도큐먼트의 ID를 모르거나 도큐먼트 id 외에 다른 조건을 추가하길 원할 때 사용하는 쿼리
    • 부모 도큐먼트가 기준에 부합하는 경우 쿼리가 자녀 도큐먼트를 반환
GET /department2/_search
{
  "query": {
    "has_parent": {
      "parent_type": "department",
      "query": {
        "term": {
          "name.keyword": "Development" 
        }
      }
    }
  }
}
  • 기본적으로 쿼리는 일치하는 부모 도큐먼트의 관계도 점수를 무시한다. (= 일치하는 부모의 도큐먼트 관계도 점수가 자녀 도큐먼트의 관계도 점수와 무관하다.)
    • 관계도 점수는 전부 1로 표기되는데, 이는 "score": true로 두면 관계도가 증가한다.
    • 항상 하나의 부모 도큐먼트만 검색되는 것은 아니다. 용어 쿼리 등을 이용해서 매칭되는 도큐먼트가 여러 개 나올 수 있음.
      • 이 때 부모 도큐먼트가 얼마나 쿼리에 매칭되는지가 중요한 척도가 될 수 있다.
  • 반대로 has_child 쿼리를 이용하면 조건에 맞는 자식 도큐먼트를 가지고 있는 부모 도큐먼트를 찾을 수도 있다.
  • 관계도 점수에 따른 조건을 추가할 수도 있다.
    • min : 자식 도큐먼트들의 점수 중 가장 낮은 점수가 매핑됨
    • max : 자식 도큐먼트들의 점수 중 가장 높은 점수가 매핑됨
    • sum : 조건에 부합한 자식의 점수가 총합되어 매핑됨
    • avg : 조건에 부합한 자식의 평균 점수가 매핑됨
    • none : 자식 도큐먼트의 점수는 무시된다. (디폴트 설정)
  • 갖고 있는 자식 문서의 수에 따른 조건을 추가할 수도 있다.
    • min_children : 2 => 최소 두 개의 자식 도큐먼트를 가지고 있는 경우만 부모 도큐먼트를 반환
    • max_children : 5 => 최대 다섯 개의 자식 도큐먼트를 가지고 있는 경우만 부모 도큐먼트를 반환
  • 정렬을 위해서 function_score 라는 파라미터를 쓸 수도 있다.
GET /_search
{
  "query": {
    "has_child": {
      "type": "child",
      "query": {
        "function_score": {
          "script_score": {
            "script": "_score * doc['click_count'].value"
          }
        }
      },
      "score_mode": "max"
    }
  }
}

 

[멀티 레벨 관계]

  • 한 관계가 여러 레벨의 관계 타입을 가지고 있는 것
  • 아래 예시는 Employee > Department > Company | Supplier > Company 의 관계 구조를 가지고 있음
PUT /company
{
	"mappings": {
		"_doc": {
			"properties": {
				"join_field": {
					"type": "join",
					"relations": {
						"company": ["department", "supplier"],
						"department": "employee"
					}
				}
			}
		}
	}
}
  • relations 객체에 지금 존재하는 관계 타입의 이름을 적어서 관계 구조를 명시
  • 위의 예시에서 도큐먼트를 인덱싱하는 방법
PUT /company/_doc/2
{
	"name": "MIRI.DIH",
	"join_field": "company"
}

PUT /company/_doc/2?routing=1
{
	"name": "Development",
	"join_field": {
		"name": "department",
		"parent": 1
	}
}

PUT /company/_doc/3?routing=1
{
	"name": "Bo Andersen",
	"join_field": {
		"name": "employee",
		"parent": 2
	}
}
  • 도큐먼트 구조 상 가장 상위 root와 동일한 샤드에 위치하기 위해서 routing을 이용하게 된다. (모든 하위 도큐먼트는 상위 도큐먼트와 같은 샤드에 위치해 있어야 함)
  • 멀티 레벨 관계에서의 쿼리 (한 단계 조인과 동일)
    • has_parent
    • has_children
    • parent_id
GET /company/_search
{
	"query": {
		"has_child": {
			"type": "department",
			"query": { // 쿼리 조건에 부합하는 department를 찾음
				"has_child": { 
					"type": "employee",
					"query": { // John Doe라는 이름을 가진 직원을 찾음
						"term": {
							"name.keyword": "John Doe"
						}
					}
				}
			}
		}
	}
}
  • inner_hit은 query 필드에도 사용될 수 있다.
    • "inner_hit" : {}를 추가
    • 어떤 직원이 부서가 쿼리 조건에 부합하는 것에 영향을 미쳤는지 알 수 있음

 

[terms lookup]

  • 쿼리에 조회 대상이 되는 용어가 여러 개 (몇 백개 이상) 될 수도 있다.
    • 이 경우를 쿼리에 적기에는 쿼리가 너무 커지고 가독성이 떨어진다.
    • => terms lookup mechanism 을 제공
  • 용어를 검색하는 대상 인덱스 타입과 도큐먼트의 id를 명시하도록 한다
  • 용어가 저장된 필드도 명확하게 알아야 한다.
  • 맑은 정신일 때 다시 봐야겠다.
  • es의 메모리와 계산 파워는 용어의 수에 비례해서 많이 필요하기 때문에 클러스터의 안정성 면에서 많은 용어들을 쿼리하는 것은 피하는 것이 좋다.
  • 각 인덱스 당 최대 65000 용어만 쿼리가 가능하다.

 

[조인의 한계점]

  • 모든 조인 관계는 같은 인덱스 내에 저장되어야 한다.
    • 성능을 위한 제한임. 다른 인덱스랑 조인하려고 하면 굉장히 느려질 것
  • root 부모 ~ leaf 자식까지는 다 같은 샤드 안에 있어야 한다.
  • 인덱스 당 딱 하나의 조인 필드만 가질 수 있다. 하지만 조인 필드에 대해 원하는 만큼의 도큐먼트 관계를 매핑할 수 있고 기존 조인 필드에 항상 새로운 관계를 추가할 수 있기 때문에 큰 문제는 아니다.
  • 자식 관계는 존재하는 부모에 대해서만 추가될 수 있다.
  • 한 문서는 하나의 부모만 가진다. (e.g. 하나의 employee는 하나의 department 만 가질 수 있음 / 하나의 department는 여러 employee O)

 

[조인 필드 성능 고려지점]

  • 가능하면 조인을 쓰지 말아라. (성능적으로 안 좋음)
  • 조인 필드를 쓰는 인덱스에 도큐먼트를 추가할 수록 has_child, has_parent 쿼리는 느려진다.
  • 멀티레벨 관계를 사용하면 각 레벨마다 오버헤드가 늘어난다. 따라서 진짜 웬만하면 쓰지 마라.
    • 그럼에도 불구하고 조인 필드를 쓰는 게 적절하고 성능도 나쁘지 않을 때가 있다.
      • 일대다 관계를 가지고, 한 타입이 다른 타입에 비해 훨씬 그 수가 많은 경우
        e.g. 부모 도큐먼트 = 레시피, 자식 도큐먼트 = 재료
  • 그럼에도.. 왜 조인을 사용하게 되는가
    • 도큐먼트의 수가 한정적이고 많이 변하지 않는 경우 : O
  • 조인이 없으면 관계를 어떻게 표현해야 하는가
    • nested data type 쓰기
      • 여의치 않을 수 있음.. 필요한 내용과 다르거나..
    • 일반적으로는.. 관계를 표현하지 마라
      • es는 RDB랑 다르다. 다른 구조다. 그러니 es의 구조를 따라라.
  • 조인을 써야하는 특정 이유가 있는 게 아니라면
    • 데이터를 비정규화 먼저 해보고 그게 소용 없을 join 필드를 사용해라

댓글