ElasticSearch 개념잡기3

hanana·2026년 3월 8일
post-thumbnail

본 포스팅은 인프런 JSCODE 박재성님의 실전에서 바로 써먹는 Elasticsearch 입문 (검색 최적화편)
강의를 들은 후 해당 강의를 참고하여 작성되었습니다.
https://inf.run/767Nk


3. 자주 사용하는 검색 기능

match

검색 키워드가 포함된 데이터를 조회하고 싶을 때
text 타입의 필드를 조회할 때만 사용 가능

예시

데이터 세팅

PUT /boards
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      }
    }
  }
}


PUT boards/_doc/1
{
  "title":"편의점 과자 내돈내산 후기"
}

결과

GET boards/_search
{
  "query": {
    "match": {
      "title": "편의점 후기"
    }
  }
}

# 결과
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "boards",
        "_id": "1",
        "_score": 0.5753642,
        "_source": {
          "title": "편의점 과자 내돈내산 후기"
        }
      }
    ]
  }
}

term, terms

특정 값과 정확하게 일치하는 데이터를 조회하고 싶을 때
text를 제외한 필드를 검색하고자할 때 사용

예시

데이터 세팅

PUT /boards
{
  "mappings": {
    "properties": {
      "board_id": {
        "type": "long"
      },
      "category": {
        "type": "keyword"
      }
    }
  }
}

PUT /boards/_doc/1
{
  "board_id": 1,
  "category": "자유 게시판"
}

PUT /boards/_doc/2
{
  "board_id": 2,
  "category": "익명 게시판"
}

PUT /boards/_doc/3
{
  "board_id": 3,
  "category": "광고 게시판"
}

결과

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

# 결과
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.9808291,
    "hits": [
      {
        "_index": "boards",
        "_id": "1",
        "_score": 0.9808291,
        "_source": {
          "board_id": 1,
          "category": "자유 게시판"
        }
      }
    ]
  }
}

결과2

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


# 결과
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "boards",
        "_id": "1",
        "_score": 1,
        "_source": {
          "board_id": 1,
          "category": "자유 게시판"
        }
      },
      {
        "_index": "boards",
        "_id": "2",
        "_score": 1,
        "_source": {
          "board_id": 2,
          "category": "익명 게시판"
        }
      }
    ]
  }
}

bool: filter, must

두가지 이상의 조건을 만족시키는 데이터를 조회하고 싶을 때

예시

자유 게시판, 익명 게시판 중 board_id가 1인 문헌 탐색

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


# 결과
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0,
    "hits": [
      {
        "_index": "boards",
        "_id": "1",
        "_score": 0,
        "_source": {
          "board_id": 1,
          "category": "자유 게시판"
        }
      }
    ]
  }
}

결과2

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


# 결과
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "boards",
        "_id": "1",
        "_score": 1,
        "_source": {
          "board_id": 1,
          "category": "자유 게시판"
        }
      },
      {
        "_index": "boards",
        "_id": "2",
        "_score": 1,
        "_source": {
          "board_id": 2,
          "category": "익명 게시판"
        }
      }
    ]
  }
}

filter 와 must의 차이

filter는 score에 영향을 주지 않고
must는 score에 영향을 준다.

  • match 처럼 score와 상관있는 쿼리인 경우 must
  • term 처럼 socre와 상관이 없는 쿼리인 경우 filter를 사용한다.

예시

자유 게시판의 게시글 중에서 '검색엔진'이랑 관련된 글을 찾고 싶다.
그리고 글은 공지글이 아니였으면 좋겠다.

데이터 세팅

PUT /boards
{
  "mappings": {
    "properties": {
      "board_id": {
        "type": "long"
      },
      "title": {
        "type": "text",
        "analyzer": "nori"
      },
      "category": {
        "type": "keyword"
      },
      "is_notice": {
        "type": "boolean"
      },
      "created_at": {
        "type": "date"
      }
    }
  }
}

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

결과

GET /boards/_search
{
  "query": {
    "bool": {
      // match는 must와 쌍을 이룸
      "must": [
        {
          "match": {
            "title" : "검색엔진"
          }
        }
      ],
      // score에 해당하지 않는 필드는 filter와 쌍을 이룸
      "filter": [
        {
          "term": {
            "category": "자유 게시판"
          }
        },
        {
          "term": {
            "is_notice": false
          }
        }
      ]
    }
  }
}

# 결과
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.5753642,
    "hits": [
      {
        "_index": "boards",
        "_id": "Bue6x5wBJYnPpoQ1cAY2",
        "_score": 0.5753642,
        "_source": {
          "board_id": 1,
          "title": "엘라스틱서치는 정말 강력한 검색엔진이에요",
          "category": "자유 게시판",
          "is_notice": false,
          "created_at": "2025-05-01T12:00:00"
        }
      }
    ]
  }
}

must_not

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

예시

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

데이터 세팅

PUT /boards
{
    "mappings": {
        "properties": {
            "board_id": {
                "type": "long"
            },
            "title": {
                "type": "text",
                "analyzer": "nori"
            },
            "category": {
                "type": "keyword"
            },
            "is_notice": {
                "type": "boolean"
            },
            "created_at": {
                "type": "date"
            }
        }
    }
}

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

POST /boards/_doc/2
{
  "board_id": 2,
  "title": "이벤트 참여 방법 안내드립니다",
  "category": "광고 게시판",
  "is_notice": false,
  "created_at": "2025-05-02T10:30:00"
}

POST /boards/_doc/3
{
  "board_id": 3,
  "title": "익명으로 질문하고 답변 받을 수 있어요",
  "category": "익명 게시판",
  "is_notice": true,
  "created_at": "2025-05-03T08:20:00"
}

결과

GET boards/_search
{
    "query": {
        "bool": {
            "must_not": [
              {"term": {
                "category": "광고 게시판"
                }
              }
            ],
            "filter": [
              {
                "term": {
                    "is_notice": false
                }
              }
            ],
            "must": [
                {
                    "match": {
                        "title": "검색엔진"
                    }
                }
            ]
        }
    }
}

# 결과
{
  "took": 13,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1.9155619,
    "hits": [
      {
        "_index": "boards",
        "_id": "1",
        "_score": 1.9155619,
        "_source": {
          "board_id": 1,
          "title": "엘라스틱서치는 정말 강력한 검색엔진이에요",
          "category": "자유 게시판",
          "is_notice": false,
          "created_at": "2025-05-01T12:00:00"
        }
      }
    ]
  }
}

range

숫자/날짜의 값에 대해 범위 조건으로 데이터를 조회할 때

예시

나이가 30살 이상이면서
회원가입 일자가 2025년 1월1일 이후인 사용자

데이터 세팅

PUT /users
{
    "mappings": {
        "properties": {
            "name": {
                "type": "keyword"
            },
            "age": {
                "type": "integer"
            },
            "created_at": {
                "type": "date"
            }
        }
    }
}

PUT /users/_doc/1
{
  "name": "kim_jisoo",
  "age": 28,
  "created_at": "2024-09-01"
}

POST /users/_doc/2
{
  "name": "lee_joon",
  "age": 35,
  "created_at": "2024-12-15"
}

POST /users/_doc/3
{
  "name": "park_saejin",
  "age": 32,
  "created_at": "2025-03-25"
}

결과

GET /users/_search
{
    "query": {
        "bool": {
            "filter": [
                {
                    "range": {
                        "age": {
                            "gte": 30
                        }
                    }
                },
                {
                    "range": {
                        "created_at": {
                            "gte" : "2025-01-01"
                        }
                    }
                }
            ]
        }
    }
}

# 결과
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0,
    "hits": [
      {
        "_index": "users",
        "_id": "3",
        "_score": 0,
        "_source": {
          "name": "park_saejin",
          "age": 32,
          "created_at": "2025-03-25"
        }
      }
    ]
  }
}

should

특정 조건을 만족하는 데이터를 상위에 노출시키고 싶을 때

예시

사용자가 특정 키워드로 검색했을때, 키워드와 관련된 데이터를 조회한다.
이때, 평점이 높거나 좋아요 수가 많은 상품이 상단에 노출되어야 한다.

데이터 세팅

PUT /products
{
    "mappings": {
        "properties": {
            "name": {
                "type": "text",
                "analyzer": "nori"
            },
            "rating": {
                "type": "double"
            },
            "likes": {
                "type": "integer"
            }
        }
    }
}

// 키워드("무선 이어폰")와 관련성은 적지만, 좋아요 수가 높고, 평점이 좋은 경우
POST /products/_doc/1
{
    "name": "무선 충전기 C타입",
    "rating": 4.9,
    "likes": 300
}

// 키워드("무선 이어폰")와 관련성은 높지만, 좋아요 수가 낮고, 평점도 낮은 경우
POST /products/_doc/2
{
    "name" : "소니 무선 이어폰 WF",
    "rating": 3.8,
    "likes": 15
}

// 키워드("무선 이어폰")와 관련성도 높고, 좋아요 수도 높고, 평점도 높은 경우
POST /products/_doc/3
{
    "name": "갤럭시 버즈2 무선 이어폰",
    "rating": 4.8,
    "likes": 310
}

// 키워드("무선 이어폰")와 관련성이 아예 없는데, 좋아요 수는 높고, 평점도 높은 경우
POST /products/_doc/4
{
    "name": "삼성 노트북 13인치",
    "rating": 5.0,
    "likes": 1000
}

결과

GET products/_search
{
    "query": {
        "bool": {
            "must": [
                {
                    "match" : {
                        "name": "무선 이어폰"
                    }
                }
            ],
            "should": [
              {
                "range": {
                  "rating": {
                    "gte": 4.5
                  }
                }
              },
              {
                "range": {
                    "likes": {
                      "gte": 100
                    }
                }
              }
            ]
        }
    }
}


# 결과
{
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "max_score": 3.0276947,
    "hits": [
      {
        "_index": "products",
        "_id": "3",
        "_score": 3.0276947,
        "_source": {
          "name": "갤럭시 버즈2 무선 이어폰",
          "rating": 4.8,
          "likes": 310
        }
      },
      {
        "_index": "products",
        "_id": "1",
        "_score": 2.3491573,
        "_source": {
          "name": "무선 충전기 C타입",
          "rating": 4.9,
          "likes": 300
        }
      },
      {
        "_index": "products",
        "_id": "2",
        "_score": 1.1223162,
        "_source": {
          "name": "소니 무선 이어폰 WF",
          "rating": 3.8,
          "likes": 15
        }
      }
    ]
  }
}

fuzziness

오타가 있어도 유사한 단어를 포함한 데이터를 조회하고 싶을 때

예시

사용자가 특정 키워드로 검색했을때, 키워드와 관련된 데이터를 조회한다.
이때, 평점이 높거나 좋아요 수가 많은 상품이 상단에 노출되어야 한다.

데이터 세팅

PUT /boards
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      }
    }
  }
}

POST /boards/_doc/1
{ "title": "elasticsearch 사용법" }

결과

GET /boards/_search
{
  "query": {
    "match": {
      "title": {
        "query": "elastiksearch",
        "fuzziness": 1
      }
    }
  }
}


# 결과
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.2655527,
    "hits": [
      {
        "_index": "boards",
        "_id": "1",
        "_score": 0.2655527,
        "_source": {
          "title": "elasticsearch 사용법"
        }
      }
    ]
  }
}

Multi Field

하나의 필드에 text와 keyword 타입을 동시에 사용하고 싶을 때

예시

데이터 세팅

PUT /products
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "nori"
      },
      "category": {
        "type": "text",
        "analyzer": "nori",
        "fields": {
          "raw": {
            "type": "keyword"
          }
        }
      }
    }
  }
}


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

결과

GET /products/_search
{
  "query": {
    "term": {
      "category.raw": "특수 가전제품"
    }
  }
}

# 결과
{
  "took": 0,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "products",
        "_id": "1",
        "_score": 0.2876821,
        "_source": {
          "name": "삼성 세탁기",
          "category": "특수 가전제품"
        }
      }
    ]
  }
}

자동완성기능

검색 키워드를 일부 입력했을 때 검색어를 추천
search_as_you_type

예시

데이터 세팅

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

POST /products/_doc
{
  "name": "곱창 돌김생김"
}

POST /products/_doc
{
  "name": "구운 돌김"
}

POST /products/_doc
{
  "name": "완도 곱창 돌김 100매"
}

POST /products/_doc
{
  "name": "삼성 노트북"
}

POST /products/_doc
{
  "name": "Nike 신발"
}

결과

GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "돌김 곱창",
      // you have th 라고 검색하면
      // you have 는 인덱스에 저장되는 토큰과 일치하는 데이터를 검색
      // th는 역인덱스 토큰중 th로 시작하는 데이터를 검색
      "type": "bool_prefix",
      "fields": [
        // 연속적으로 단어가 일치할수록 score 향상
        "name",
        "name._2gram",
        "name._3gram"
      ]
    }
  }
}

# 결과
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 3,
      "relation": "eq"
    },
    "max_score": 4.303034,
    "hits": [
      {
        "_index": "products",
        "_id": "E-2tzJwBx8CjKePGQB0I",
        "_score": 4.303034,
        "_source": {
          "name": "곱창 돌김생김"
        }
      },
      {
        "_index": "products",
        "_id": "Fe2tzJwBx8CjKePGQB0c",
        "_score": 3.9317808,
        "_source": {
          "name": "완도 곱창 돌김 100매"
        }
      },
      {
        "_index": "products",
        "_id": "FO2tzJwBx8CjKePGQB0V",
        "_score": 1.8573356,
        "_source": {
          "name": "구운 돌김"
        }
      }
    ]
  }
}
profile
성숙해지려고 노력하지 않으면 성숙하기까지 매우 많은 시간이 걸린다.

0개의 댓글