본문 바로가기

DevOps/Elastic Stack

[Elasticsearch] Elasticsearch를 활용한 특정 행정구역 내 데이터 집계하기 (feat. java) - 컴도리돌이

728x90
728x90

 

[PostgreSQL] PostGIS 설치 및 사용 - 컴도리돌이

주어진 좌표가 어떤 행정구역에 포함되는지 알아야 하는 상황이 생겼습니다. 여러 방법이 있지만 그중에서 PostgreSQL에 있는 확장 라이브러리인 PostGIS가 속도 및 정확성에 성능이 매우 뛰어난다

comdolidol-i.tistory.com

 

[PostgreSQL] PostGIS 성능 비교: GEOMETRY vs TEXT 저장 방식, 얼마나 차이 날까? - 컴도리돌이

회사에서 PostGIS 도입을 고민하면서, 어떤 이슈가 발생할지 모르기 때문에 행정구역 경계 값을 TEXT로 처리하여 JSON 형태로 변환 후 Elasticsearch의 geo-bounding box query로 조회하는 방안을 계획했습니다

comdolidol-i.tistory.com

 

운영 데이터에서 특정 행정구역의 경계 내에 몇 개의 데이터가 존재하는지 확인해야 하는 경우가 많습니다. 이를 위해 PostgreSQL의 확장 기능인 PostGIS를 고려했지만, 실제 운영 환경에서 발생할 수 있는 성능 이슈를 예측하기 어려웠습니다. 특히 대량의 공간 데이터를 실시간으로 조회할 때, RDBMS보다 Elasticsearch가 더 빠르고 효율적일 것으로 판단하여 이를 활용하기로 했습니다. 이번 포스팅에서는 Elasticsearch를 이용하여 경계 데이터를 저장하고, 해당 경계 내에 존재하는 데이터를 집계하는 방법을 소개하겠습니다. 🤓

 

Elasticsearch에서 공간 데이터를 다룰 때, 일반적으로 geo_shape 필드를 사용하여 공간 검색을 수행할 수 있습니다. 하지만 저는 보다 유연한 처리와 빠른 인덱싱을 위해 WKT(Well-Known Text) 값을 text 형식으로 저장한 후, Java 로직에서 이를 변환하여 조회하는 방식을 채택했습니다. 이 방법을 사용하면 Elasticsearch의 기본 기능에 의존하지 않고도 공간 데이터를 효율적으로 다룰 수 있습니다.

 

먼저, 경계 데이터를 저장할 Elasticsearch 인덱스를 생성합니다. name 필드는 행정구역명을 저장하며, wkt 필드는 WKT 형식으로 경계 정보를 저장하는 역할을 합니다.

PUT boundaries
{
    "mappings": {
      "properties": {
        "name": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        },
        "wkt": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
        }
      }
    }
}

 

위 설정을 통해 행정구역의 이름과 WKT 데이터를 저장할 수 있도록 했습니다. 이후 데이터를 추가하는 과정에서 길이가 길어 일부 생략하였지만, 예제로 서울특별시의 경계 데이터를 다음과 같이 추가할 수 있습니다.

POST boundaries/_doc/1
{
  "name": "서울특별시",
  "wkt": "MULTIPOLYGON(((26.82434476795297 37.51022586624618,126.82412736794552 37.5104182450281,126.82406008111737 37.510480817253125,126.82385670190524 37.51087508322444,126.82414707105036 37.512440442257486,126.82440812510971 37.51317655114924,129.38753070286643)))"
}

 

경계 내 포함 여부를 검사할 데이터를 저장하기 위해 새로운 인덱스를 생성했습니다. 여기서는 geo_point 필드를 사용하여 위치 데이터를 저장하며, name 필드는 장소명을 의미합니다.

PUT locations
{
  "mappings": {
    "properties": {
      "name": { "type": "keyword" },
      "location": { "type": "geo_point" }
    }
  }
}

 

테스트 데이터를 추가하여 특정 위치 정보를 저장했습니다.

POST locations/_doc/1
{
  "name": "서울특별시 강남구 역삼동 638-12번지",
  "location": { "lat": 37.502127203, "lon": 127.033051782 }
}

 

이제 Elasticsearch의 geo_shape 필터를 활용하여 특정 행정구역 내 포함된 데이터를 조회할 수 있습니다. 다음 쿼리는 다각형 좌표를 기준으로 특정 지역 내부에 포함된 데이터를 검색합니다.

GET locations/_search
{
  "query": {
    "bool": {
      "filter": {
        "geo_shape": {
          "location": {
            "shape": {
              "type": "polygon",
              "coordinates": [[[127.0276, 37.4979], [127.0353, 37.4969], [127.0378, 37.5010], [127.0298, 37.5042], [127.0276, 37.4979]]]
            },
            "relation": "within"
          }
        }
      }
    }
  }
}

 

위의 쿼리는 강남구 경계 내에 포함된 모든 데이터를 검색합니다. 여기서 relation 옵션을 변경하면 경계 내부(within), 경계 선(intersects), 특정 지점(contains)에 따라 필터링할 수 있습니다. 데이터를 집계하여 해당 영역 내 개수를 구하려면 value_count 집계를 사용할 수 있습니다.

GET locations/_search
{
  "size": 0,
  "query": {
    "bool": {
      "filter": {
        "geo_shape": {
          "location": {
            "shape": {
              "type": "polygon",
              "coordinates": [[[127.0276, 37.4979], [127.0353, 37.4969], [127.0378, 37.5010], [127.0298, 37.5042], [127.0276, 37.4979]]]
            },
            "relation": "within"
          }
        }
      }
    }
  },
  size: 1,
  "aggs": {
    "count_within_boundary": {
      "value_count": { "field": "name" }
    }
  }
}

 

이제 Java 코드에서 WKT 값을 조회하고 이를 활용하여 특정 경계 내 데이터를 필터링하는 로직을 구현할 수 있습니다.

public Optional<String> getWktFromSiteBounds() throws IOException {
    SearchRequest searchRequest = new SearchRequest.Builder()
            .index("boundaries")
            .build();

    return elasticsearchClient.search(searchRequest, Map.class).hits().hits()
            .stream()
            .map(Hit::source) // JsonData 대신 Map으로 처리
            .filter(Objects::nonNull)
            .map(source -> (String) source.get("boundary")).findFirst();

}

 

위의 메서드는 boundaries 인덱스에서 WKT 값을 조회하며, 조회된 데이터가 없을 경우 빈 값을 반환합니다. 이후 WKT 값을 활용하여 geo_shape 필터를 적용하여 경계 내 데이터를 집계하는 로직을 작성할 수 있습니다.

public int countLocationsWithinBounds() throws IOException {
    Optional<String> wktOptional = getWktFromSiteBounds();

    if (wktOptional.isEmpty()) { return 0; }
    
    String wkt = wktOptional.get();

    return (int) elasticsearchClient.count(
            c -> c.index("locations")
                    .query(q -> q.geoShape(
                            gs -> gs
                                    .field("location")
                                    .shape(
                                            s -> s
                                                    .shape(JsonData.of(wkt))
                                                    .relation(GeoShapeRelation.Within)
                                    )
                    ))
    ).count();
}

 

이 메서드는 count API를 활용하여 특정 경계 내에 포함된 데이터를 집계하며, GeoShapeRelation.Within 옵션을 사용하여 해당 지역 내부에 있는 데이터 개수를 반환합니다. 이를 통해 Elasticsearch에서 공간 데이터를 효과적으로 활용하여 특정 행정구역 내 데이터를 집계하는 방법을 정리했습니다. Elasticsearch의 geo_shape 기능을 활용하면 공간 데이터를 효율적으로 처리할 수 있으며, Java 코드와 함께 적용하면 보다 유연한 공간 데이터 검색 및 집계가 가능합니다.


728x90
728x90