본문 바로가기
BackEnd/DB

[DB] ElasticSearch의 Search API

by 경험의 가치 2024. 11. 19.

SearchAPI란?

SearchAPI는 ElasticSearch에서 데이터 검색과 필터링을 위한 API입니다. 특정 조건으로 데이터를 검색하고, 정렬, 페이징, 집계 등에 적용할 수 있습니다. 크게 두가지 방법으로 전달할 수 있습니다.

 

첫번째가 URI Search인데 이건 우리가 ?query=”hello” 이런식으로 REST에서 보내는 방법입니다. 간단한 검색이면 몰라도, 사실 크게 자주 쓰지 않습니다.

 

두번째가 Query DSL인데 아래처럼 Body에 넣어서 보내는 것입니다. 대부분 후자 방법을 이용합니다. (더 상세한 표현이 가능하고 재사용성이 좋기 때문입니다.)

QueryDSL에는 위 사진과 같은 옵션을 넣을 수 있습니다. 특히, query부분이 중요한데, 이 곳에 우리가 SQL로 작성해서 DB에서 원하는 정보를 뽑아오는 것 처럼, query에 json으로 우리가 원하는 구체적인 조건을 작성합니다. 아래 코드는 search API의 QueryDSL 예시입니다.

GET /my_index/_search
{
  "size": 5,                        // 반환할 문서 개수를 5개로 지정
  "from": 10,                       // 10번째 문서부터 시작하여 가져옴 (페이징)
  "timeout": "2s",                  // 2초 내에 응답이 없으면 타임아웃
  "_source": ["title", "author"],   // title과 author 필드만 반환
  "query": {                        // 검색 조건문
    "match": {
      "content": "Elasticsearch"
    }
  },
  "aggs": {                         // 집계 사용 (예: 작성자별 문서 개수 집계)
    "authors": {
      "terms": {
        "field": "author.keyword"
      }
    }
  },
  "sort": [                         // 정렬 조건 (예: 날짜 내림차순)
    {
      "published_date": "desc"
    }
  ]
}

Search Query

search query는 크게 Query Context와 Filter Context로 나눌 수 있다.

Query Context

너무 많아서 주요 Query만 소개할 예정이다. 더 자세한 정보를 원하면 공식문서를 참조바란다.

 

Query DSL | Elasticsearch Guide [8.16] | Elastic

Elasticsearch provides a full Query DSL (Domain Specific Language) based on JSON to define queries. Think of the Query DSL as an AST (Abstract Syntax Tree) of queries, consisting of two types of clauses: Leaf query clauses Leaf query clauses look for a par

www.elastic.co

 

Query Context의 특징

  1. 유사도 계산 알고리즘에 의해 가장 연관성 높은 도큐먼트를 찾는 API들 → ES가 자체적으로 유사도에 대한 점수를 매기는데 그것을 가지고 Sorting해서 반환해줌.
  2. 루씬 레벨에서 분석과정을 거침으로 상대적으로 느림.
  3. 결과가 캐싱되지 않음

Query Context의 종류

 

1. Term Query

GET /my_index/_search
{
  "query": {
    "term": {
      "status": "active"
    }
  }
}

정확하게 단어가 일치하는 문서를 검색할 때 사용. 위에 예시는 status 필드가 active로 정확히 일치하는 것을 찾음. 형태소 분석을 하지 않음.

 

2. Match Query

GET /my_index/_search
{
  "query": {
    "match": {
      "description": "quick brown fox"
    }
  }
}

형태소 분석을 수행하여 단어를 토큰으로 분리한 것을 기준으로 검색함. 즉, 부분 일치된 결과도 검색이 가능하다는 것.

{
  "query": {
    "match": {
      "title": {
        "query": "elastic search",
        "operator": "and" // 기본 연산자는 or
      }
    }
  },
  "_source": ["title", "tags"]
}

위와 같이 옵션을 주어서 두 단어가 다 포함되어야만 검색되게도 가능함.

 

{
  "query": {
    "match": {
      "title": {
        "type": "phrase",
        "query": "elastic search",
        "slop": 1 // "elastic"과 "search" 사이에 한 단어가 있을 수 있음
      }
    }
  },
  "_source": ["title", "tags"]
}

위와 같은 형태도 가능.

 

3. Multi-Match Query

POST /my_index/_search
{
  "query": {
    "multi_match": {
      "query": "Elasticsearch",
      "fields": ["title", "description"]
    }
  }
}

여러개의 필드에서 단어가 포함됐는지 여부 검색. title 필드 또는 description 필드 중에 하나라도 Elasticsearch라는 단어가 포함되면 검색됨.

 

4. Wild Card Query

{
  "query": {
    "wildcard": {
      "title": "he*o"
    }
  }
}

특정 패턴에 일치하는 모든 문자열 검색할 때 사용. 형태소 분석 없이 그대로 입력한 텍스트를 매칭함!! 즉, 우리가 형태소 분석을 통해 통으로 저장하지 않고 토큰화를 했다면? 정상 작동하지 않음.

 

5. Prefix Query

{
  "query": {
    "prefix": {
      "user.id": {
        "value": "ki"
      }
    }
  }
}

접두사가 있는 모든 문서를 검색

 

6. Bool Query

{
  "query": {
    "bool" : {
      "must" : {
        "term" : { "user.id" : "kimchy" }
      },
      "filter": {
        "term" : { "tags" : "production" }
      },
      "must_not" : {
        "range" : {
          "age" : { "gte" : 10, "lte" : 20 }
        }
      },
      "should" : [
        { "term" : { "tags" : "env1" } },
        { "term" : { "tags" : "deployed" } }
      ],
      "minimum_should_match" : 1,
      "boost" : 1.0
    }
  }
}

하나의 쿼리나 여러개의 쿼리를 조합해서 검색함. AND, OR, NAND, FILTER를 must, must_not, should, filter로 표

 

7. Script Query

{
  "query": {
    "script": {
      "script": {
        "source": "doc['age'].value >= 20 && doc['age'].value <= 30"
      }
    }
  }
}

커스텀 스크립트를 이용하여 문서를 필터링하거나 점수를 계산하는 데 사용되는 쿼리. 특정 조건이나 복잡한 계산을 기반으로 문서를 검색하고자 할 때 사용자 정의 스크립트를 작성하여 쿼리를 날릴 수 있음. 아래 예시는 age 필드가 20 이상이고 30 이하인 document만 검색하는 script 쿼리임. 자세히 다루기엔 내용이 너무 많으므로 아래 문서 참고

 

[Elasticsearch] 스크립트 쿼리

스크립트 쿼리Elasticsearch에서 제공하는 스크립트 언어를 활용해서 검색 쿼리 생성필터 기반의 검색 쿼리여러 필드를 동시에 다루거나 검색 조건이 달라지는 경우 활용 스크립트 사용 방법 스

jaimemin.tistory.com

 

8. Fuzzy Query

 

오타 보정을 해주는 Query이다. 편집 거리 알고리즘 기반을 응용한 독자적인 Damerau-Lavenshtein 알고리즘을 사용하여 두 단어 간의 유사성을 측정하고 지정한 fuzziness(편집 거리)를 통해 검색어인 오타와 일치하는 단어를 찾음.

 

Fuzzy Query는 상대적으로 Cost가 높으며, 기본적으로 Analyzer를 거친 검색와 색인된 데이터를 비교함. 그러다보니 복잡한 분석기를 거칠 경우 의도와 다른 결과가 나타날 수 있음.

 

ex 1) N-gram Tokenizer는 너무 텍스트를 세분화함. 이렇게 세분화된 필드에 Fuzzy Query를 이용하면 검색어와 일치하는 부분이 너무 많아 비정상적으로 많은 결과가 반환될 수 있음.

ex 2) 동의어 분석기와 결합되면 원래 단어가 아닌 동의어와 일치하는 결과를 나타낼 수 있음. 동의어 설정에서 “자동차”와 “차량”이 동일한 의미로 처리되게 설정했다면, Fuzzy Query는 자동차를 검색했는데 차량까지 반환해서 예상과 다른 결과가 나올 수 있음.

 

※ 동의어 분석기 : 다른 비슷한 대체어로도 검색할 수 있게 해주는 것. 아래 처럼 애니 이름 검색인데 “코노스바”라고 검색해도 “이 멋진 세계의 축복을!”이 검색되게하고 싶으면 아래처럼 하면 됨.

"settings": {
    "analysis": {
      "filter": {
        "synonym_filter": {                 
          "type": "synonym",
          "synonyms": [
            "코노스바, 이 멋진 세계의 축복을",          // 첫 번째 동의어 그룹
            "리제로, Re:제로부터 시작하는 이세계 생활"   // 두 번째 동의어 그룹
          ]
        }
      },
      "analyzer": {
        "synonym_analyzer": {              
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "synonym_filter"] 
        }
      }
    }

ex 3) snowball와 같은 어간 추출 분석기를 이용하면 예상치 못한 결과를 반환할 수 있음. 예를 들어서 snowball 분석기를 이용하여 running을 run으로 축소함. 그런데 Fuzzy Query로 runninga를 검색하면 run과 일치하지 않아 running이 검색되지 않음.

 

※ 어간 추출 분석기 : 단어의 기본 형태를 추출하는 분석기. 예를 들어, running, runs, ran은 모두 run으로 동일한 의미로 인식되기 위해서 사용하는 것. 대표적으로 snowball 분석기가 있음.

따라서, Standard, WhiteSpace Tokenizer 같은 비교적 간단한 텍스트 분석기와 결합하는 것이 좋다.

!!한글은 Nori 형태소 분석기 등을 통해서 제대로 Token화 해야 정상 작동함을 유의하자!!

 

※ 편집 거리 알고리즘 : Lave로 검색했는데, 의도한 것은 Love라고 해보자. 둘의 차이는 a와 o의 차이 하나이므로 편집거리는 1이다. 이에 대해 더 탐구하고 싶으면 아래 백준 문제를 풀어보자.

https://www.acmicpc.net/problem/15483

 

Fuzzy Query는 아래와 같고, 다른 쿼리와도 조합이 가능하다.

 

GET /my_index/_search
{
  "query": {
    "fuzzy": {
      "content": {
        "value": "elasticserch",
        "fuzziness": "AUTO"      // 오타 허용 수준을 자동으로 설정
      }
    }
  }
}
GET test-fuzzy/_search
{
  "query": {
    "match": {
      "message": {
        "query": "hello world",
        "fuzziness": 1
      }
    }
  }
}
POST test-fuzzy/_search
{
  "suggest": {
    "song-suggest": {
      "prefix": "누진스",
      "completion": {
        "field": "suggest",
        "fuzzy": {
          "fuzziness": 1
        }
      }
    }
  }
}

 

9. Nested Query

{
  "query": {
    "nested": {
      "path": "comments",
      "query": {
        "bool": {
          "must": [
            { "match": { "comments.user": "John" } },
            { "match": { "comments.message": "great" } }
          ]
        }
      }
    }
  }
}

문서 내부에 또 다른 문서가 중첩되어 있는 구조일 때 사용하는 쿼리

 

Filter Context

  1. Yes/No로 판단 가능. Score를 계산하지 않음.
  2. 점수 계산하지 않기 때문에 빠름. 결과가 내부적으로 캐싱됨.
GET /my_index/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "status": "active"
          }
        }
      ]
    }
  }
}

점수를 계산하지 않고 무작위로 정렬해서 조건에 정확히 일치하는 검색 결과들만 반환해줌.