Study/elasticsearch

elasticsearch join type field

voider 2023. 8. 5. 18:23

엘라스틱서치 조인 가이드 문서 링크

https://www.elastic.co/guide/en/elasticsearch/reference/current/parent-join.html#parent-join

엘라스틱서치는 Parent-join이라는 것을 제공함.

The join data type is a special field that creates parent/child relation within documents of the same index. The relations section defines a set of possible relations within the documents, each relation being a parent name and a child name.

`join` 타입은 동일한 인덱스 내에서 parent-child 관계를 만드는 특별한 필드다. `relations` 섹션은 문서 내에서 가능한 관계 집합을 정의한다. 각 릴레이션의 관계는 parent 이름과 child 이름으로 구성된다.

→ Elasticsearch에서 Join은 같은 문서 내에서만 가능함. 7.x 버전부터 하나의 인덱스에 다양한 문서를 저장할 수 없으므로, 같은 문서 내에서 부모-자식 관계를 만들어서 조인을 할 수 있다는 것으로 보임.

We don’t recommend using multiple levels of relations to replicate a relational model. Each level of relation adds an overhead at query time in terms of memory and computation. For better search performance, denormalize your data instead.

관계형 모델을 복제하기 위해 여러 수준의 릴레이션을 사용하지 않는 것을 권장함. 릴레이션은 아무튼 오버헤드가 있음. 더 나은 퍼포먼스를 얻고 싶으면 디노멀라이즈 하라.

→ 그나마도 권장하지는 않음. 반드시 필요한 경우가 아니라면 비정규화 하라고 말하고 있음.

조인을 사용하려면 문서를 인덱싱할 때 릴레이션의 이름과 도큐먼트의 optional parent `source`에 제공해야 함. 예를 들어, 두 개의 `question` 도큐먼트를 만드려면 위와 같은 쿼리를 해야 함.

→ 같은 문서 내에서 조인을 하려면 색인할 때 릴레이션의 이름을 지정해야 하는 것 같음. `my_join_field`에 이것이 어떤 역할인지(parent or child), 또한 (선택적으로) 부모가 무엇인지도 `source`에 전달해야 하는 것 같음?

Parent-Join and performance

join 필드는 RDB처럼 쓰면 안 된다. Elasticsearch 성능의 핵심은 데이터를 비정규화 하는 것. 각 조인 필드 `has_child` 또는 `has_parent` 쿼리는 성능에 TAX를 부과함.

`join` 필드가 의미있는 경우는 데이터에 한 엔티티가 다른 엔티티보다 훨씬 많은 one-to-many relationship이 포함된 경우.

예를 들어, products와 offers. 이 경우 offers가 products보다 훨씬 많은 경우에는 product를 상위 문서로, offer를 하위 문서로 모델링하는 게 좋다.

→ Product와 Offer는 다른 도큐먼트인 것 같은데 어떻게 조인한다는 것인지?

Parent-Join restrictions

  • 인덱스 당 하나의 조인 필드 매핑만 허용함
  • parent와 child 도큐먼트는 반드시 같은 샤드 내에 인덱싱 되어야 함. 왜냐하면 하위 문서를 getting, deleting, updating 할 때 같은 `routing` 값을 지정해야 함.
  • 엘리먼트는 여러 자식을 가질 수 있지만 부모는 오직 하나여야 함.
  • 기존 `join` 필드에 새 릴레이션을 추가하는 게 가능함
  • 또한 기존 요소에 자식을 추가하는 것도 가능함. 하지만 해당 요소가 이미 부모인 경우에만 가능.

 

Post Document와 Comment Document를 Parent-Child 관계를 맺어서 조인하여 사용할 수 있는지 검토하며 공부한 내용

Elasticsearch는 Join type이라는 특별한 타입을 정의한다. 이 필드를 이용해서 parent-child라는 relations를 지정해서 조인할 수 있다.

  • has_child, has_parent 쿼리
    1. has_child: 지정한 child document를 포함하는 parent document 조회
    2. has_parent: 지정한 parent를 부모로 가지는 child document 조회

주의할 점은 엘라스틱서치는 서로 다른 인덱스 간 조인을 지원하지 않는다. 주로 계층형 데이터이면서 1:N 관계일 때 사용되며, RDB의 self-join과 비슷하다. 웬만하면 비정규화를 사용하도록 권장하고 있다. 문서에서도 has_child, has_parent 쿼리는 성능에 tax를 부과하는 것이며, 정말 필요한 경우가 아니라면 비정규화 하라고 한다.

parent - child relations를 만들 때 몇 가지 제약사항도 존재한다.

  • 인덱스 당 하나의 조인 필드 매핑만 허용함
  • parent와 child 도큐먼트는 반드시 같은 샤드 내에 인덱싱 되어야 함. 왜냐하면 하위 문서를 getting, deleting, updating 할 때 같은 routing 값을 지정해야 함.
  • 엘리먼트는 여러 자식을 가질 수 있지만 부모는 오직 하나여야 함.
  • 기존 join 필드에 새 릴레이션을 추가하는 게 가능함
  • 또한 기존 요소에 자식을 추가하는 것도 가능함. 하지만 해당 요소가 이미 부모인 경우에만 가능.

사용을 검토하며 테스트 해본 내용

# Parent Join(has-child) Example

# create index
PUT /companies
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "employees": {
        "type": "join",
        "relations": {
          "company": "employee"
        }
      }
    }
  }
}

# index parent document
POST /companies/_doc/1
{
  "name": "Acme Corp",
  "employees": "company"
}

POST /companies/_doc/2
{
  "name": "XYZ Corp",
  "employees": "company"
}

# index child document
POST /companies/_doc/3?routing=1
{
  "name": "John Doe",
  "employees": {
    "name": "employee",
    "parent": 1
  }
}

POST /companies/_doc/4?routing=1
{
  "name": "Jane Smith",
  "employees": {
    "name": "employee",
    "parent": 1
  }
}

POST /companies/_doc/5?routing=2
{
  "name": "Bob Johnson",
  "employees": {
    "name": "employee",
    "parent": 2
  }
}

위와 같이 테스트 데이터를 만들어 둔 뒤, 조인 쿼리를 날려봄

# join query: Acme Corp에 다니는 employees를 조회
GET /companies/_search
{
  "query": {
    "has_parent": {
      "parent_type": "company",
      "query": {
        "match": {
          "name": "Acme Corp"
        }
      }
    }
  }
}

# 결과
    "hits": [
      {
        "_index": "companies",
        "_id": "3",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "name": "John Doe",
          "employees": {
            "name": "employee",
            "parent": 1
          }
        }
      },
      {
        "_index": "companies",
        "_id": "4",
        "_score": 1,
        "_routing": "1",
        "_source": {
          "name": "Jane Smith",
          "employees": {
            "name": "employee",
            "parent": 1
          }
        }
      }
    ]

한 인덱스 내에서 성격이 다른 두 가지 문서를 운용하는 것이 부적합하다고 판단했기 때문에 사용하지 않기로 결정함. 한 document를 comment로도 사용하고 post로도 사용할 때 문제 생길 여지 많음.

Nested Query

대안으로 nested 쿼리를 사용할 수도 있다. nested는 post의 프로퍼티로 nested 타입을 지정한 뒤, 그 안에 comments 프로퍼티를 저장하는 방식이다.

# parent join(nested) example

# create index
PUT /blog_posts
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      },
      "content": {
        "type": "text"
      },
      "comments": {
        "type": "nested"
      }
    }
  }
}

# index parent document
POST /blog_posts/_doc/1
{
  "title": "엘라스틱서치를 소개합니다",
  "content": "엘라스틱서치는 파워풀한 분산 검색 엔진입니다",
  "comments": []
}

POST /blog_posts/_doc/2
{
  "title": "키바나 시작하기",
  "content": "키바나는 데이터 비주얼라이제이션과 탐색 툴입니다.",
  "comments": []
}

# index child document
PUT /blog_posts/_doc/1
{
  "title": "엘라스틱서치를 소개합니다",
  "content": "엘라스틱서치는 파워풀한 분산 검색 엔진입니다",
  "comments": [
    {
      "text": "Great introduction to Elasticsearch!"
    },
    {
      "text": "Looking forward to learning more about Elasticsearch."
    }
  ]
}


PUT /blog_posts/_doc/2
{
  "title": "키바나 시작하기",
  "content": "키바나는 데이터 비주얼라이제이션과 탐색 툴입니다.",
  "comments": [
    {
      "text": "Kibana's visualization features are impressive!"
    }
  ]
}

# nested query
GET /blog_posts/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "nested": {
            "path": "comments",
            "query": {
              "match": {
                "comments.text": "Elasticsearch"
              }
            }
          }
        }
      ]
    }
  }
}