/erickor

엘라스틱서치 자동완성,초성검색,오타교정을 위한 한글 플러그인

Primary LanguageKotlinMIT LicenseMIT

EricKor ElasticSearch korean token filter plugin

엘라스틱서치 한글 관련 토큰 필터 플러그인입니다.

해당 필터를 사용하여 자동완성, 초성검색, 오타교정 등을 구현할 수 있습니다.

본 프로젝트는 Kotlin, JDK17, Gradle 7.6버전을 사용하여 제작되었습니다.

아래 섹션에 해당 플러그인을 사용한 자동완성, 초성검색, 오타교정을 구현한 예제가 구현되어 있습니다.

본 플러그인이 유용하다면, Github Star 부탁드립니다 >.<

Versions

[note] ElasticSearch Released Versions

Plugin Version

프로젝트 구조

├── kotlin
│   ├── constants : 한글,영어의 초,중,종성별 유니코드 변환 맵
│   ├── filters : 플러그인 구현체
│   ├── plugin : 구현한 필터들을 플러그인의 내부 필터로 등록하기 위한 클래스 
│   └── util : 플러그인 내부 로직
└── yamlRestTest
    ├── kotlin
    │     └── FilterTest : Rest test를 위한 테스트 클래스
    └── resources
          └── rest-api-spec
                └── plugin
                      └── filter.yml : Rest test 시나리오 파일 

필터 목록

erickor_jamo Filter (자모 분리 필터)

한글 문자열의 자음과 모음을 분리하는 필터입니다.

해당 필터와 Edge-Ngram or Ngram 토큰필터를 함께 사용해 자동완성 기능을 구현할 수 있습니다.

안녕하세요 반갑습니다 -> [ㅇㅏㄴㄴㅕㅇㅎㅏㅅㅔㅇㅛ], [ㅂㅏㄴㄱㅏㅂㅅㅡㅂㄴㅣㄷㅏ]

erickor_chosung Filter (초성 분리 필터)

한글 문자열의 초성을 추출하는 필터입니다.

해당 필터를 사용하여 초성검색을 구현할 수 있습니다.

여기어때 -> [ㅇㄱㅇㄸ]
엘라스틱서치 -> [ㅇㄹㅅㅌㅅㅊ]

erickor_hantoeng Filter (한글->영어 변환 필터)

한글 문자열을 자모를 분해한 후 키보드에서 해당 글자와 일치하는 영어 문자열로 변환합니다.

해당 필터를 사용하여 사용자의 영어 오입력에 대해 올바른 결과를 찾을 수 있습니다.

안녕하세요 반갑습니다 -> [dkssudgktpdy], [qksrkqtmqslek]

erickor_engtohan Filter (영어->한글 변환 필터)

영어 문자열을 키보드에서 해당 글자와 일치하는 한글 문자열로 변환합니다.

해당 필터를 사용하여 사용자의 한글 오입력에 대해 올바른 결과를 찾을 수 있습니다.

dkssudgktpdy qksrkqtmqslek -> [안녕하세요], [반갑습니다]

필터 등록

작성한 필터를 플러그인에 등록하기 위하여, /src/main/kotlin/plugin/EricKorPlugin에서 Map<String, AnalysisProvider<TokenFilterFactory>> 구조로 필터를 등록합니다.

프로젝트 빌드 및 설치

gradle clean
gradle assemble

assemble 후 /build/distributions/erickor-{version}.zip 파일이 생성됩니다.

해당 zip 파일을 엘라스틱서치에서 등록합니다.

sudo bin/elasticsearch-plugin install file:///path/directory/filename

플러그인을 도커에서 설치하기 위해서는, Dockerfile에서 이미지 빌드시 플러그인을 함께 설치하도록 할 수 있습니다.

COPY plugins/erickor-1.0.zip /plugins/erickor-1.0.zip
RUN elasticsearch-plugin install file:///plugins/erickor-1.0.zip

설치가 완료되었다면, 엘라스틱서치를 재시작하여 플러그인을 적용할 수 있습니다.

설치된 플러그인 목록을 확인하려면, 아래 API를 호출하여 확인할 수 있습니다.

GET /_cat/plugins

Yaml Rest Test

gradle yamlRestTest

작성한 필터의 테스트를 위해서는, yamlRestTest 기능을 이용하면 됩니다. 해당 기능이 실행되면 elasticsearch test cluster가 실행되고, 프로젝트의 플러그인을 불러온 뒤, Yaml 구조에 따라 테스트를 실행합니다.

/src/main/yamlRestTest/kotlin/FilterTest에서 testCandidate를 등록하고,

/src/main/yamlRestTest/resources/rest-api-spec/test/plugin/filter.yml에 작성한 내용대로 필터를 test하게 됩니다.

참고문서

공식문서

공식문서 github


Example

1. 오타교정

특정 필드에 대해 오타교정을 구현하기 위해서는 자모분리 필터를 활용할 수 있습니다. 오타교정을 위한 필드에 자모분리 필터(erickor_jamo)를 추가하고, Term Suggest API를 사용하여 오타교정 기능을 구현합니다.

1. 인덱스 생성
PUT /test1
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "index.max_ngram_diff": 10,
    "analysis": {
      "analyzer": {
        "custom_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "erickor_jamo"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "custom_analyzer"
      }
    }
  }
}

2. 색인
POST test1/_bulk
{ "index" : { "_id" : "doc1" } }
{ "name" : "코틀린" }

3. 오타교정
POST /test1/_search
{
  "suggest": {
    "name_suggest": {
      "text": "코틀란",
      "term": {
        "field": "name",
        "max_edits": 2
      }
    }
  }
}

4. 결과
{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 0,
      "relation": "eq"
    },
    "max_score": null,
    "hits": []
  },
  "suggest": {
    "name_suggest": [
      {
        "text": "ㅋㅗㅌㅡㄹㄹㅏㄴ",
        "offset": 0,
        "length": 3,
        "options": [
          {
            "text": "ㅋㅗㅌㅡㄹㄹㅣㄴ",
            "score": 0.875,
            "freq": 1
          }
        ]
      }
    ]
  }
}

2. 자동완성

ngram 또는 edge-ngram 토큰 필터와 함께 사용하여 자동완성을 구현할 수 있습니다.

인덱싱용 분석기에는 자모분리필터와 ngram필터를 사용하고, 검색용 분석기에는 자모분리 필터만 사용하여야 합니다.

1. 인덱스 생성
PUT /test2
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "index.max_ngram_diff": 10,
    "analysis": {
      "filter": {
        "1_10_ngram_filter": {
          "type": "ngram",
          "min_gram": 1,
          "max_gram": 10
        }
      },
      "analyzer": {
        "search_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "erickor_jamo"
          ]
        },
        "index_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "erickor_jamo",
            "1_10_ngram_filter"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "index_analyzer",
        "search_analyzer": "search_analyzer"
      }
    }
  }
}

2. 색인
POST test2/_bulk
{ "index" : { "_id" : "doc1" } }
{ "name" : "코틀린" }
{ "index" : { "_id" : "doc2" } }
{ "name" : "맛있는 코카콜라" }

3. 자동완성
POST /test2/_search
{
  "query": {
    "match": {
      "name": "코"
    }
  }
}

4. 결과
{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 0.34450948,
    "hits": [
      {
        "_index": "test2",
        "_id": "doc2",
        "_score": 0.34450948,
        "_source": {
          "name": "맛있는 코카콜라"
        }
      },
      {
        "_index": "test2",
        "_id": "doc1",
        "_score": 0.30519044,
        "_source": {
          "name": "코틀린"
        }
      }
    ]
  }
}

3. 한/영 오타 교정

hantoeng_analyzer와 engtohan_analyzer 분석기를 구현하고 교정을 위한 필드에 분석기를 등록합니다.

1. 인덱스 생성
PUT /test3
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "index.max_ngram_diff": 10,
    "analysis": {
      "analyzer": {
        "hantoeng_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "erickor_hantoeng"
          ]
        },
        "engtohan_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "erickor_engtohan"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "keyword",
        "copy_to": [
          "name_hantoeng",
          "name_engtohan"
        ]
      },
      "name_hantoeng": {
        "type": "text",
        "analyzer": "hantoeng_analyzer"
      },
      "name_engtohan": {
        "type": "text",
        "analyzer": "engtohan_analyzer"
      }
    }
  }
}

2. 색인
POST test3/_bulk
{ "index" : { "_id" : "doc1" } }
{ "name" : "코틀린" }
{ "index" : { "_id" : "doc2" } }
{ "name" : "cocacola" }

3. 한영 오타교정
POST /test3/_search
{
  "query": {
    "match": {
      "name_engtohan": "zhxmffls"
    }
  }
}

{
  "query": {
    "match": {
      "name_hantoeng": "챛ㅁ채ㅣㅁ"
    }
  }
}

4. 결과
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.6931471,
    "hits": [
      {
        "_index": "test3",
        "_id": "doc1",
        "_score": 0.6931471,
        "_source": {
          "name": "코틀린"
        }
      }
    ]
  }
}


{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.6931471,
    "hits": [
      {
        "_index": "test3",
        "_id": "doc2",
        "_score": 0.6931471,
        "_source": {
          "name": "cocacola"
        }
      }
    ]
  }
}

4. 초성검색

초성 분리 필터인 erickor_chosung를 분석기에 등록하여 초성검색을 구현합니다.

1. 인덱스 생성
PUT /test4
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0,
    "index.max_ngram_diff": 10,
    "analysis": {
      "analyzer": {
        "chosung_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": [
            "lowercase",
            "erickor_chosung"
          ]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "chosung_analyzer"
      }
    }
  }
}

2. 색인
POST test4/_bulk
{ "index" : { "_id" : "doc1" } }
{ "name" : "코틀린" }

3. 초성검색
POST /test4/_search
{
  "query": {
    "match": {
      "name": "ㅋㅌㄹ"
    }
  }
}

4. 결과
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 0.2876821,
    "hits": [
      {
        "_index": "test4",
        "_id": "doc1",
        "_score": 0.2876821,
        "_source": {
          "name": "코틀린"
        }
      }
    ]
  }
}