💡Object field type

PUT testindex1/_doc/100
{ 
  "patients": [ 
    {"name" : "John Doe", "age" : 56, "smoker" : true},
    {"name" : "Mary Major", "age" : 85, "smoker" : false}
  ] 
}

평범한 경우라면 위와 같이 nested object가 object field type으로 자동으로 매핑될 것이다.

위 데이터가 저장되면, object는 flat 해진다.

{
    "patients.name" : ["John Doe", "Mary Major"],
    "patients.age" : [56, 85],
    "smoker" : [true, false]
}

여기서 age가 75 이상이고, 흡연자인 환자를 검색하려면 아래와 같은 쿼리를 날리게 된다.

GET testindex1/_search 
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "patients.smoker": true
          }
        },
        {
          "range": {
            "patients.age": {
              "gte": 75
            }
          }
        }
      ]
    }
  }
}

나이가 75 이상이고 흡연자인 환자는 없으므로 결과가 나오지 않는것을 기대했겠지만.. 결과는 다음과 같다.

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.3616575,
    "hits" : [
      {
        "_index" : "testindex1",
        "_type" : "_doc",
        "_id" : "100",
        "_score" : 1.3616575,
        "_source" : {
          "patients" : [
            {
              "name" : "John Doe",
              "age" : "56",
              "smoker" : true
            },
            {
              "name" : "Mary Major",
              "age" : "85",
              "smoker" : false
            }
          ]
        }
      }
    ]
  }
}

일반 object field type은 데이터가 평면화 되기 때문에, name이 John Doe이고, age가 56이고, smoker는 true이다.. 이러한 관계가 없어지게 된다.

그래서 위와 같은 일이 벌어지게 된다.

 

Elasticsearch와 Opensearch는 이를 해결하기 위해서 Nested field type을 제공한다.

 

💡Nested field type

  • Nested field type은 특별한 type이다.
  • "nested" 로 지정된 필드는, 필드 내 객체들이 각자 document가 된다.
  • 필드 내 객체 하나 하나가 document이기 때문에, 각 document들은 서로 다른 역 색인 구조를 가진다.
  • 쿼리는 nested 쿼리를 사용해야 한다.
  • 각 객체가 document 이기 때문에, 위 예시와 달리 nested field 내 document 개별 조회/수정/삭제가 가능하다.

patients의 타입을 nested 로 지정했다.

PUT testindex1
{
  "mappings" : {
    "properties": {
      "patients": { 
        "type" : "nested"
      }
    }
  }
}

object field type의 예시처럼, 아래 데이터를 삽입한다.

PUT testindex1/_doc/100
{ 
  "patients": [ 
    {"name" : "John Doe", "age" : 56, "smoker" : true},
    {"name" : "Mary Major", "age" : 85, "smoker" : false}
  ] 
}

마찬가지로 age가 75 이상인 환자또는 흡연자를 검색해본다.

nested 필드 내 객체이므로 nested 쿼리를 사용한다.

GET testindex1/_search
{
  "query": {
    "nested": {
      "path": "patients",
      "query": {
        "bool": {
          "should": [
            {
              "term": {
                "patients.smoker": true
              }
            },
            {
              "range": {
                "patients.age": {
                  "gte": 75
                }
              }
            }
          ]
        }
      }
    }
  }
}

아래가 쿼리의 결과이다.

{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

기대한것처럼, 조건에 맞는 document가 없다고 나온다.

 

📌그러나, Nested field는 주의해서 써야 한다.

Elasticsearch나 Opensearch는 Apache Lucene을 기반으로 하고 있다.

그리고 루씬에서 segment는 immutable 이다.

 

index는 하나 또는 여러개의 shard로 구성되어 있다.

shard는 여러개의 segment로 구성되어 있다. es나 os 모두 delete, update api를 제공하지만

이는 표면적으로 제거, 수정이지 물리적으로 처리된 것이 아니다.

  • document를 삭제하면, deleted 표시만 한다.
  • document를 수정(update)하면, deleted표시를 하고 새 document를 write한다.
  • 주기적으로 merge 해 새 segment로 옮기는 작업을 거치는데, 이때가 되어서야 deleted 표시된 document가 물리적으로 제거되게 된다.

위와 같은 루씬의 특성때문에, 인덱스를 자주 수정한다면 클러스터의 쓰기 대역폭을 잡아먹어 전체적으로 낮은 인덱싱 성능(throughput)을 초래하게 된다.

이러한 문제는 검색 성능에도 지장을 주게 된다. 일정 크기 이상의 segment는 고유 query cache를 가지고 있는데, segment를 불필요하게 자주 merge하게 되면 문서의 쿼리 캐시가 사라져, Query cache cold miss가 발생하고 이는 검색 쿼리의 높은 지연시간 (tail latency) 를 유발한다.

 

이야기가 많이 벗어났는데,

nested field 타입을 사용하면 nested 객체를 수정하지 않아도, 상위 document만 수정해도 포함된 nested document들을 모두 삭제하고 새로 쓰는 작업을 하게 된다.

불필요한 삭제-쓰기가 잦아지면 성능상 좋지 않으므로, 잘 생각하고 사용하는것이 좋다.

 

 

참고자료