티스토리 뷰

이번 포스팅은 사내에서 Elasticsearch 관련 내용 발표를 위해 "시작하세요! 엘라스틱서치"서적을 기반으로 학습하고 이해한 내용을 정리하는 포스팅이다. Elasticsearch 역시 내용이 많기 때문에 시리즈로 나눠서 정리할 예정이다. 모든 내용은 Elasticsearch 7.6 버전 기준이다.

 

오늘은 Elasticsearch 의 검색방법중 QueryDSL(Domain Specific Language) 을 알아볼 예정이다.  

 

QueryDSL 

 Elasticsearch 에서 Query DSL을 이용해서 데이터를 찾는 기능은 크게 Query 와 Filter 를 이용한 방법으로 나뉜다.

Query 와 Filter 모두 JSON 형식이며 둘의 차이점은 다음과 같다. 자세한 내용은 여기에서 확인 할 수 있다. 

  • Query 는 전문검색(full text search) 에 사용되고 Filter 는 Yes/No 조건에 사용된다. 
  • Query 는 점수가 계산되나 Filter 는 점수(score)가 계산되지 않는다. 
  • Query 는 결과를 캐싱하지 않으나 Filter 는 결과를 캐싱한다. 

쿼리(Query)

 Query 의 사용방법은 다음과 같다. 

{ 
  "query" : {
    "<쿼리타입>": {
      "<필드명>" : { parameters }
    }
  }
}

이제부터 Query 의 종류를 하나씩 살펴보자. 

 

1. 텀,텀즈 쿼리(term, terms)

 term 의 개념에 대해서 알기위해서는 형태소 분석에 대해서 가볍게 보고 가자.

Index 에 Document 가 색인(indexing) 될때는 형태소 분석과정을 거치고 저장된다. 예를들어 지난번 포스팅에서 저장했던 user_bulk 라는 Index 에 country 필드가 San Diego 인 Document 가 색인(indexing) 될때는 다음과 같은 토큰으로 나뉘어서 저장된다. 

San Diego -> san, deigo

 모든 대문자는 소문자로 변형되고, 중복을 삭제하는 과정을 거치고 저장된 san, deigo 같은 토큰을 텀(term) 이라 부른다. 

그리고 term 이 정확히 일치하는 내용을 찾는 Query 가 텀, 텀즈 쿼리(term terms query) 이다.

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "term" : { "country": "Diego" }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

다음과 같이 term 에 Diego 로 검색할경우 Diego 라는 term 이 없기 때문에 결과값이 없다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "term" : { "country": "diego" }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.6952169,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 1.6952169,
        "_source" : {
          "first_name" : "Dean",
          "last_name" : "Ketelsen",
          "birth" : 1988,
          "country" : "San Diego",
          "zip" : "11801"
        }
      }
    ]
  }
}

term 을 diego 로 지정할경우 기대하던 값이 나온다. 하지만 term Query 로 san diego 로 검색하면 또다시 결과값이 나오지 않는다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "term" : { "country": "san diego" }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 0,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  }
}

그 이유는 san deigo 와 같이 두개의 term 을 한 Query 에 입력하면 "san diego" 라는 하나의 term 을 찾으려고 하고, 당연히 결과가 나오지 않는다. 이렇게 2개 이상의 term 을 검색하려면 텀즈(terms) 를 이용한다. terms Query 는 전달받은 term 중에 하나라도 일치하는 Document 를 반환하게 된다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "terms" : { "country": ["san", "diego"] }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "8",
        "_score" : 1.0,
        "_source" : {
          "first_name" : "Lenna",
          "last_name" : "Newville",
          "birth" : 2002,
          "country" : "San Francisco",
          "zip" : "27601"
        }
      },
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 1.0,
        "_source" : {
          "first_name" : "Dean",
          "last_name" : "Ketelsen",
          "birth" : 1988,
          "country" : "San Diego",
          "zip" : "11801"
        }
      }
    ]
  }
}

terms 는 여러개의 terms 를 추가해서 검색할수 있지만 terms 쿼리를 이용해서는 우리가 원하는 San Diego 만을 추출해 낼 수 없다. 

term, terms Query 에 대한 더 자세한 내용은 여기서 확인 할 수 있다.

 

2. 매치 쿼리(match)

 match Query 도 term Query 와 마찬가지로 주어진 Query 를 term 과 비교해 일치하는 Document 를 검색한다.

다만 term Query 와는 다르게 주어진 Query 를 형태소 분석한뒤에 분석된 Query 로 검색을 수행한다.
예를 들어 San Diego 로 검색할경우 match Query 는 형태소 분석을 거쳐 san, diego 로 Query 를 바꾸고 이 값을 term 과 비교해서 검색한다. URI 검색처럼 operator 를 이용해서 조건을 지정할 수 있으며 기본은 or 이다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "match" : {
      "country": {
        "query" : "San Diego",
        "operator" : "and"
      }
    }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 2.9558086,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 2.9558086,
        "_source" : {
          "first_name" : "Dean",
          "last_name" : "Ketelsen",
          "birth" : 1988,
          "country" : "San Diego",
          "zip" : "11801"
        }
      }
    ]
  }
}

match Query 에 대한 더 자세한 내용은 여기서 확인 할 수 있다.

 

3. 멀티매치 쿼리(multi-match)

multi-match Query 는 match Query 와 유사하며 질의문을 여러 필드에 적용 할 수 있다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "multi_match" : {
      "fields" : ["first_name", "last_name"],
      "query" : "Lenna Ketelsen"
    }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : 1.9924302,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 1.9924302,
        "_source" : {
          "first_name" : "Dean",
          "last_name" : "Ketelsen",
          "birth" : 1988,
          "country" : "San Diego",
          "zip" : "11801"
        }
      },
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.4816045,
        "_source" : {
          "first_name" : "Lenna",
          "last_name" : "Paprocki",
          "birth" : 1991,
          "country" : "Anchorage",
          "zip" : "99501"
        }
      },
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "8",
        "_score" : 1.4816045,
        "_source" : {
          "first_name" : "Lenna",
          "last_name" : "Newville",
          "birth" : 2002,
          "country" : "San Francisco",
          "zip" : "27601"
        }
      }
    ]
  }
}

multi-match Query 에 대한 더 자세한 내용은 여기서 확인 할 수 있다.

 

4. 불 쿼리(bool)

불 쿼리는 내부의 Query 를 다른 Query 들을 포함시켜 사용한다. bool Query 에서 사용할 수 있는 조건문으로는 다음 4가지가 있다. 

  • must : 이 쿼리는 반드시 해당되어야 한다. AND 조건에 해당한다. 
  • must_not : 이 쿼리는 반드시 해당되면 안된다. NOT 조건에 해당한다.
  • should : 이 쿼리는 반드시 해당될 필요는 없지만 해당된다면 더 높은 스코어를 가진다. OR 조건과 유사하다.
  • filter : 이 쿼리는 반드시 해당되어야 하지만 스코어에는 영향을 주지 않는다.

bool Query 에 대한 내용은 밑에서 Filter 를 학습하면서 가볍게 볼 예정이다. bool Query  에 대한 더 자세한 내용은 여기서 확인 할 수 있다.

 

5. 문자열 쿼리(query_string)

query_string Query 는 이전 포스트의 URI 검색에서 q 매개변수를 사용한것과 동일한 방식으로 사용할 수 있는 쿼리다. 

Query 에 <필드명> : <parameters> 형식으로 필드를 지정할 수 있고 and, or 값을 이용해 조건문을 사용 할 수 있다.
그 외에도 .* 와 같은 와일드카드를 사용할 수 있어 가장 다양한 질의를 구현 할 수 있다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "query_string" : { 
      "query": "San Diego",
      "default_field":"country",
      "default_operator": "and"
    }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 2.9558086,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 2.9558086,
        "_source" : {
          "first_name" : "Dean",
          "last_name" : "Ketelsen",
          "birth" : 1988,
          "country" : "San Diego",
          "zip" : "11801"
        }
      }
    ]
  }
}

query_string Query 에 대한 더 자세한 내용은 여기서 확인 할 수 있다.

 

6. 접두어 쿼리(Prefix)

 Prefix Query 는 term Query 와 마찬가지로 질의어에 형태소 분석이 적용되지 않으므로 정확한 텀(term) 값을 고려해서 검색해야 한다. 

밑의 예시는 질의어 s 로 시작하는 텀(term) 이 포함된 Document 를 검색하려 한다. Prefix Query 는 보통 검색어의 자동완성 기능 같은 서비스를 구현하는데 사용될 수 있다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "prefix" : { 
      "country": "s"
    }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "8",
        "_score" : 1.0,
        "_source" : {
          "first_name" : "Lenna",
          "last_name" : "Newville",
          "birth" : 2002,
          "country" : "San Francisco",
          "zip" : "27601"
        }
      },
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 1.0,
        "_source" : {
          "first_name" : "Dean",
          "last_name" : "Ketelsen",
          "birth" : 1988,
          "country" : "San Diego",
          "zip" : "11801"
        }
      }
    ]
  }
}

Prefix Query 에 대한 더 자세한 내용은 여기서 확인 할 수 있다.

 

7. 범위 쿼리(Range) 

Range Query 를 사용해 주어진 범위에 해당하는 필드값이 있는 Document 를 검색 할 수 있다. Range Query 에 사용되는 값은 다음과 같다. 

  • gte(greater than or equal) - 주어진 값보다 크거나 같다. 
  • gt(greater than) - 주어진 값보다 크다. 
  • lte(less than or equal) - 주어진 값보다 작거나 같다. 
  • lt(less than) - 주어진 값보다 작다. 

다음 예시는 birth 가 1989 이상부터 1992 미만의 user 를 찾는 쿼리이다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "range" : { 
      "birth": { "gte" : 1989, "lt": 1992 }
    }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 1.0,
        "_source" : {
          "first_name" : "Lenna",
          "last_name" : "Paprocki",
          "birth" : 1991,
          "country" : "Anchorage",
          "zip" : "99501"
        }
      },
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "6",
        "_score" : 1.0,
        "_source" : {
          "first_name" : "Simona",
          "last_name" : "Morasca",
          "birth" : 1989,
          "country" : "Ashland",
          "zip" : "44805"
        }
      }
    ]
  }
}

Range Query 에 대한 더 자세한 내용은 여기서 확인 할 수 있다.

 

필터(Filter)

Filter 는 점수(score)를 계산하지 않기 때문에 쿼리와 비교했을때 속도가 빠르다. 일반적으로 쿼리는 복잡한 조건으로, Filter 는 Yes/No 와 같은 바이너리 조건 검색에 사용할 것을 권장한다. Filter 의 결과는 보통 메모리에 캐싱되며 다른 Filter 나 Query 등의 처리에 이용된다.

별도로 존재했던 Filter Context 는 Elasticsearch 7.0 으로 버전업되면서 deprecated 되었다. 이에 대한 내용은 여기에 있다.

하지만 Filter 의 문법은 bool 쿼리에서 사용할 수 있다. 

 

1. 텀 필터(term)

term Query 와 마찬가지로 텀(term) 과 일치하는 값을 검색한다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "bool" : { 
      "filter": {
        "term" : { "country":"san" }
      }
    }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "8",
        "_score" : 0.0,
        "_source" : {
          "first_name" : "Lenna",
          "last_name" : "Newville",
          "birth" : 2002,
          "country" : "San Francisco",
          "zip" : "27601"
        }
      },
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "9",
        "_score" : 0.0,
        "_source" : {
          "first_name" : "Dean",
          "last_name" : "Ketelsen",
          "birth" : 1988,
          "country" : "San Diego",
          "zip" : "11801"
        }
      }
    ]
  }
}

 

2. 범위 필터(Range)

Range Query 와 마찬가지로 범위에 해당하는 필드의 값을 가진 Document 를 검색한다. 

curl -H 'Content-Type: application/json' -X GET 'localhost:9200/user_bulk/_search?pretty' -d '
{
  "query": {
    "bool" : { 
      "filter": {
        "range" : { 
          "birth": { "gte" : 1989, "lt": 1992 }
        }
      }
    }
  }
}'
--- 반환값 ---
{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 0.0,
    "hits" : [
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "4",
        "_score" : 0.0,
        "_source" : {
          "first_name" : "Lenna",
          "last_name" : "Paprocki",
          "birth" : 1991,
          "country" : "Anchorage",
          "zip" : "99501"
        }
      },
      {
        "_index" : "user_bulk",
        "_type" : "_doc",
        "_id" : "6",
        "_score" : 0.0,
        "_source" : {
          "first_name" : "Simona",
          "last_name" : "Morasca",
          "birth" : 1989,
          "country" : "Ashland",
          "zip" : "44805"
        }
      }
    ]
  }
}

 

오늘은 여기까지~

누군가에게 도움이 되었길 바라면서 오늘의 포스팅 끝~

댓글