이 문서는 Nori를 개발한 Jim Ferenczi 님(프랑스 엔지니어)이 작성한 'How to use a custom dictionary in Nori'의 문서를 기반으로 Nori, Lucene, Elasticsearch 버전을 '21. 12 기준의 릴리즈 버전으로 빌드하기 위해서 원문을 한글로 번역하고 일부분은 보완한 문서 입니다.
https://github.com/jimczi/nori/blob/master/how-to-custom-dict.asciidoc
Lucene의 한국어 분석기인 Nori는 mecab-ko-dic의 특정 버전(mecab-ko-dic-2.0.3-20170922)에서 빌드되었습니다.
이 문서에서는 사용자 지정 사전을 사용하는 배포판을 만드는 방법을 보여줍니다.
이 작업은 수동으로 수행되며 여러 단계가 필요하므로 기존 사전에 일부 단어를 추가하려는 경우에는 이 작업을 수행하지 마십시오.
이것은 Elasticsearch’s plugin 자체에서 사용자 사전을 제공하여 수행되지만 도메인별 어휘가 많으면(수 천개) 추가 규칙을 사용하여 원래 사전을 다시 빌드한 이후에는 다음과 같은 장점을 얻을 수 있습니다:
Lucene 과 Elasticsearch를 컴파일 해야 하므로 로컬 시스템에 gradle 7.2+
및 java 17+
가 설치되어 있는지 확인하십시오.
로컬 시스템으로 MacOS를 사용하는 경우 컴파일에 필요한 autoconf
, automake
및 libtool
을 설치합니다.
$ brew install autoconf automake libtool coreutils
또한 MacOS에서는 ._ 백업 파일이 생성되는 것을 방지하기 위해서 .zshrc 파일또는 .bash_profile에 환경변수를 추가 합니다.
참고 : https://astrocoke.tistory.com/19
export COPYFILE_DISABLE=1
먼저 MeCab을 설치해야 합니다, mecab-ko를 다운로드 하십시오. (한국어용 MeCab fork) 이곳에서 다운로드 하세요.
다운로드 받은 파일의 압축을 해제한 디렉토리에서 다음의 명령을 실행하여 MeCab을 설치합니다:
$ ./configure
$ make
$ sudo make install
컴파일이 완료된 이후에 아래와 같이 실행할 수 있어야 합니다:
$ mecab -v
mecab of 0.996/ko-0.9.2
mecab-ko-dic (다운로드)의 최신 버전을 다운로드 합니다. Nori가 기본적으로 사용하는 사전입니다. 다운로드 받은 파일의 압축을 해제한 디렉토리로 이동합니다:
$ tar xvf mecab-ko-dic-2.1.1-20180720.tar.gz
$ cd mecab-ko-dic-2.1.1-20180720
다음의 명령을 실행하여 사전을 설치합니다:
$ autoreconf
$ ./configure
$ make
$ sudo make install
이 섹션에서는 이전 단계에서 다운로드 한 원래 배포판에 사용자 정의 단어를 추가하는 방법을 설명합니다.
mecab-ko-dic의 user-dic
디렉토리에 csv
확장자 (예: custom-words.csv
)의 파일을 만듭니다. 디렉토리는 다음과 같습니다:
$ ls
AUTHORS EP.csv IC.csv MM.csv NNBC.csv Person-actor.csv README VX.csv XSV.csv config.log install-sh model.def unk.def
COPYING ETM.csv INSTALL Makefile NNG.csv Person.csv Symbol.csv Wikipedia.csv aclocal.m4 config.status left-id.def pos-id.def unk.dic
ChangeLog ETN.csv Inflect.csv Makefile.am NNP.csv Place-address.csv VA.csv XPN.csv autogen.sh configure matrix.bin rewrite.def user-dic
CoinedWord.csv Foreign.csv J.csv Makefile.in NP.csv Place-station.csv VCN.csv XR.csv char.bin configure.ac matrix.def right-id.def
EC.csv Group.csv MAG.csv NEWS NR.csv Place.csv VCP.csv XSA.csv char.def dicrc missing sys.dic
EF.csv Hanja.csv MAJ.csv NNB.csv NorthKorea.csv Preanalysis.csv VV.csv XSN.csv clean feature.def model.bin tools
$ ls user-dic
README.md custom-words.csv nnp.csv person.csv place.csv
다른 파일 person.csv
, place.csv
및 nnp.csv
를 삭제합니다. (사용자 정의 항목의 예가 포함되어 있습니다):
$ rm user-dic/person.csv user-dic/place.csv user-dic/nnp.csv
$ ls user-dic
README.md custom-words.csv
주의 | 추가 파일 삭제는 필수 입니다, place.csv 에는 Nori가 구문 분석할 수 없는 항목이 들어 있습니다. |
---|
csv 파일에 사용자 정의 단어를 추가합니다:
대우,,,,NNP,*,F,대우,*,*,*,*
구글,,,,NNP,*,T,구글,*,*,*,*
까비,,,,NNP,인명,F,까비,*,*,*,*
세종,,,,NNP,지명,T,세종,*,*,*,*
세종시,,,,NNP,지명,F,세종시,Compound,*,*,세종/NNP/지명+시/NNG/*
다른 품사를 추가해야 하는 경우 전체 목록을 이곳에서 확인 할 수 있습니다.
다음의 명령을 실행하십시오:
$ ./tools/add-userdic.sh
MacOs에서 'no such file or directory' 에러가 발생하면 아래의 URL을 참고하여 add-userdic.sh를 수정해 줍니다.
no such file or directory:/../dicrc 해결하기
https://lsjsj92.tistory.com/585
실행한 후 mecab-ko-dic 디렉토리는 다음과 같습니다:
$ls
AUTHORS ETN.csv MAG.csv NNBC.csv Place-address.csv VCP.csv XSV.csv configure missing unk.def
COPYING Foreign.csv MAJ.csv NNG.csv Place-station.csv VV.csv aclocal.m4 configure.ac model.bin unk.dic
ChangeLog Group.csv MM.csv NNP.csv Place.csv VX.csv autogen.sh dicrc model.def user-custom-words.csv
CoinedWord.csv Hanja.csv Makefile NP.csv Preanalysis.csv Wikipedia.csv char.bin feature.def pos-id.def user-dic
EC.csv IC.csv Makefile.am NR.csv README XPN.csv char.def install-sh rewrite.def
EF.csv INSTALL Makefile.in NorthKorea.csv Symbol.csv XR.csv clean left-id.def right-id.def
EP.csv Inflect.csv NEWS Person-actor.csv VA.csv XSA.csv config.log matrix.bin sys.dic
ETM.csv J.csv NNB.csv Person.csv VCN.csv XSN.csv config.status matrix.def tools
user-custom-words.csv 파일이 존재하고 사용자 정의 엔트리의 확장 버전이 포함되어 있는지 확인하십시오:
$ ls user-custom-words.csv
$ cat user-custom-words.csv
대우,1786,3545,3821,NNP,*,F,대우,*,*,*,*
구글,1786,3546,2953,NNP,*,T,구글,*,*,*,*
까비,1788,3549,5472,NNP,인명,F,까비,*,*,*,*
세종,1789,3553,5515,NNP,지명,T,세종,*,*,*,*
세종시,1789,3552,5497,NNP,지명,F,세종시,Compound,*,*,세종/NNP/지명+시/NNG/*
다음 명령을 사용하여 수정된 사전의 압축 파일을 만듭니다:
$ tar cvzf custom-mecab-ko-dic.tar.gz mecab-ko-dic-2.1.1-20180720
다음 섹션에서 이 압축파일을 사용하여 Lucene 모듈을 빌드 합니다.
Nori 모듈은 mecab-ko-dic 배포판에서 만든 바이너리 사전을 사용합니다.
이 섹션에서는 수정된 배포판을 사용하여 Lucene 한국어 모듈의 바이너리 사전을 작성합니다.
사전은 소스에서 빌드되고 jar 내부에 패키징되므로 Lucene을 체크아웃 해야 합니다. Lucene 9.0.0 사용자 정의 jar를 생성 합니다:
$ git clone -b 'releases/lucene/9.0.0' https://github.com/apache/lucene.git
$ cd lucene
텍스트 편집기 (예: vim)로 gradle/generation/nori.gradle 파일을 열고 이전 섹션에서 압축한 파일의 이름과 주소로 수정 합니다:
$ vim lucene/gradle/generation/nori.gradle
//def dictionaryName = "mecab-ko-dic-2.1.1-20180720"
# 파일명은 아래와 같이 수정합니다:
def dictionaryName = "custom-mecab-ko-dic"
# URL의 주소 행은 아래와 같이 수정합니다:
//def dictionarySource = "https://bitbucket.org/eunjeon/mecab-ko-dic/downloads/${dictionaryName}.tar.gz"
def dictionarySource = "[file://`/change/me`/${dictionaryName}.tar.gz](file:///Users/cjenm/Projects/mecab/${dictionaryName}.tar.gz)"
이렇게 하면 원본 사전이 이전 단계에서 수정한 사전으로 바뀝니다.
이 문서에서는 원문과 다르게 mecab-ko-dic 2.1.1 버전을 사용하므로 변경전 사전에는 다른 POS 태그 목록이 있고 일부 POS 태그에는 다른 ID가 있으므로 에러 없이 새로운 사전을 빌드하려면 JAVA 소스코드를 수정해야 합니다.
POS 태그 목록에 UNIT을 추가하고 UnknownDictionaryBuilder의 32행을 다음과 같이 변경하는 경우:
$ vim lucene/gradle/generation/nori.gradle
private static final String NGRAM_DICTIONARY_ENTRY = "NGRAM,1798,3559,3677,SY,*,*,*,*,*,*,*";
# 아래와 같이 수정합니다:
private static final String NGRAM_DICTIONARY_ENTRY = "NGRAM,1801,3566,3640,SY,*,*,*,*,*,*,*";
lucene/analysis/nori
로 이동하여 아래의 명령을 실행 합니다:
$ cd lucene/analysis/nori
$ gradle compileMecabKo
.....
> Task :lucene:analysis:nori:compileMecabKo
Download file:/mecab/custom-mecab-ko-dic.tar.gz
Automaton regenerated from dictionary: custom-mecab-ko-dic
BUILD SUCCESSFUL in 19s
14 actionable tasks: 8 executed, 6 up-to-date
$ gradle assemble
.....
BUILD SUCCESSFUL in 2s
9 actionable tasks: 4 executed, 5 up-to-date
gradle 빌드 중에 check-environment.gradle에서 정의된 gradle 버전이 틀려서 FAILURE: Build failed with an exception. 이 발생하면
FAILURE: Build failed with an exception.
* Where:
Script '/lucene/gradle/validation/check-environment.gradle' line: 43
* What went wrong:
A problem occurred evaluating script.
> Gradle 7.2 is required (hint: use the gradlew script): this gradle is Gradle 7.4
다음과 같이 expectedGradleVersion을 Local 환경에 설치된 gradle version으로 수정합니다.
$ gradle -v
------------------------------------------------------------
Gradle 7.4
------------------------------------------------------------
$ vim gradle/validation/check-environment.gradle
configure(rootProject) {
ext {
expectedGradleVersion = '7.4'
}
gradle 빌드가 완료된 이후 사용자 정의 모듈의 jar는 lucene 체크아웃한 루트 경로의 lucene/analysis/nori/build/libs/lucene-analyzers-nori-9.0.0-SNAPSHOT.jar 에서 찾을 수 있습니다.
이 파일을 복사하세요. 다음 단계에서 필요합니다.
그리고 src/resources/org/apache/lucene/analysis/ko/dict/ 의 새로운 사전에서 새 바이너리 사전을 생성합니다.
바이너리 사전이 존재하고 원본 사전과 다른지 확인하십시오:
$ git status
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: gradle/generation/nori.gradle
modified: gradle/validation/check-environment.gradle
modified: lucene/analysis/nori/src/java/org/apache/lucene/analysis/ko/util/UnknownDictionaryBuilder.java
modified: lucene/analysis/nori/src/resources/org/apache/lucene/analysis/ko/dict/ConnectionCosts.dat
modified: lucene/analysis/nori/src/resources/org/apache/lucene/analysis/ko/dict/TokenInfoDictionary$buffer.dat
modified: lucene/analysis/nori/src/resources/org/apache/lucene/analysis/ko/dict/TokenInfoDictionary$fst.dat
modified: lucene/analysis/nori/src/resources/org/apache/lucene/analysis/ko/dict/TokenInfoDictionary$posDict.dat
modified: lucene/analysis/nori/src/resources/org/apache/lucene/analysis/ko/dict/TokenInfoDictionary$targetMap.dat
modified: lucene/analysis/nori/src/resources/org/apache/lucene/analysis/ko/dict/UnknownDictionary$buffer.dat
modified: lucene/analysis/nori/src/resources/org/apache/lucene/analysis/ko/dict/UnknownDictionary$posDict.dat
이 섹션에서는 이전 단계에서 생성된 Lucene 모듈 jar를 사용하여 Nori용 Elasticsearch 플러그인의 사용자 정의 버전을 빌드할 것입니다.
첫 번째 작업은 Elasticsearch 8.0.1 코드를 체크아웃 하는 것입니다.
$ git clone -b v8.0.1 https://github.com/elastic/elasticsearch
텍스트 편집기 (예: vim)로 elasticsearch/plugins/analysis-nori
로 이동하여 build.gradle
파일을 열고 다음의 행을 변경하십시오:
api "org.apache.lucene:lucene-analyzers-nori:${versions.lucene}"
아래와 같이 수정합니다:
api files('/change/me/lucene-analyzers-nori-9.0.0-SNAPSHOT.jar')
이렇게 하면 이전 단계에서 빌드된 jar를 사용하여 플러그인을 빌드하도록 gradle에 설정합니다.
analysis-nori
디렉토리에서 다음의 명령을 실행하여 플러그인에 사용자 지정 배포를 생성합니다:
$ gradle assemble
...
BUILD SUCCESSFUL in 2m 11s
36 actionable tasks: 28 executed, 8 up-to-date
$ ls build/distributions
analysis-nori-8.0.1-SNAPSHOT-javadoc.jar analysis-nori-8.0.1-SNAPSHOT-sources.jar analysis-nori-8.0.1-SNAPSHOT.jar analysis-nori-8.0.1-SNAPSHOT.pom analysis-nori-8.0.1-SNAPSHOT.zip
명령이 성공하면 Elasticsearch 내에서 사용할 수 있는 zip 배포판이 build/distributions
에 있습니다. 이 파일을 복사합니다. 다음 단계에서 필요합니다.
Elasticsearch 에서 8.0.1 버전을 다운로드 받습니다.
압축파일을 해제한 후 Elasticsearch 디렉토리에서 다음 명령을 실행합니다:
$ ./bin/elasticsearch-plugin install file:///change/me/analysis-nori-8.0.1-SNAPSHOT.zip
이제 완료 되었습니다, Elasticsearch를 시작하고 사용자 정의 단어가 인식되는지 확인할 수 있습니다:
$ ./bin/elasticsearch
다음과 같이 Nori 분석기를 사용해 보십시오:
POST _analyze
{
"text": "대우그룹",
"analyzer": "nori",
"explain": true
}
결과는 다음과 같아야 합니다:
{
"detail": {
"custom_analyzer": false,
"analyzer": {
"name": "org.apache.lucene.analysis.ko.KoreanAnalyzer",
"tokens": [
{
"token": "대우",
"start_offset": 0,
"end_offset": 2,
"type": "word",
"position": 0,
"bytes": "[eb 8c 80 ec 9a b0]",
"leftPOS": "NNP(Proper Noun))",
"morphemes": null,
"posType": "MORPHEME",
"positionLength": 1,
"reading": null,
"rightPOS": "NNP(Proper Noun)",
"termFrequency": 1
},
{
"token": "그룹",
"start_offset": 2,
"end_offset": 4,
"type": "word",
"position": 1,
"bytes": "[ea b7 b8 eb a3 b9]",
"leftPOS": "NNG(General Noun)",
"morphemes": null,
"posType": "MORPHEME",
"positionLength": 1,
"reading": null,
"rightPOS": "NNG(General Noun)",
"termFrequency": 1
}
]
}
}
}
지금부터는 mecab-ko-dic 버전을 최신 버전으로 빌드해야 하는 이유를 확인하기 위해서 Elasticsearch plugin으로 배포되는 Nori plugin과 사용자 정의로 빌드한 Nori plugin의 분석 결과를 비교해 보겠습니다.
이전 단계에서 설치한 Nori plugin을 삭제한 후 배포판 Nori plugin을 설치 합니다. Elasticsearch 디렉토리에서 다음 명령을 실행합니다:
$ ./bin/elasticsearch-plugin remove analysis-nori
$ ./bin/elasticsearch-plugin install analysis-nori
Elasticsearch를 시작하고 배포판의 분석결과를 확인합니다:
./bin/elasticsearch
다음과 같이 Nori의 분석결과를 요청 합니다:
POST _analyze
{
"text": "아버지가방에들어가신다.",
"analyzer": "nori",
"explain": true
}
결과는 다음과 같이 '가방'이 명사로 분석되는 것을 확인하실 수 있습니다:
{
"detail" : {
"custom_analyzer" : false,
"analyzer" : {
"name" : null,
"tokens" : [
{
"token" : "아버지",
"start_offset" : 0,
"end_offset" : 3,
"type" : "word",
"position" : 0,
"bytes" : "[ec 95 84 eb b2 84 ec a7 80]",
"leftPOS" : "NNG(General Noun)",
"morphemes" : null,
"posType" : "MORPHEME",
"positionLength" : 1,
"reading" : null,
"rightPOS" : "NNG(General Noun)",
"termFrequency" : 1
},
{
"token" : "가방",
"start_offset" : 3,
"end_offset" : 5,
"type" : "word",
"position" : 1,
"bytes" : "[ea b0 80 eb b0 a9]",
"leftPOS" : "NNG(General Noun)",
"morphemes" : null,
"posType" : "MORPHEME",
"positionLength" : 1,
"reading" : null,
"rightPOS" : "NNG(General Noun)",
"termFrequency" : 1
},
{
"token" : "들어가",
"start_offset" : 6,
"end_offset" : 9,
"type" : "word",
"position" : 3,
"bytes" : "[eb 93 a4 ec 96 b4 ea b0 80]",
"leftPOS" : "VV(Verb)",
"morphemes" : null,
"posType" : "MORPHEME",
"positionLength" : 1,
"reading" : null,
"rightPOS" : "VV(Verb)",
"termFrequency" : 1
}
]
}
}
}
다음으로 이전 단계와 같이 사용자 정의 Nori plugin을 설치 합니다. Elasticsearch 디렉토리에서 다음 명령을 실행합니다:
$ ./bin/elasticsearch-plugin remove analysis-nori
$ ./bin/elasticsearch-plugin install file:///change/me/analysis-nori-8.0.1-SNAPSHOT.zip
Elasticsearch를 시작합니다:
$ ./bin/elasticsearch
같은 문장으로 Nori에 분석요청을 합니다:
POST _analyze
{
"text": "아버지가방에들어가신다.",
"analyzer": "nori",
"explain": true
}
결과는 다음과 같이 '방'으로 분석 됩니다:
{
"detail" : {
"custom_analyzer" : false,
"analyzer" : {
"name" : null,
"tokens" : [
{
"token" : "아버지",
"start_offset" : 0,
"end_offset" : 3,
"type" : "word",
"position" : 0,
"bytes" : "[ec 95 84 eb b2 84 ec a7 80]",
"leftPOS" : "NNG(General Noun)",
"morphemes" : null,
"posType" : "MORPHEME",
"positionLength" : 1,
"reading" : null,
"rightPOS" : "NNG(General Noun)",
"termFrequency" : 1
},
{
"token" : "방",
"start_offset" : 4,
"end_offset" : 5,
"type" : "word",
"position" : 2,
"bytes" : "[eb b0 a9]",
"leftPOS" : "NNG(General Noun)",
"morphemes" : null,
"posType" : "MORPHEME",
"positionLength" : 1,
"reading" : null,
"rightPOS" : "NNG(General Noun)",
"termFrequency" : 1
},
{
"token" : "들어가",
"start_offset" : 6,
"end_offset" : 9,
"type" : "word",
"position" : 4,
"bytes" : "[eb 93 a4 ec 96 b4 ea b0 80]",
"leftPOS" : "VV(Verb)",
"morphemes" : null,
"posType" : "MORPHEME",
"positionLength" : 1,
"reading" : null,
"rightPOS" : "VV(Verb)",
"termFrequency" : 1
}
]
}
}
}