이번에는 지난 글에 이어서 Elasticsearch에서 공식적으로 지원하는 한글 형태소 분석기인 Nori를 설치하고 사용해 보자. 기본 Analyzer 사용 시와 Nori 형태소 분석기를 사용했을 때의 차이점에 대해 비교해 보자.
이번에 사용할 데이터셋은 wikimedia kowiki로 wikimedia에서 제공하는 한국어 데이터셋을 사용할 거다.
다음 명령어는 지난번에 다뤘기 때문에 빠르게 넘어간다.
!pip install elasticsearch==8.8.0
!wget -q https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.8.0-linux-x86_64.tar.gz
!tar -xzf elasticsearch-8.8.0-linux-x86_64.tar.gz
!sudo chown -R daemon:daemon elasticsearch-8.8.0/
!umount /sys/fs/cgroup
!apt install cgroup-tools
1. 한글 형태소 분석기 Nori 설치
한글 형태소 분석기 Nori 설치해 준다.
! /content/elasticsearch-8.8.0/bin/elasticsearch-plugin install analysis-nori
그리고 설치 확인을 해보자. elasticsearch 플러그인에 어떤 게 있는지 목록을 보여주는 명령어인데 이 결과로 analysis-nori가 보여야 한다.
! /content/elasticsearch-8.8.0/bin/elasticsearch-plugin list
이제 엘라스틱서치의 데몬 인스턴스를 만들어주자.
만약에 Nori 설치 이전에 데몬을 생성하면 Nori를 바로 사용할 수 없다. 이때는 데몬 재실행이 필요하다. (지금은 Nori를 먼저 설치했기 때문에 바로 실행해도 된다.)
import os
from elasticsearch import Elasticsearch, helpers
import numpy as np
import pandas as pd
import json
from subprocess import Popen, PIPE, STDOUT
es_server = Popen(['elasticsearch-8.8.0/bin/elasticsearch'],
stdout=PIPE, stderr=STDOUT,
preexec_fn=lambda: os.setuid(1) # as daemon
)
# 인스턴스를 로드하는 데 약간의 시간이 걸림
import time
time.sleep(30)
데몬이 실행되었는지 확인해 보자. 세 개의 daemon process가 있어야 한다.
!ps -ef | grep elasticsearch
데몬을 실행하고 비밀번호 설정까지는 지난번과 동일해서 빠르게 넘어가자.
!/content/elasticsearch-8.8.0/bin/elasticsearch-setup-passwords auto -url "https://localhost:9200"
username = 'elastic'
# 위 명령 실행 결과의 마지막 부분인 PASSWORD elastic 값으로 교체 필요
password = 'awcBjNqVcIBiBUCdT93s'
es = Elasticsearch(['https://localhost:9200'], basic_auth=(username, password), ca_certs="/content/elasticsearch-8.8.0/config/certs/http_ca.crt")
resp = dict(es.info())
resp
2. Nori analyzer 활용
우선 Nori를 사용하지 않을 경우 형태소 분석 결과를 확인해보자. 기본적으로는 default analyzer인 "Standard analyzer"를 사용하게 된다. (공백으로 단어 분리, 소문자로 변환, 불용어 제거, 문장부호 기호 등 제거)
import pprint
result = es.indices.analyze(text ='모든 권력은 국민으로부터 나온다.')
pp = pprint.PrettyPrinter(indent=4, width=20)
pp.pprint(result)
result = es.indices.analyze(text ='홍대입구역너무복잡해')
pp = pprint.PrettyPrinter(indent=4, width=20)
pp.pprint(result)
일부러 띄어쓰기를 하지 않은 건 전부 한 단어로 나오는 걸 볼 수 있다. 왜냐하면 대체로 공백 단위로 단어를 분리하기 때문이다. 추가적으로 품사, 조사 이런 건 구별하지 않는다.
이제 Nori 사용할 경우 형태소 분석 결과를 확인해 보자. 확인하는 방법은 analyzer 인자에 nori를 추가해 주면 된다.
result = es.indices.analyze(analyzer="nori", text ='모든 권력은 국민으로부터 나온다.')
pp = pprint.PrettyPrinter(indent=4, width=20)
pp.pprint(result)
result = es.indices.analyze(analyzer="nori", text ='홍대입구역너무복잡해')
pp = pprint.PrettyPrinter(indent=4, width=20)
pp.pprint(result)
조금 더 상세히 보자면 '권력', '국민', '나오'까지 분리가 된다. 희한하게 '모든'은 빠졌다...
그리고 '홍대', '입구', '역', '복집' 이렇게 나온 걸 볼 수 있다. 이렇게 보면 nori는 품사가 고려된 채 분석된 걸 볼 수 있다.
ObjectApiResponse({'tokens': [{'token': '권력', 'start_offset': 3, 'end_offset': 5, 'type': 'word', 'position': 1}, {'token': '국민', 'start_offset': 7, 'end_offset': 9, 'type': 'word', 'position': 3}, {'token': '나오', 'start_offset': 14, 'end_offset': 17, 'type': 'word', 'position': 5}]}) ObjectApiResponse({'tokens': [{'token': '홍대', 'start_offset': 0, 'end_offset': 2, 'type': 'word', 'position': 0}, {'token': '입구', 'start_offset': 2, 'end_offset': 4, 'type': 'word', 'position': 1}, {'token': '역', 'start_offset': 4, 'end_offset': 5, 'type': 'word', 'position': 2}, {'token': '복잡', 'start_offset': 7, 'end_offset': 9, 'type': 'word', 'position': 4}]})
3. 위키데이터를 활용한 색인/검색 예시
이제 위키데이터를 확용해서 색인/검색을 살펴보자.
그러기 위해서 위키미디어로부터 kowiki 데이터를 다운로드하자.
!wget https://dumps.wikimedia.org/kowiki/latest/kowiki-latest-pages-articles1.xml-p1p82407.bz2
# 위키데이터의 노이즈를 제거하고 json 형태로 반환하는 코드를 참조
!git clone https://github.com/attardi/wikiextractor.git
# 다운로드 받은 샘플 위키 데이터를 전처리하여 검색의 입력으로 사용
# 결과는 elastic 폴더에 'extract_result/AA,AB,AC.../wiki_00..99'라는 새로운 폴더에 저장된다.(용량이 비슷하게 나눠서 저장됨)
# 변환결과 wiki_00 파일의 내용 샘플 {"id": "5", "revid": "641228", "url": "https://ko.wikipedia.org/wiki?curid=5", "title": "\uc9c0\...\ud130", "text": "\uc81c\...\ub2e4."}
!python -m wikiextractor.wikiextractor.WikiExtractor kowiki-latest-pages-articles1.xml-p1p82407.bz2 --json -o extract_result
다운로드한 json 파일을 읽어 들여 색인할 수 있는 index_docs에 저장해 보자.
# 'wiki_dump_json_file'에 있는 JSON 파일 읽어들여 index_docs에 저장
index_docs = []
wiki_dump_json_file = '/content/extract_result/AA/wiki_00'
for line in open(wiki_dump_json_file, encoding="utf-8"):
# JSON 데이터를 읽어들여 파이썬 딕셔너리로 변환
json_data = json.loads(line)
# 색인할 문서 목록에 추가
index_docs.append(json_data)
index_docs[0]
id, revid, url, titlen, text로 이루어진 걸 확인할 수 있다.
그리고 색인을 할 때 필요한 mapping을 생성해 주자.
title에 대해서는 type을 text로 주고 analyzer로는 nori로 설정했다. 그리고 nori를 선언하면서도 "decompound_mode": "mixed"로 줘서 복합 명사일 경우 각각의 단어를 따로 색인도 하고 복합 명사 자체를 색인할 수 있게 해 줬다. 추가로 filter도 설정해서 색인할 필요가 없는 부분은 하지 않도록 설정해 줬다. 예를 들면, 어미, 조사, 구분자, 줄임표, 지정사, 보조 용언 등이 있다.
# 색인을 위한 mapping 설정
setting = {
"settings": {
"analysis": {
"analyzer": {
"nori": {
"type": "custom",
"tokenizer": "nori_tokenizer",
"decompound_mode": "mixed",
"filter": ["nori_posfilter"]
}
},
"filter": {
"nori_posfilter": {
"type": "nori_part_of_speech",
# 어미, 조사, 구분자, 줄임표, 지정사, 보조 용언 등
"stoptags": ["E", "J", "SC", "SE", "SF", "VCN", "VCP", "VX"]
}
}
}
},
"mappings": {
"properties": {
"title": {"type": "text", "analyzer": "nori"},
"text": {"type": "text", "analyzer": "nori"}
}
}
}
이후 ES 색인이나 검색을 할 때 사용할 공통 함수를 정의해 주자.
# Elasticsearch 색인/검색을 위한 공통 함수 정의
from elasticsearch import Elasticsearch, helpers
import json
import pprint as pp
def create_es_index(index, body):
# 인덱스가 이미 존재하는지 확인
if es.indices.exists(index=index):
# 인덱스가 이미 존재하면 설정을 새로운 것으로 갱신하기 위해 삭제
es.indices.delete(index=index)
# 지정된 설정으로 새로운 인덱스 생성
es.indices.create(index=index, body=body)
def delete_es_index(index):
# 지정된 인덱스 삭제
es.indices.delete(index=index)
def bulk_add(index, docs):
# 대량 인덱싱 작업을 준비
actions = [
{
'_index': index,
'_source': doc
}
for doc in docs
]
# Elasticsearch 헬퍼 함수를 사용하여 대량 인덱싱 수행
return helpers.bulk(es, actions)
- create_es_index: 실제 인덱스 명을 받고 색인 설정을 받아서 실제 색인을 생성하는 코드다. 만약에 존재하는 인덱스 명이 있으면 삭제하고 추가해 주는 코드로 작성했다.
- delete_es_index: 인덱스를 삭제해 준다.
- bulk_add: 대량 작업을 위해 작성했다. actions라는 함수에 인덱스와 데이터를 준비해 딕셔너리 형태로 만들면 한꺼번에 색인을 추가해 줄 수 있다.
위에 선언한 함수들을 사용해 보자.
# setting으로 설정된 내용으로 'test' 인덱스 생성
create_es_index("test", setting)
# 'test' 인덱스에 대량 색인화 수행
ret = bulk_add("test", index_docs)
# 결과 출력
print(ret)
여기 쓰여있는 61은 61개의 데이터가 성공적으로 색인되었다는 뜻이다.
이제 title과 text 두 필드에서 검색을 해보자.
이때는 multi_match 쿼리를 사용하면 되고 실제 질의는 query에 넣어준다. multi_match에서는 fields 항목을 통해 여러 필드에서 한꺼번에 검색할 수 있다.
size는 최종 몇 개를 출력할지 결정한다.
body = {
"query": {
"multi_match": {
"query": "대한민국 대통령",
"fields": ['title', 'text'] # wiki에서 json만들어질때 컬럼명이 제목은 title , 내용은 text
}
},
"size": 10
}
res = es.search(index="test", body=body)
결과를 출력해 보자.
# 결과 출력
for rst in res['hits']['hits']:
print('score:', rst['_score'], 'source::', rst['_source'])
이렇게 Nori에 대해서 간략하게 살펴봤다. 이제 다음 글에서 Nori를 사용해서 실제 검색하는 QueryDSL도 작성해 보자!
'Tools' 카테고리의 다른 글
Vector 유사도 검색 - Faiss (0) | 2024.12.05 |
---|---|
Vector 유사도 검색 - Elasticsearch (0) | 2024.12.05 |
구글 코랩에서 Elasticsearch Nori Query DSL 사용해서 검색하기 (1) | 2024.12.04 |
구글 코랩에서 Elasticsearch 설치 및 실습 (1) | 2024.12.04 |
Slack과 GitHub 연동 (2) | 2023.10.25 |