Elastic Search

Ch07. 한글 검색 확장 기능 - 한글 키워드 자동완성

webmaster 2025. 9. 28. 02:02
728x90
  • Suggest API는 영문을 처리하는데 적합하지만 한들을 처리하는 데는 적합하지 않다. 

Completion Suggest API를 이용한 한글 자동 완성

1. 인덱스 생성

PUT /ac_test
{
  "settings": {
    "index": {
      "number_of_shards": 5,
      "number_of_replicas": 1
    }
  }
}

2. 매핑 설정

PUT /ac_test/_mapping/ac_test
{
  "properties": {
    "itemSrc": {
      "type": "keyword"
    },
    "itemCompletion": {
      "type": "completion"
    }
  }
}
  • 자동 완성을 위해 필드 타입을 completion으로 설정해야한다.
  • itemSrc: 일반적인 매칭 검색 용도로 사용(keyword 타입)
  • itemCompletion: 자동 완성 용도로 사용하는 필드(Completion 타입)

3. 자동 완성 데이터 색인

POST /ac_test/_bulk
{ "index" : { "_index": "ac_test", "_id" : "1" } }
{ "itemSrc": "신혼", "itemCompletion": "신혼" }
{ "index" : { "_index": "ac_test", "_id" : "2" } }
{ "itemSrc": "신혼가전", "itemCompletion": "신혼가전" }
{ "index" : { "_index": "ac_test", "_id" : "3" } }
{ "itemSrc": "신혼가전특별전", "itemCompletion": "신혼가전특별전" }

 

4. 자동 완성 요청

GET /ac_test/_search
{
  "suggest": {
    "s1": {
      "prefix": "신혼",
      "completion": {
        "field": "itemCompletion",
        "size": 10
      }
    }
  }
}
  • 자동완성도 검색이기 때문에 _searchAPI를 이용하며, suggest 키워드를 사용해서 검색 요청을 해야 한다(match x)
  • 자동완성 같은 경우 리턴되는 문서의 수가 많기 때문에 size 속성이 추가로 제공된다.

Suggest API를 이용한 한글 자동 완성의 문제점

부분 일치 불가

GET /ac_test/_search
{
  "suggest": {
    "s1": {
      "prefix": "가전",
      "completion": {
        "field": "itemCompletion",
        "size": 10
      }
    }
  }
}
  • 키워드의 일부분으로는 자동 완성의 결과가 제공되지 않음
  • 자동 완성 결과는 없는데, 그 이유는 Completion Suggest API는 내부적으로 Prefix 방식의 매칭만 지원하고 있기 때문에 키워드의 시작 부분이 반드시 일치해야 결과로 제공하기 때문이다.
  • Completion Suggest API를 이용해 자동완성을 할 경우 키워드의 시작어를 알고 사용해야 한다.

한글 초성 검색 불가

ㅅㅎㄱㅈㅌㅂㅈ
  • Completion Suggest API를 이용한 자동 구현은 한글 자모 분석을 지원하지 않기 때문에 초성 검색이 불가능하다.

한글 자모 검색 불가

[키 입력 순서 1번]: (입력 없음)
[키 입력 순서 2번]: I → 시
[키 입력 순서 3번]: L → 신
[키 입력 순서 4번]: ㅎ → 싢
[키 입력 순서 5번]: (입력 없음) → 신 호
[키 입력 순서 6번]: L → 신 혼
  • 키가 입력되는 도중 유니코드가 변경되기 떄문에 검색 시 원하는 결과를 얻지 못할 수 있다.

직접 구현해 보는 한글 자동 완성

1. 루씬의 분석 기능 활용

방식 구현하는 이유
Completion Suggest API이용하는 이유 - 엘라스틱에서 자동완성을 위해 기본적으로 제공
- 제공되는 Completion 키워드를 이용해 인덱스 구현 시 바로 이용가능
- 데이터가 메모리에서 동작(빠름)
- 메모리에서 동작하므로 데이터 크기에 제약이 있다.
- 현재는 장방일치(prefix)만 제공
루씬을 이용해 직접 구현 -  직접 구현해야 하므로 관련 지식이 필요하다.
- 루씬의 역색인 구조를 기반으로 동작하기 때문에 상대적으로 느리다.
- 사실상 데이터 크기에 제약이 없다.
- 루씬이 제공하는 분석기로 원하는 방식을 모두 구현할 수 있다.
  • 루씬 API를 확장해서 제공하기 때문에 이를 활용해서 개발하면 된다.

2. 확장된 Ngram 검색 적용

부분 일치(전방, 부분, 후방 일치)를 가장 쉽게 구현하는 방법은 Ngram을 사용하는 것이다. Ngram 분석기는 엘라스틱 서치에서 기본적으로 제공하는 분석기로 단어 한 글자 한글자 단위로 잘라 토큰화하기 때문에 누락 없는 부분일치를 구현할 수 있다.

분석기 설명 예시
Ngram - 음절 단위로 토큰을 생성하기 떄문에 재현율이 높으나 정확도는 떨어진다.
- 첫 음절 기준 max_gram에서 지정한 길이만큼 토큰 생성
아버지가 방에 들어가신다.
1. 아버지가, 방에, 들어가신다
2.
[아, 아버, 아버지, 아버지가, 버, 버지, 버지가, 지, 지가, 가]
[방, 방에, 에]
[들, 들어, 들어가, 들어가신, 들어가신다, 어, 어가, 어가신, 어가신다, 가, 가신, 가신다, 신, 신다, 다]
Edge Ngram - 대부분 Ngram과 유사
- 지정한 토크나이저 특성에 따라 Ngram 발생 
아버지가 방에 들어가신다.
1. 아버지가, 방에, 들어가신다
2.
[아, 아버, 아버지, 아버지가]
[방, 방에]
[들, 들어, 들어가, 들어가신, 들어가신다]
Edge Ngram Back Analyzer - Edge Ngram과 반대로 동작하는 토크나이저를 사용
- 옵션으로 side:back을 설정해야 한다.
아버지가 방에 들어가신다
1. 아버지가 방에 들어가신다.
2. [아, 버, 아버, 지, 버지, 아버지, 가, 지가, 버지가, 아버지가]
[방, 에, 방에]
[들, 어, 들어, 가, 어가, 들어가, 신, 가신, 어가신, 들어가신, 다,. 신다, 가신다, 어가신다, 들어가신다]
  • 위 3가지 분석기를 활용하면 어떤 부분일치도 구현할 수 있다.

사용법

1. 토크나이저 등록

"ngram_tokenizer": {
  "type": "nGram",
  "min_gram": 1,
  "max_gram": 50,
  "token_chars": [
    "letter",
    "digit",
    "punctuation",
    "symbol"
  ]
}

"edge_ngram_tokenizer": {
  "type": "edgeNGram",
  "min_gram": 1,
  "max_gram": 50,
  "token_chars": [
    "letter",
    "digit",
    "punctuation",
    "symbol"
  ]
}

2. 필터 정의

"edge_ngram_filter_front": {
  "type": "edgeNGram",
  "min_gram": 1,
  "max_gram": 50,
  "side": "front"
}

"edge_ngram_filter_back": {
  "type": "edgeNGram",
  "min_gram": 1,
  "max_gram": 50,
  "side": "back"
}

3. Custom 분석기 정의

"ngram_analyzer": {
  "type": "custom",
  "tokenizer": "ngram_tokenizer",
  "filter": [
    "lowercase",
    "trim"
  ]
}

"edge_ngram_analyzer": {
  "type": "custom",
  "tokenizer": "edge_ngram_tokenizer",
  "filter": [
    "lowercase",
    "trim",
    "edge_ngram_filter_front"
  ]
}

"edge_ngram_analyzer_back": {
  "type": "custom",
  "tokenizer": "edge_ngram_tokenizer",
  "filter": [
    "lowercase",
    "trim",
    "edge_ngram_filter_back"
  ]
}

4. 인덱스 생성

PUT /ac_test2
{
  "settings": {
    "index": {
      "number_of_shards": 5,
      "number_of_replicas": 1
    },
    "analysis": {
      "tokenizer": {
        "ngram_tokenizer": {
          "type": "nGram",
          "min_gram": 1,
          "max_gram": 50,
          "token_chars": ["letter", "digit", "punctuation", "symbol"]
        },
        "edge_ngram_tokenizer": {
          "type": "edgeNGram",
          "min_gram": 1,
          "max_gram": 50,
          "token_chars": ["letter", "digit", "punctuation", "symbol"]
        }
      },
      "filter": {
        "edge_ngram_filter_front": {
          "type": "edgeNGram",
          "min_gram": 1,
          "max_gram": 50,
          "side": "front"
        },
        "edge_ngram_filter_back": {
          "type": "edgeNGram",
          "min_gram": 1,
          "max_gram": 50,
          "side": "back"
        }
      },
      "analyzer": {
        "ngram_analyzer": {
          "type": "custom",
          "tokenizer": "ngram_tokenizer",
          "filter": ["lowercase", "trim"]
        },
        "edge_ngram_analyzer": {
          "type": "custom",
          "tokenizer": "edge_ngram_tokenizer",
          "filter": ["lowercase", "trim", "edge_ngram_filter_front"]
        },
        "edge_ngram_analyzer_back": {
          "type": "custom",
          "tokenizer": "edge_ngram_tokenizer",
          "filter": ["lowercase", "trim", "edge_ngram_filter_back"]
        }
      }
    }
  }
}

5. 매핑 설정

PUT /ac_test2/_mapping/ac_test2
{
  "properties": {
    "item": {
      "type": "keyword",
      "boost": 30
    },
    "itemNgram": {
      "type": "text",
      "analyzer": "ngram_analyzer",
      "search_analyzer": "ngram_analyzer",
      "boost": 3
    },
    "itemNgramEdge": {
      "type": "text",
      "analyzer": "edge_ngram_analyzer",
      "search_analyzer": "ngram_analyzer",
      "boost": 2
    },
    "itemNgramEdgeBack": {
      "type": "text",
      "analyzer": "edge_ngram_analyzer_back",
      "search_analyzer": "ngram_analyzer",
      "boost": 1
    }
  }
}
  • item: 일반적인 매칭 검색 용도
  • itemNgram: Ngram으로 분석된 정보를 자동완성으로 매칭하기 위한 용도
  • itemNgramEdge: Edge Ngram로 분석된 정보를 자동완성으로 매칭하기 위한 필드(색인 시, edge_ngram_analyzer/ 분석 시 edge_analyzer를 사용한다)
  • itemNgramBackEdge: Edge Back Ngram로 분석된 정보를 자동완성으로 매칭하기 위한 필드(색인 시, edge_ngram_analyzer_back/ 분석 시 edge_analyzer를 사용한다)

6. 자동완성 데이터 색인

POST /ac_test2/_bulk
{ "index": { "_index": "ac_test2", "_id": "1" } }
{ 
  "item": "신혼",
  "itemNgram": "신혼",
  "itemNgramEdge": "신혼",
  "itemNgramEdgeBack": "신혼"
}
{ "index": { "_index": "ac_test2", "_id": "2" } }
{ 
  "item": "신혼가전",
  "itemNgram": "신혼가전",
  "itemNgramEdge": "신혼가전",
  "itemNgramEdgeBack": "신혼가전"
}
{ "index": { "_index": "ac_test2", "_id": "3" } }
{ 
  "item": "신혼가전특별전",
  "itemNgram": "신혼가전특별전",
  "itemNgramEdge": "신혼가전특별전",
  "itemNgramEdgeBack": "신혼가전특별전"
}

7. 자동 완성 요청

GET /ac_test2/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "prefix": {
            "item": "신혼"
          }
        },
        {
          "term": {
            "itemNgram": "신혼"
          }
        },
        {
          "term": {
            "itemNgramEdge": "신혼"
          }
        },
        {
          "term": {
            "itemNgramEdgeBack": "신혼"
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

한글 초성 검색 적용

1. 인덱스 생성

PUT /ac_test3
{
  "settings": {
    "number_of_shards": 5,
    "number_of_replicas": 1,
    "analysis": {
      "analyzer": {
        "chosung_index_analyzer": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": [
            "javacafe_chosung_filter",
            "lowercase",
            "trim",
            "edge_ngram_filter_front"
          ]
        },
        "chosung_search_analyzer": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": [
            "javacafe_chosung_filter",
            "lowercase",
            "trim"
          ]
        }
      },
      "tokenizer": {
        "edge_ngram_tokenizer": {
          "type": "edgeNGram",
          "min_gram": 1,
          "max_gram": 50,
          "token_chars": ["letter", "digit", "punctuation", "symbol"]
        }
      },
      "filter": {
        "edge_ngram_filter_front": {
          "type": "edgeNGram",
          "min_gram": 1,
          "max_gram": 50,
          "side": "front"
        },
        "javacafe_chosung_filter": {
          "type": "javacafe_chosung"
        }
      }
    }
  }
}

2. 매핑 설정

PUT /ac_test3/_mapping
{
  "properties": {
    "item": {
      "type": "keyword",
      "boost": 30
    },
    "itemChosung": {
      "type": "text",
      "analyzer": "chosung_index_analyzer",
      "search_analyzer": "chosung_search_analyzer",
      "boost": 10
    }
  }
}
  • 초성 분석기를 갖도록 필드 정의
  • item: 일반적인 매칭 검색 용도로 사용
  • itemChosung: 초성으로 분석된 정보를 자동완성으로 매칭하기 위한 필드
    • 필드 정의 시 index_analyzer와 search_ananlyzer를 다르게 등록해야 한다

3. 자동완성 데이터 색인

POST /ac_test3/_bulk
{ "index": { "_index": "ac_test3", "_id": "1" } }
{ "item": "신혼", "itemChosung": "신혼" }
{ "index": { "_index": "ac_test3", "_id": "2" } }
{ "item": "신혼가전", "itemChosung": "신혼가전" }
{ "index": { "_index": "ac_test3", "_id": "3" } }
{ "item": "신혼가전특별전", "itemChosung": "신혼가전특별전" }

4. 자동 완성 요청

POST /ac_test3/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "term": {
            "itemChosung": {
              "value": "ㅅㅎㄱㅈ"
            }
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

 

한글 자모 검색 적용

1. 인덱스 생성

PUT /ac_test4
{
  "settings": {
    "index": {
      "number_of_shards": 5,
      "number_of_replicas": 1
    },
    "analysis": {
      "analyzer": {
        "jamo_index_analyzer": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": [
            "javacafe_jamo_filter",
            "lowercase",
            "trim",
            "edge_ngram_filter_front"
          ]
        },
        "jamo_search_analyzer": {
          "type": "custom",
          "tokenizer": "keyword",
          "filter": [
            "javacafe_jamo_filter",
            "lowercase",
            "trim"
          ]
        }
      },
      "tokenizer": {
        "edge_ngram_tokenizer": {
          "type": "edgeNGram",
          "min_gram": "1",
          "max_gram": "50",
          "token_chars": ["letter", "digit", "punctuation", "symbol"]
        }
      },
      "filter": {
        "edge_ngram_filter_front": {
          "type": "edgeNGram",
          "min_gram": "1",
          "max_gram": "50",
          "side": "front"
        },
        "javacafe_jamo_filter": {
          "type": "javacafe_jamo"
        }
      }
    }
  }
}

2. 자동 매핑

PUT /ac_test4/_mapping/ac_test4
{
  "properties": {
    "item": {
      "type": "keyword",
      "boost": 30
    },
    "itemJamo": {
      "type": "text",
      "analyzer": "jamo_index_analyzer",
      "search_analyzer": "jamo_search_analyzer",
      "boost": 10
    }
  }
}
  • item: 일반적인 매칭 검색 용도
  • itemJamo: 자모 분석된 정보를 자동완성으로 매칭하기 위한 필드
    • 필드 정의시 index_analyzer와 search_ananlyzer를 다르게 등록해야함

3. 자동완성 데이터 색인

POST /ac_test4/_bulk
{ "index": { "_index": "ac_test4", "_id": "1" } }
{ "item": "신혼", "itemJamo": "신혼" }
{ "index": { "_index": "ac_test4", "_id": "2" } }
{ "item": "신혼가전", "itemJamo": "신혼가전" }
{ "index": { "_index": "ac_test4", "_id": "3" } }
{ "item": "신혼가전특별전", "itemJamo": "신혼가전특별전" }

4. 자동 완성 요청

GET /ac_test4/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "prefix": {
            "itemJamo": "ㅅㅣㄴㅎ"
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}
728x90