자주 사용하는 검색 기능

WAS·2025년 8월 11일
0

Elasticsearch

목록 보기
4/4

match

  • 검색 키워드가 포함된 데이터를 조회하고 싶을 때 사용
  • 💡 match 쿼리는 text 타입의 필드에서만 사용하는 쿼리
  • match 쿼리는 검색 키워드가 포함된 모든 도큐먼트를 조회

EX)

PUT /boards // 인덱스 생성 및 매핑 정의
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      }
    }
  }
}

POST /boards/_doc // 데이터 삽입
{
  "title": "벨로그 짱짱 멋짐"
}

GET /boards/_search // 도큐먼트로 검색 (아래와 같이 조회하면 결과값이 조회됨)
{
  "query": {
    "match": {
      "title": "벨로그 멋짐" 
    }
  }
}

term

  • 특정 값과 정확하게 일치하는 데이터를 조회하고 싶을 때 사용
  • term 쿼리는 특정 값과 정확히 일치하는 모든 도큐먼트를 조회
  • term 쿼리는 text 를 제외한 모든 타입에서 사용

EX)

PUT /boards // 인덱스 생성 및 매핑 정의
{
  "mappings": {
    "properties": {
      "board_id": {
        "type": "long"
      },
      "category": {
        "type": "keyword"
      }
    }
  }
}

POST /boards/_doc // 데이터 삽입
{
   "board_id" : 1,
   "category" : "자유 게시판"
}

POST /boards/_doc // 데이터 삽입
{
  "board_id": 2,
  "category": "익명 게시판"
}

---------------------------
  
GET /boards/_search // 조회1
{
  "query": {
    "term": {
      "category": "자유"
    }
  }
}

GET /boards/_search // 조회2
{
  "query": {
    "term": {
      "category": "자유게시판"
    }
  }
}

GET /boards/_search // 조회3
{
  "query": {
    "term": {
      "category": "자유 게시판"
    }
  }
}

위와같이 조회를 3번 해봤을 때 어떤것이 조회될까?
조회3만 도큐먼트가 조회된다

조회1 -> 자유 (데이터가 정확하게 일치하지 않음)
조회2 -> 자유게시판 (공백이 있어서 데이터가 일치하지 않음)

즉 데이터가 가진 값과 정확히 일치하게 검색하지 않으면 도큐먼트가 조회되지 않는다

SQL문으로 표현하면 SELECT * FROM boards WHERE category = "자유 게시판" 와 동일하다

그럼 여러개 값중 하나라도 일치하면 도큐먼트를 조회하고 싶을 때 어떻게 해야할까?

terms

  • terms 쿼리는 여러 개의 값 중 하나라도 일치하는 모든 도큐먼트를 조회
  • SQL문의 IN 과 비슷한 역할

EX)

GET /boards/_search
{
  "query": {
    "terms": {
      "category": ["자유 게시판", "익명 게시판"]
    }
  }
}

위와 같이 조회를 하면 자유 게시판 익명 게시판 둘다 조회되는것을 확인할 수 있다
이 문장은 SELECT * FROM boards WHERE category IN ("자유 게시판", "익명 게시판") 와 동일하다

그러면 여러개의 조건을 동시에 만족하는 데이터를 조회할 때는 어떻게 해야할까?
SELECT * FROM boards WHERE category = “자유 게시판” AND board_id = 1 와 같이
2가지 이상의 조건을 활용해서 검색하려 한다
아래와 같이 조회하면 오류가 발생한다
term 쿼리에서는 2개이상 필드를 사용하는 것을 막아놨다

GET /boards/_search
{
  "query": {
    "term": {
      "category": "자유 게시판",
      "board_id": 1
    }
  }
}

✅ 2가지 이상의 조건을 만족시키는 데이터를 조회하고 싶을 때 사용하는 쿼리

bool 쿼리에서 must or filter 를 사용하면 된다
즉 SQL에서 AND 역할을 한다

filtermust 의 큰 차이점은
filter 는 Score(점수) 영향을 주지않고, must 는 Score(점수) 영향을 준다

Score(점수)는 사용자가 입력한 값이 얼마나 잘 일치하는지 수치로 표현한 관련도 점수이며
역인덱스를 이용해서 계산된 관련도 점수이다

역인덱스는 필드 값을 단어마다 쪼개서 찾기 쉽게 정리해놓은 목록이다

여기서 전에 배운 text 타입과 역인덱스와 Score(점수) 는 밀접한 관련이 있다

text 타입으로 필드를 만든 후 데이터를 넣으면 Analyzer 에 의해서 역인덱스 가 생성된다
역인덱스 에 저장된 단어들을 토큰 이라고 부른다. 그리고 검색 쿼리가 오면 역인덱스 를 통해 데이터를 찾고 Score(점수)를 계산한다

결론은 text 타입으로만 Score(점수) 가 사용되며 계산되며
text 타입의 경우 match 쿼리를 사용하기 때문에 Score(점수)와 상관있는 쿼리의 경우 match 사용

text 이외의 타입의 경우는 Score(점수)와 상관없기 때문에 term 을 사용

💡 정리
유연한 검색이 필요하면
-> text 타입, match 쿼리, boolmust 사용

정확한 검색이 필요하면
-> text 이외의 타입, term 쿼리, boolfilter 사용

예시를 보면서 이해해보자

board_id : long 타입
title : text 타입
category : keyword 타입
is_notice : boolean 타입
created_at : date 타입

위처럼 boards라는 인덱스를 생성하고 매핑까지 했다고 가정하자
그 후 데이터를 삽입

POST /boards/_doc
{
  "board_id": 1,
  "title": "엘라스틱서치는 정말 강력한 검색엔진이에요",
  "category": "자유 게시판",
  "is_notice": false,
  "created_at": "2025-05-01T12:00:00"
}

여기서 검색쿼리를 사용할 때 다음과 같은 조건을 걸고 조회하려면 어떻게 해야할까?
자유 게시판 의 게시글 중에서 검색엔진 과 관련된 글을 찾고 싶다
그런데 공지글 이 아닌 게시글 중에서만 검색하고 싶다

GET /boards/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "검색엔진" } } // 유연한 검색이 필요 (must)
      ],
      "filter": [
        { "term": { "category": "자유 게시판" } }, // 정확한 검색이 필요 (filter)
        { "term": { "is_notice": false } } // 정확한 검색이 필요 (filter)
      ]
    }
  }
}

자유 게시판의 게시글 중에서라는 뜻 -> 자유 게시판 이라고 정확한 검색이 필요 (filter 사용)
검색엔진과 관련된 글 뜻 -> 유연한 검색이 가능함 (must 사용)
공지글이 아닌 게시글 뜻 -> 공지글이 아닌것은 false 로 정확히 구분 (filter 사용)


✅ 특정 조건을 만족하지 않는 데이터를 조회하고 싶을 때

bool 쿼리에서 must_not 을 사용하면 된다
즉 SQL에서 NOT 역할을 한다

직전 예시를 그대로 구현해보자

EX) 광고 게시판 의 글이 아니면서, 공지 글 이 아니면서, 검색엔진 의 키워드와 관련된 게시글을 조회

GET /boards/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "검색엔진" } } // 유연한 검색이 필요 (must)
      ],
      "filter": [
        { "term": { "is_notice": false } } // 정확한 검색이 필요 (filter)
      ],
      "must_not": [
        { "term": { "category": "광고 게시판" } }
      ]
    }
  }
}

광고 게시판의 글이 아니라는 뜻 -> 광고 게시판이 아니여야 한다는 정확한 검색이 필요 (mustnot 사용)
공지 글이 아니면서 뜻 -> 공지글이 아니여야 한다는 정확한 검색 필요 (filter 사용)
검색엔진 의 키워드와 관련된 뜻 -> (must
사용)

💡 must_nottermmatch 를 둘 다 사용할 수 있지만 조건에 따라 사용이 다르다
ex) 어떤 필드값이 광고가 아니여야 할 때 -> term 사용
ex) 어떤 필드값이 '광고’ 관련 내용을 유사하게라도 포함한 글이 아니여야할 때 -> match 사용


✅ 숫자/날짜의 값에 대한 범위 조건으로 데이터를 조회하고 싶을 때
range 쿼리를 사용하면 된다

range 쿼리에서 사용하는 연산자

  • gte : 이상 lte : 이하 gt 초과 lt 미만
  • gte = Greater Than or Equal to (크거나 같음)
  • lte = Less-than or equal to (작거나 같음)

ex) 나이가 30살 이상이면서 회원가입 날짜가 2025년 1월 1일 이후인 사용자를 조회

GET /users/_search
{
  "query": { 
    "bool": { // 2가지 이상의 조건이 있기때문에 bool 쿼리 사용
      "filter": [ // 정확한 검색을 해야하기때문에 filter 사용
        {
          "range": {
            "age": {
              "gte": 30
            }
          }
        },
        {
          "range": {
            "created_at": {
              "gte": "2025-01-01"
            }
          }
        }
      ]
    }
  }
}

✅ 특정 조건을 만족하는 데이터 위주로 상위 노출시키고 싶을 때
boole 쿼리에서 should 를 사용하면 된다

위에서 배운 mustfilter 는 반드시 조건을 만족하는 데이터만 조회하지만
should 는 조건을 만족하지 않는 데이터도 조회된다

그 이유는 should 는 조건을 추가해서 그 조건이 맞을경우 score(점수) 에 가산점을 부여해서
데이터가 상위로 노출될 가능성이 높아진다

ex) 검색 결과 중 평점이 높고 좋아요 수가 많은 글 을 상위에 노출시키고 싶은 경우

인덱스를 다음과 같이 생성했다고 가정하자

PUT /products
{
  "mappings": {
    "properties": {
      "name":   {  // 제품이름
        "type": "text",
        "analyzer": "nori"
      },
      "rating": { "type": "double" }, // 평점
      "likes":  { "type": "integer" } // 좋아요 수
    }
  }
}

데이터는 아래와 같이 넣었다고 가정하자
"name": "무선 충전기 C타입", "rating": 4.9, "likes": 300
"name": "소니 무선 이어폰 WF", "rating": 3.8, "likes": 15
"name": "갤럭시 버즈2 무선 이어폰", "rating": 4.8, "likes": 310
"name": "삼성 노트북 13인치", "rating": 5.0, "likes": 1000

should 조건 없이 데이터를 조회하면 무선 이어폰 키워드가 없는 데이터는 아예 조회되지도 않고
평점이 낮거나 좋아요 수가 적은 상품이 상위에 노출되는 것을 확인할 수 있다

아래처럼 should 를 사용해서 조회를 해보자

GET /products/_search
{
  "query": {
    "bool": {
      "must": [ // 특정키워드로 연관성이 있는 것 즉 score를 매겨야하기 때문에 must 사용
        {
          "match": {
            "name": "무선 이어폰" 
          }
        }
      ],
      "should": [
        {
          "range": {
            "rating": {
              "gte": 4.5 // 4.5 이상의 평점의 상품일 경우 score에 가산점 부여
            }
          }
        },
        {
          "range": {
            "likes": {
              "gte": 100 // 좋아요 수가 100개 이상인 상품일 경우 score에 가산점 부여
            }
          }
        }
      ]
    }
  }
}

결과는 어떻게 나올까?
1 순위 : 갤럭시 버즈2 무선 이어폰 -> 검색 키워드도 관련성 높으면서 평점도 좋고 좋아요 수도 많아서 1위
2 순위 : 무선 충전기 C타입 -> 검색 키워드는 '무선' 만 일치하지만 평점과 좋아요 수가 높아서 2위
3 순위 : 소니 무선 이어폰 WF -> 검색 키워드는 관련성이 높지만 평점과 좋아요 수가 낮아서 3위

삼성 노트북 13인치는 관련 키워드가 아예 없기 때문에 조회되지 않음


✅ 오타가 있더라도 유사한 단어를 포함한 데이터를 조회하고 싶을 때
fuzziness 를 사용하면 된다

데이터에 name 필드가 => 메이플 캐논슈터 일때
아래와 같이 조회하면 데이터가 잘 조회되는 것을 확인할 수 있다

GET /boards/_search
{
  "query": {
    "match": {
      "title": "메이플"
    }
  }
}

하지만 마이플 이라고 검색하면 데이터가 조회되지 않는다
이럴 때 오타가 어느정도 있어도 검색되게 만들기 위해서 fuzziness 를 사용한다

GET /boards/_search
{
  "query": {
    "match": {
      "title": {
        "query": "마이플",
        "fuzziness": "AUTO"
      }
    }
  }
}

fuzziness : AUTO : 단어 길이에 따라 오타 허용 개수를 자동으로 설정


✅ 여러 필드에서 검색 필드가 포함된 데이터를 조회하고 싶을 때 (ex : 제목, 내용)
multi_match 를 사용하면 된다

예시로 데이터를 4개 삽입했다고 가정하자

POST /boards/_doc // (1) title, content 둘 다에 키워드 포함
{
  "title": "엘라스틱서치 적용 후기",
  "content": "회사 프로젝트에 엘라스틱서치를 적용한 후기를 공유합니다."
}

POST /boards/_doc // (2) title 에만 키워드 포함
{
  "title": "엘라스틱서치를 사용해보니",
  "content": "검색 엔진 도입 후 성능이 향상되었습니다."
}

POST /boards/_doc // (3) content에만 키워드 포함
{
  "title": "검색엔진 도입 사례",
  "content": "이번 프로젝트에 엘라스틱서치를 적용한 후 많은 개선 효과가 있었습니다."
}

POST /boards/_doc // (4) title, content 둘 다 포함 안 됨
{
  "title": "레디스 캐시 사용기",
  "content": "서비스 속도 개선을 위해 캐시 시스템을 사용했습니다."
}

여기서 아래와 같이 multi_match 를 이용해서 조회하면

GET /boards/_search
{
  "query": {
    "multi_match": { // 두가지 이상 필드에서는 multi_match 사용
      "query": "엘라스틱서치 적용 후기",
      "fields": ["title", "content"] // 찾고자하는 여러개의 필드를 배열로 지정
    }
  }
}

(1) <- (3) <- (2) 순으로 상위노출되서 조회되는것을 확인할 수 있다
(4)는 title과 content에 둘 다 키워드가 없어서 조회되지 않는다

왜 위와 같은 순서로 상위노출 조회 됬을까?
다음과 같은 조건으로 score 로 점수를 매긴다

  • 검색 키워드가 문서에서 자주 등장할 수록 점수가 높게 측정됨
  • 전체 문서중 희귀한 검색어가 일치할수록 점수가 높게 측정됨
  • 필드 값의 길이가 작은데도 불구하고 키워드가 등장하면 점수가 높게 측정

(1) 은 title과 content에 키워드가 둘 다 들어있기 때문에 score 를 높게 받아 제일 먼저 조회된다

그러면 내가 원하는 필드에 검색어가 포함될 때 점수를 더 높게 주려면 어떻게 해야할까? (가중치)
다음과 같이 작성하면 된다

GET /boards/_search
{
  "query": {
    "multi_match": {
      "query": "엘라스틱서치 적용 후기",
      "fields": ["title^2", "content"] // title에 2배 더 높은 score를 부여
    }
  }
}

위와 같이 조회한다면 (1) <- (2) <- (3) 순으로 조회되는 것을 확인할 수 있다


✅ 검색한 키워드를 하이라이팅 처리하고 싶을 때
highlight 를 사용하면 된다

바로 위 예시로 사용해서 작성해보자

GET /boards/_search
{
  "query": {
    "multi_match": {
      "query": "엘라스틱서치 적용 후기",
      "fields": ["title", "content"]
    }
  },
  "highlight": {
    "fields": {
      "title": {
        "pre_tags": ["<mark>"],
        "post_tags": ["</mark>"]
      },
      "content": {
        "pre_tags": ["<b>"],
        "post_tags": ["</b>"]
      }
    }
  } 
}

위처럼 title에서 특정 키워드가 일치하면은
pre_tags (키워드 앞에는) mark 태그를 붙이고 post_tags (키워드 뒤에는) /mark 태그를 붙인다

또 content에서 키워드가 일치하면
pre_tags (키워드 앞에는) b 태그를 붙이고 post_tags (키워드 뒤에는) /b 태그를 붙인다

조회 결과는 아래와 같이 HTML 태그로 감싸져서 결과 값이 반환된다

"title": [ "<mark>엘라스틱</mark><mark>서치</mark> <mark>적용</mark> <mark>후기</mark>" ],
"content": [ "회사 프로젝트에 <b>엘라스틱</b><b>서치</b>를 <b>적용</b>한 <b>후기</b>를 공유합니다." ]

✅ 페이징과 정렬

우선 페이징은 sizefrom 을 사용하면 된다

데이터를 총 7개 (1번글 ~ 7번글) 을 생성했다고 가정하자

POST /boards/_doc
{
  "title": "1번 글",
  "likes": 12
}

POST /boards/_doc
{
  "title": "2번 글",
  "likes": 35
}
..... ~ 7번글까지

여기서 페이징 처리를 적용시켜서 데이터를 조회해보자

GET /boards/_search
{
  "query": {
    "match": {
      "title": "글"
    }
  },
  "size": 3, // 3개만 불러오기
  "from": 0 // 첫번째부터 
}

size : 한 페이지에 불러올 데이터 개수 (SQL문의 LIMIT과 동일)
from : 몇 번째 데이터부터 불러올 지 (SQL문의 OFFSET과 동일, 0부터 시작)

그럼 2페이지는 어떻게 조회하는가?
size 는 그대로 3이고 from 을 3으로 바꾸면 된다

다음은 정렬을 해보자
likes(좋아요) 필드를 기준으로 정렬해보자

GET /boards/_search
{
  "query": {
    "match": {
      "title": "글"
    }
  },
  "sort": [
    {
      "likes": {
        "order": "desc" // 내림차순
      }
    }
  ]
}

✅ 하나의 필드에 textkeyword 타입을 동시에 사용하고 싶을 때

text 는 유연한 검색, keyword 는 정확한 검색이 필요할 때 사용한다
유연한 검색과 정확한 검색을 둘 다 하고 싶을 때 사용 한다

아래 예시는 category 필드에 두 가지 타입을 사용한 경우이다

PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "nori"
      },
      "category": {
        "type": "text", // text 타입 선언
        "analyzer": "nori",
        "fields": { 
          "raw": { // 서브 필드명 (다른 이름으로 설정해도 됨)
            "type": "keyword" // keyword 타입 선언
          }
        }
      }
    }
  }
}

위처럼 인덱스를 생성하고 매핑 한 후 데이터를 다음과 같이 삽입하면

POST /products/_doc
{
  "name": "삼성 세탁기",
  "category": "특수 가전제품"
}

text 타입은 토큰으로 분리해서 저장한다 -> 특수 가전 제품
keyword 타입은 값 자체를 통째로 저장한다 -> 특수 가전제품

이 상태에서 유연하게 검색을 하면

POST /products/_search
{
  "query": {
    "multi_match": {
      "query": "가전",
      "fields": ["name", "category"] // name필드나 category 필드에 가전이 들어가면 조회됨
    }
  }
}

이번엔 특수 가전제품 카테고리의 상품만 검색하고 싶으면

POST /products/_search
{
  "query": {
    "term": { // 정확하게 일치하는 데이터를 찾기위해서 term 사용
      "category.raw": "특수 가전제품"
    }
  }
}

✅ 검색어를 추천해주는 기능 ( 자동 완성 기능)

여러가지 방법이 있지만 자주 사용하고 가성비 좋은 search_as_you_type 을 활용해보자

search_as_you_type

  • Elasticsearch에서 자동 완성 기능을 쉽게 구현할 수 있게 설계된 데이터 타입
  • text 타입처럼 애널라이저를 거쳐 토큰으로 분리된다
  • 이 타입을 활용해서 필드를 만들면 내부적으로
    _2gram , _3gram 이라는 멀티 필드(Multi Field)도 같이 만든다.

_2gram : 두 단어씩 묶어서 토큰을 만든다
_3gram : 세 단어씩 묶어서 토큰을 만든다

예를 들어 아래와 같이 search_as_you_type 타입으로 인덱스를 생성하고

PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "search_as_you_type",
        "analyzer": "nori"
      }
    }
  }
}

Analyze로 토큰을 분석해보자

GET /products/_analyze
{
  "field": "name",
  "text": "You have the big banana"
  // 일반 text 타입처럼 동일한 방식으로 분리됨 -> 'you' 'have' 'the' 'big' 'banana'
}

GET /products/_analyze
{
  "field": "name._2gram",
  "text": "you have the big banana"
  // 두 단어씩 분리됨 -> 'you have' 'have the' 'the big' 'big banana'
}

GET /products/_analyze
{
  "field": "name._3gram",
  "text": "you have the big banana"
  // 세 단어씩 분리됨 -> 'you have the' 'have the big' 'the big banana'
}

이제 자동완성 기능을 사용해보자

데이터를 곱창 돌김생김 구운 돌김 완도 곱창 돌김 100매 삼성 노트북 Nike 신발 을 삽입했다고 가정

위 데이터를 바탕으로 아래처럼 조회를 하면 데이터가 자동완성 된다

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "돌김", 
      "type": "bool_prefix", 
      "fields": [
        "name",
        "name._2gram",
        "name._3gram"
      ]
    }
  }.
  "size" : 5  // 5개까지만
}

여기서 bool_prefix 앞쪽 단어는 match, 마지막 단어는 prefix match로 처리한다
ex) you have the 라고 검색하면 you have 는 연인덱스에 저장된 토큰과 일치하는 데이터를 찾고
마지막 단어인 the 로 시작하는 데이터를 조회하는 것이다

profile
우측 상단 햇님모양 클릭하셔서 무조건 야간모드로 봐주세요!!

0개의 댓글