엘라스틱 서치 검색 기능 및 작동원리

WAS·2025년 7월 10일
0

Elasticsearch

목록 보기
2/3

✅ 역 인덱스

  • 필드 값을 단어마다 쪼개서 찾기 쉽게 정리해놓은 목록

✅ 역 인덱스의 원리

PUT /products  // products 인덱스(테이블)을생성하면서 매핑(스키마) 하기
{
    "mappings": {
        "properties": {
            "name" : {
                "type": "text"
            }
        }
    }
}

POST /products/_create/1 // id : 1
{
  "name": "Apple 2025 맥북 에어 13 M4 10코어"
}

POST /products/_create/2 // id : 2
{
  "name": "Apple 2024 에어팟 4세대"
}

POST /products/_create/3 // id : 3
{
  "name": "Apple 2024 아이패드 mini A17 Pro"
}


GET /products/_search // 특정 단어(문장)을 포함한 도큐먼트를 조회하기
{
    "query": {
        "match": {
          "name" : "Apple 2024 아이패드"
        }
    }
}

위와 같이 3개의 도큐먼트를 엘라스틱 서치에 저장한다고 가정해보자
그러면 내부적으로 데이터가 단어단위로 잘라지고 역인덱스 로 저장이 된다

  1. 단어 단위로 자르기
    [Apple, 2025, 맥북, 에어, 13, M4, 10코어]
    [Apple, 2024, 에어팟, 4세대]
    [Apple, 2024, 아이패드, mini, A17, Pro]

💡 필드값에서 추출되어 역 인덱스에 저장된 단어를 토큰 이라고 부름
💡 생성된 역 인덱스는 시스템 내부적으로만 생성이 되어서 눈으로 확인 불가능

  1. 역 인덱스로 저장

  2. 검색을 할 경우 역 인덱스 활용
    EX) Apple 2024 아이패드 를 검색하면 역 인덱스를 활용하여 일치하는 단어가 많은
    도큐먼트를 우선적으로 조회함

    id가 1 도큐먼트는 토큰이 1개 일치
    id가 2 도큐먼트는 토큰이 2개 일치
    id가 3 도큐먼트는 토큰이 3개 일치

    즉 id가 3인 도큐먼트가 제일 먼저 조회됨
    ElasticSearch 가 자체적인 로직으로 점수를 매겨서 높은순으로 도큐먼트를 조회한다

대략적으로 점수를 계산하는 로직에는 3가지 정도로 판단된다
1. 문서 내에서 검색어가 얼마나 자주 등장하는지? -> 많이 등장할수록 점수 UP
2. 검색어가 전체 문서 중 얼마나 희귀하는지? -> 희귀할수록 점수 UP
3. 문서(필드)가 짧을수록 점수 UP

ElasticSearch는 역 인덱스의 기능을 가지고 있기 때문에, 단어의 순서랑 상관없이 도큐먼트를 조회 가능함
참고로 이러한 구조는 데이터 타입의 text 타입 한해서만 해당한다


✅ 애널라이저 (Analyzer)

  • 텍스트 데이터를 검색 가능하게 만들기 위해 분석하고 처리하는 도구로
    쉽게 말해 문장을 잘게 쪼개고 정리해서 검색하기 쉽게 바꾸는 도구이다

문자열 -> 토큰으로 만들어주는 도구
캐릭터 필터, 토크나이저, 토큰필터 이렇게 3가지 전부 합쳐서 애널라이저라 함

캐릭터 필터 : 문자열을 토큰으로 자르기전에 문자열을 다듬는 역할을 함
다양한 종류의 필터가 존재하며, 여러 개의 필터를 적용시킬 수 있음
💡 기본값으로는 캐릭터 필터가 설정되어 있지 않음
ex) html_strip 필터 적용 (html 태그 제거)
<h1>아이폰 15 사용 후기</h1> -> 아이폰 15 사용 후기


토크나이저 : 문자열을 토큰으로 자르는 역할을 함
💡 기본값으로 standard 토크나이저 가 적용되어 있으며
공백 , . ! ? 와 같은 문장 부호를 기준으로 자름
ex) The Brown-Foxes jumped over the roof.
→ [The, Brown, Foxes, jumped, over, the, roof]


토큰 필터 : 잘린 토큰을 최종적으로 다듬는 역할을 함
다양한 종류의 필터가 존재하며, 여러 개의 필터를 적용시킬 수 있음
💡 기본값으로는 lowercase 필터가 적용되어 있음
ex) lowercase 필터 적용 (소문자로 변환)
[The, Brown, Foxes, jumped, over, the, roof]
→ [the, brown, foxes, jumped, over, the, roof]

ex) stop 필터 적용 (a, the, is와 같은 특별한 의미를 가지지 않는 단어 제거)
[the, brown, foxes, jumped, over, the, roof]
→ [brown, foxes, jumped, roof]

ex) stemmer 필터 적용 (단어의 원래 형태로 변환)
[brown, foxes, jumped, roof]
→ [brown, fox, jump, roof]


✅ 애널라이저가 토큰을 나누는 방식

방법 1 : "analyzer": "standard"
-> analyzer 를 standart 기본값으로 명시하는 방법

방법 2

"char_filter": [], 
"tokenizer": "standard", 
"filter": ["lowercase"]

-> standard analyer의 구성을 직접 명시하는 방법

아래와 같이 입력하면 토큰이 나눠지는 것을 볼 수 있다
GET /_analyze 란?
Elasticsearch에서 텍스트가 Analyzer에 의해 어떻게 처리되는지 직접 확인할 수 있게 해주는 분석 도구 API

// 방법 1 
GET /_analyze
{
  "text": "Apple 2025 맥북 에어 13 M4 10코어",
  "analyzer": "standard"
}

// 방법 2
GET /_analyze
{
  "text": "Apple 2025 맥북 에어 13 M4 10코어",
  "char_filter": [],
  "tokenizer": "standard",
  "filter": ["lowercase"]
}

✅ 대소문자 구분없이 검색하는 방법 (예시)

PUT /products // 인덱스 생성 + 매핑 정의 + Custom Analyzer 적용
{
  "settings": {
    "analysis": {
      "analyzer": {
        "products_name_analyzer": {
          "char_filter": [],
          "tokenizer": "standard",
          "filter": ["lowercase"] // lowercase token filter 추가
        }
      }
    }
  },
  "mappings": {
	  "properties": {
	    "name": {
	      "type": "text",
	      "analyzer": "products_name_analyzer"
	    }
	  }
	}
}

POST /products/_create/1 // id가 1값인 데이터 삽입
{
  "name": "Apple 2025 맥북 에어 13 M4 10코어"
}

이런식으로 데이터를 삽입한 후 조회해보도록 하겠다

GET /products/_search // 조회 잘됨
{
  "query": {
    "match": {
      "name": "apple" 
    }
  }
}

GET /products/_search // 조회 잘됨
{
  "query": {
    "match": {
      "name": "Apple"
    }
  }
}

둘다 조회가 잘 되는것을 확인할 수 있다
이제 Analyze API 사용해서 분석을 해보자

GET /products/_analyze
{
  "field": "name"
  "text": "Apple 2025 맥북 에어 13 M4 10코어"
}


위 사진처럼 도큐먼트를 생성할 때 Analyzer가 문자열을 분리해서 역인덱스를 생성하기 때문에
토큰이 나눠지는것을 확인할 수 있다
그러면 토큰에는 소문자로 apple 이 저장되있어서 text를 apple로 검색했을 때 나오는 것은 알겠는데
왜 대문자로 Apple 을 검색했을 때는 조회가 잘 되는가?

💡 그 이유는 검색을 할때의 Text 값도 Analyzer을 사용하여 토큰을 분리시킨 다음
실제 역인덱스 값이랑 비교하기 때문에 조회할 수 있는 것이다

그래서 사용자는 대소문자를 신경쓰지 않고 데이터를 조회할 수 있다


html_strip (HTML 태그 제거)
💡 전체적인 예시는 위에서 Custom Analyzer 적용할 때
character filter 값만 바꿔주면 되기때문에 생략하겠다 => character filter : ["html_strip"]

게시글 서비스에는 굵게, 기울임, 링크 등을 포함해서 글을 작성하기 때문에
HTML태그를 포함해서 DB에 저장하는 경우가 있다

POST /boards/_doc
{
  "content": "<h1>이 물품 팝니다/h1>"
}

위처럼 HTML 태그가 포함되있는 상태에서 데이터가 저장된 상태에서 h1을 검색하면

GET /boards/_search
{
  "query": {
    "match": {
      "content": "h1"
    }
  }
}

데이터가 조회되는것을 확인할 수 있다
하지만 실제 글과 상관없는 h1이라는 키워드로 게시글이 조회된다면 검색 품질이 떨어지는 것이다
실제로 어떤식으로 토큰이 저장되어있는지 확인해보자

GET /boards/_analyze
{
  "field": "content",
  "text": "<h1>Running cats, jumping quickly — over the lazy dogs!</h1>"
}

결과값으로 앞뒤로 h1 태그들도 역 인덱스에 저장되는것을 확인할 수 있어서 디스크 공간을 낭비한다
이러한 이유로 html_strip 를 사용하여 HTML 태그를 제거하고 저장을 해야한다


✅ 검색할 때 필요없는 불용어 제거하기 = stop

사용자가 검색할 때 그 사과는 맛있다 라고 입력하면
사실 검색에서 중요한 단어는 사과맛있다는 필요없는 단어이기 때문에 제거하는게 좋다
필요없는 단어를 제거해야 역인덱스의 저장공간을 절약할 수 있다

영어: "a", "the", "is", "at", "which", "on", "in"

한글: "이", "그", "저", "그리고", "하지만", "또는", "은", "는", "이", "가", "에", "의"

적용방법은 filter 부분에 stop 을 추가하면 된다
filter": ["lowercase", "stop"]


✅ 단어의 형태에 상관없이 검색하는 방법 = stemmer
stemmer : 어미, 접미사 등을 제거해서 어근만 남기는 것

ex) 영어
running, ran, runs -> run
playing, played, player -> play

ex) 한글
먹었다, 먹고, 먹는먹다
사랑한다, 사랑해서, 사랑할사랑

이렇게 형태는 달라도 같은 의미인 단어들을 하나의 형태로 통일시켜서
검색 시 더 정확한 매칭이 되도록 도움을 준다

적용방법은 filter 부분에 stemmer 를 추가하면 된다

stemmer 를 적용하면 영단어나 한글을 기본형으로 변환한 후 토큰에 저장한다
그러면 토큰에는 사랑 이라고 저장되있는데 왜 사랑한다 를 검색하면 조회될까?
아까 위에서 말한 예시와 동일하다
사랑한다 라고 검색했을 때, stemmer 에 의해 기본형인 사랑 으로 바뀐채로 검색을 하기 때문이다


✅ 동의어로 검색하는 방법 = synonym
synonym 필터는 동의어 필터로 동일하거나 유사한 의미의 단어들을 하나처럼 취급한다
적용방법은 filtersynonym 추가하고나서 세부적으로 매칭을 추가시켜야함
ex) 나는 랩탑을 새로 샀다 에서 노트북 이라고 검색했을 때, 인식을 시키고 싶으면?

PUT /korean_synonym_test
{
  "settings": {
    "analysis": {
      "filter": {
        "korean_synonym_filter": {
          "type": "synonym",
          "synonyms": [
            "노트북, 랩탑", // 노트북, 랩탑 입력시 다 매칭
            "핸드폰, 휴대폰, 스마트폰", // 핸드폰, 휴대폰, 스마트폰 입력시 다 매칭
            "자동차, 차량, 차" // 자동차, 차량, 차 입력시 다 매칭
          ]
        }
      },
      "analyzer": {
        "korean_synonym_analyzer": {
          "tokenizer": "standard",
          "filter": ["lowercase", "korean_synonym_filter"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "korean_synonym_analyzer"
      }
    }
  }
} 

다음과 같이 저장했을 때, 토큰을 분석하면 사진과 같이 동의어들이 토큰에 저장되는것을 확인할 수 있다

GET /products/_analyze
{
    "field": "name",
    "text": "노트북 스마트폰 차량"
}


Nori Analyzer

"filter": ["lowercase", "stop", "stemmer"] 로 인덱스를 생성했을 때
영어와 같은 경우 띄어쓰기로 단어가 명확하게 구분되어 토큰으로 저장이 된다
ex) "text": "I like bananas" -> i like banana

하지만 한글과 같은 경우 ex) "text": "백화점에서 쇼핑을 하다가 친구를 만났다."
-> 백화점에서 쇼핑을 하다가 친구를 만났다 단순히 띄어쓰기로 토큰이 저장된다
이럴경우 사용자가 백화점 쇼핑 친구 라는 키워드로 검색하면 조회되지가 않는 문제가 발생한다

이러한 문제점을 해결하기 위해 한글에 맞는 Nori Analyzer 를 써야 한다.
Nori Analyzer 를 사용하려면 플러그인을 설치해야한다

도커 예시

# Dockerfile
FROM docker.elastic.co/elasticsearch/elasticsearch:8.17.4 

# Nori Analyzer 플러그인 설치
RUN bin/elasticsearch-plugin install analysis-nori 
services:
  elastic:
    build:
      context: .
      dockerfile: Dockerfile  # 위에 저장한 Dockerfile 넣기
    ports:
      - 9200:9200 # 9200번 포트에서 Elasticsearch 실행
    environment:
      # 아래 설정은 개발/테스트 환경에서 간단하게 테스트하기 위한 옵션 (운영 환경에서는 설정하면 안 됨)
      - discovery.type=single-node # 단일 노드 
      - xpack.security.enabled=false # 보안 설정
      - xpack.security.http.ssl.enabled=false # 보안 설정
  kibana:
    image: docker.elastic.co/kibana/kibana:8.17.4 # 8.17.4 버전
    ports:
      - 5601:5601 # 5601번 포트에서 kibana 실행
    environment:
      - ELASTICSEARCH_HOSTS=http://elastic:9200 # kibana에게 통신할 Elasticsearch 주소 알려주기

이런식으로 바꿔서 컨테이너를 띄우고 Analyze API 활용해 디버깅 해보면

GET /_analyze
{
  "text": "백화점에서 쇼핑을 하다가 친구를 만났다.",
  "analyzer": "nori"
}

## 위 아래 동일한 방법이다 (아래 방법은 Nori Analyze 와 동일한 애널라이저이다)

GET /_analyze
{
  "text": "백화점에서 쇼핑을 하다가 친구를 만났다.",
  "char_filter": [], 
	"tokenizer": "nori_tokenizer", 
	"filter": ["nori_part_of_speech", "nori_readingform", "lowercase"]
}
# `nori_part_of_speech` : 의미 없는 조사(``, ``), 접속사 등을 제거
# `nori_readingform` : 한자를 한글로 바꿔서 토큰으로 저장

백화 쇼핑 친구 만나 로 토큰이 저장되어 있다
이제 백화점 쇼핑 친구 키워드로 검새하면 잘 조회되는것을 확인할 수 있다

그러면 한글과 영어를 섞어서 검색하려면 어떻게 해야할까?

정답은 Nori analyzer 를 사용하고 필드값의 특징에 따라
character filtertoken filter 를 추가해주면 된다

ex) Nori analyzer 에 불용어 제거와 단어의 기본형태로 저장하기

// Nori analyzer의 구성을 직접 명시
GET /_analyze
{
  "text": "오늘 영어 책에서 'It depends on the results.'이라는 문구를 봤다.",
  "char_filter": [], 
	"tokenizer": "nori_tokenizer", 
	"filter": ["nori_part_of_speech", "nori_readingform", "lowercase", "stop", "stemmer"]
}
profile
우측 상단 햇님모양 클릭하셔서 무조건 야간모드로 봐주세요!!

0개의 댓글