장난감 연구소

[Elasticsearch] 최대한 빠르게 한글 검색하기 본문

개발/Elasticsearch

[Elasticsearch] 최대한 빠르게 한글 검색하기

changi1122 2022. 1. 12. 15:19

    익힌 내용을 나중에 사용하기 위해 정리한 것으로 부족한 부분이 많아 계속 수정해나가고 있습니다. 처음 Elasticsearch 사용하는 분 아니면 대부분 알고 계시는 내용일 겁니다.

    학교 과제나 작은 프로젝트를 진행하면서 검색 기능이 필요한 경우가 있다. 다행히도 Elasticsearch(엘라스틱 서치)라는 훌륭한 오픈소스 검색 엔진을 사용하면 검색 기능을 쉽게 만들 수 있다. 그런데 하나씩 꼼꼼히 익혀가면서 만들기에는 홈페이지의 가이드 내용이 너무 방대하다. 그런 상황에 쓸 수 있게, 한글 검색이 최소한으로 작동만 하게 만들어 보는 가이드 글이다.

    오라클 클라우드 프리티어에서 제공하는 무료 Compute 인스턴스의 메모리 용량은 1GB이다. 이 글의 목적에서 알 수 있듯이 무료 Compute 인스턴스에서 작동해줬으면 했기에, 1GB 메모리 우분투 서버 가상머신에서 테스트되었다.

    Elasticsearch 설치

    Getting started with the Elastic Stack | Getting Started [7.16] | Elastic

    먼저 Elasticsearch부터 설치한다. 링크된 웹 페이지에서 deb 패키지 란의 스크립트를 입력하여 Elasticsearch를 설치한다.

    $ curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-<최신 버전>-amd64.deb
    $ sudo dpkg -i elasticsearch-<최신 버전>-amd64.deb
    # <최신 버전>은 링크된 문서에서 확인

    * Kibana도 설치한다면 DevTool 페이지에서 curl 대신 HTTP 요청을 보낼 수 있어 편리하다. 서버 운영체제가 아니라 PC에 설치하거나, 5601번 포트를 열고 외부에서 접근할 수 있도록 하는 수고를 감수할 수 있다면 Kibana도 설치하여 사용하기 바란다.

    환경 설정

    deb 패키지로 Elasticsearch를 설치한 경우 환경 설정을 위한 파일들이 /etc/elasticsearch 디렉토리에 모여 있다.

    /etc/elasticsearch 디렉토리 내부 파일들
    /etc/elasticsearch 디렉토리 내부

    이 디렉토리의 소유자는 'root'이며 접근 권한이 'drwxr-s---'이기에 읽기·쓰기 권한을 root 계정만 가지고 있다. 그래서 이 디렉토리에 접근하려면, "$ su" 명령을 통해서 root 계정에 접속한 후 파일들을 수정한다.

    # root 계정의 비밀번호 입력 필요. 설정하지 않은 경우 설정하기
    $ su
    $ cd /etc/elasticsearch
    $ ll

    JVM 설정 (jvm.options)

    먼저 자바 가상머신(JVM)의 힙 크기를 설정해준다. 힙 크기는 jvm.options 파일을 수정하거나, jvm.options.d 디렉토리에 .options 확장자의 파일을 생성하여 설정해준다. 공식 문서는 후자를 권장한다.

    '-Xms<숫자>g'로 최소 힙 크기를, '-Xmx<숫자>g'로 최대 힙 크기를 설정해준다. 예를 들어 둘 다 1GB로 설정하려면 파일 내용을 '-Xms1g', '-Xmx1g'로 설정하면 된다. Xms와 Xmx는 전체 메모리 용량의 50% 이하로 설정하는 걸 권장한다고 한다.

    1GB 메모리 가상머신에서 Elasticsearch를 정상적으로 실행하려면 힙 크기를 128MB로 설정해야 한다. 먼저 root 계정에 접속한 후 작업 디렉토리를 /etc/elasticsearch로 이동한다. 그리고 jvm.options.d 디렉토리에 jvm.options 파일을 작성하고 저장한다.

    $ su
    $ cd /etc/elasticsearch
    $ vi jvm.options.d/jvm.options
    # /etc/elasticsearch/jvm.options.d/jvm.options : 파일 작성 후 저장
    -Xms128m
    -Xmx128m

    관련 공식 문서

    Elasticsearch 설정

    Elasticsearch에 관한 설정은 elasticsearch.yml에 모여 있다. elasticsearch.yml을 수정하는데 이 또한 root 계정으로 수정이 필요하다.

    관련 공식 문서

    경로

    먼저 Elasticsearch의 데이터와 로그 파일의 경로를 설정한다. 중간쯤 path.data와 path.logs를 찾을 수 있는데 이 값을 원하는 경로로 설정한다. deb 패키지로 설치했을 때 기본 경로는 /var/lib/elasticsearch이다.

    # /etc/elasticsearch/elasticsearch.yml
    
    # ----- Paths -----
    path.data: /var/lib/elasticsearch
    path.logs: /var/lib/elasticsearch

    클러스터명

    Elasticsearch는 여러 개의 노드를 모아 클러스터를 구성함으로써 한 노드가 작동하지 않더라도 데이터의 가용성과 무결성을 보장한다고 한다. 이때 각 노드는 클러스터명 구분을 통해서 결합한다. 이를 위해 적당한 이름을 정해준다.

    1GB 메모리에서는 한 개의 노드 밖에 실행할 수 없기에 이 글에선 1개 노드만 사용한다.

    # ----- Cluster -----
    cluster.name: my-application

    노드명

    노드의 이름도 구분할 수 있게 정해준다.

    # ----- Node -----
    node.name: node-1

    네트워크 호스트

    network.host 값을 설정하여 내부나 외부에서 접근할 수 있도록 해준다. _local_로 설정하면 내부에서만 접근할 수 있고, 로컬 네트워크(오라클 클라우드 인스턴스끼리 등)간에는 _site_, 외부 네트워크로 공개하려면 _global_로 설정한다.

    오라클 클라우드 프리티어에서 사용할 때는 _site_로 설정하고, 다른 Compute 인스턴스에서 Private IP로 접근할 수 있다.

    http.port 값을 통해 HTTP 요청을 받을 포트를 설정해준다. 9200번에서 9300번 사이를 기본 값으로 사용한다.

    * 외부 네트워크에서 바로 HTTP 요청을 할 수 있게 하기보다 다른 서버 프로그램을 통해서 접근하는 걸 권장한다고 한다.

    관련 공식 문서

    # ----- Network -----
    network.host: ["_local_", "_site_"]
    http.port: 9200

    discovery.type

    한 개의 노드만 사용할 때는 discovery.type을 single-node로 설정한다.

    # ----- Discovery -----
    discovery.type: single-node

    bootstrap.memory_lock

    bootstrap.memory_lock을 설정해 줌으로써 힙 메모리가 swap되는 걸 막는다. swap이 일어나면 Elasticsearch의 성능에 매우 안 좋다고 한다.

    관련 공식 문서

    # ----- Memory -----
    bootstrap.memory_lock: true

    그리고 제한 없이 memory lock이 가능하도록 시스템 설정이 필요하다. deb 패키지로 설치한 경우 systemd를 사용하기에 /usr/lib/systemd/system/elasticsearch.service 파일이 그와 관련된 기본 값을 가지고 있다.

    간단하게 LimitMEMLOCK을 설정하려면 "$ sudo systemctl edit elasticsearch" 명령어를 통해 /etc/systemd/system/elasticsearch.service.d/override.conf 파일에다가 내용을 작성하고 저장하면 된다.

    # root 계정이 아니어도 됨
    $ sudo systemctl edit elasticsearch
    # 수정 후 적용
    $ sudo systemctl daemon-reload
    # /etc/system/system/elasticsearch.service.d/override.conf
    [Service]
    LimitMEMLOCK=infinity

    관련 공식 문서

    Elasticsearch 서비스 실행, 종료

    이제 서비스 실행 후 정상적으로 작동하는지 테스트한다.

    관련 공식 문서

    # 서비스 실행
    $ sudo systemctl start elasticsearch.service
    # 종료
    $ sudo systemctl stop elasticsearch.service
    # 부팅시마다 자동 실행 설정
    $ sudo systemctl enable elasticsearch.service

    정상적으로 실행되었다면 curl을 사용하여 HTTP 요청을 보내고 응답이 정상적으로 오는지 확인한다. 아래 사진처럼 응답 받을 수 있다면 정상적으로 작동되고 있는 것임을 알 수 있다.

    $ curl -X GET "localhost:9200/?pretty"

    elasticsearch / GET 응답
    위 요청의 응답

    보안 설정

    정상적으로 작동되는 걸 확인했으면 최소한의 보안 설정을 시작한다. 먼저 Elasticsearch 서비스가 실행되어 있으면 종료한다.

    관련 공식 문서

    password를 통한 기본 인증

    root 계정에 접속한 뒤 다시 /etc/elasticsearch/elasticsearch.yml 파일을 수정한다. xpack.security.enabled를 새로 입력하고 값을 true로 저장한다.

    # /etc/elasticsearch/elasticsearch.yml
    
    # ----- Security -----
    xpack.security.enabled: true

    이제 다시 curl로 GET 요청을 보내면 missing authentication credentials라면서 401을 리턴하고 정상적으로 응답하지 않는다. 이제 요청을 보내려면 username과 password를 설정하고, 각 요청 때마다 같이 보내야 한다.

    먼저 비밀번호를 설정한다. root 계정에서 /usr/share/elasticsearch/bin/elasticsearch-setup-passwords를 실행한다. 비밀번호를 한번 설정한 후 다시 설정하려면 모든 데이터를 지워야 하니 조심한다. 6개 유저의 비밀번호를 모두 설정해야 한다.

    $ /usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive

    이제 다시 curlhost 앞에 “elastic:<비밀번호>@”을 붙인 후 GET 요청을 보낸다. 비밀번호를 함께 보내면 정상적으로 응답하는 것을 확인할 수 있다.

    $ curl -X GET "elastic:<password>@localhost:9200/?pretty"

    인덱스 생성 및 문서 색인

    이제 환경 설정을 마쳤으며 데이터를 색인하고 검색해본다.

    인덱스는 데이터베이스로 치면 테이블로, 인덱스에 데이터를 입력하거나, 수정·삭제하는 등의 작업을 할 수 있다. 인덱스의 mappings는 입력되는 데이터의 형태이고, settings는 인덱스 단위의 설정들을 담고 있다.

    인덱스 test를 생성한다.

    curl -X PUT "elastic:<password>@localhost:9200/test?pretty" -H 'Content-Type: application/json' -d'
    {
      "mappings": {
        "properties": {
          "field1": { "type": "text" }
        }
      }
    }'

    만들어진 인덱스를 삭제하려면 PUT 말고 DELETE 요청을 보내면 된다. 이외 인덱스 관련 API는 아래 공식 문서와 이외의 참고 자료에서 확인할 수 있다.

    Index APIs | Elasticsearch Guide [7.16] | Elastic

    이제 test 인덱스에 document를 색인한다.

    curl -X POST "elastic:<password>@localhost:9200/test/_doc?pretty" -H 'Content-Type: application/json' -d'
    {
      "field1": “hello test document”
    }'

    document 색인 요청 및 응답
    document를 색인(index) 요청 및 응답

    URL_doc 뒤에 “/<문서 id>”를 붙이고 GET 요청을 보내면 조회, DELETE 요청을 보내면 삭제되는 등 Document 관련 API도 직관적이다. Document 관련 API도 아래 공식 문서와 이외의 참고 자료에서 확인할 수 있다.

    Document APIs | Elasticsearch Guide [7.16] | Elastic

    검색

    더미 document를 몇 개 만든 후 인덱스에서 검색을 해본다. curl로 "/<인덱스>/_search"에 그냥 GET 요청을 보내면 모든 document를 확인할 수 있다.

    curl -X GET "elastic:<password>@localhost:9200/test/_search?pretty"

    field1에 "hello"라는 단어가 있는 document를 검색해본다.

    curl -X GET "elastic:<password>@localhost:9200/test/_search?pretty" -H 'Content-Type: application/json' -d'
    {
      "query": {
        "term": {
          "field1": "hello"
        }
      }
    }'

    "hello"를 찾은 검색 결과

    위 사진과 같이 두 개의 document가 매치되었다.

    Search 관련 API도 아래 공식 문서와 이외의 참고 자료에서 확인할 수 있다.

    Search APIs | Elasticsearch Guide [7.16] | Elastic

    Elasticsearch Clients

    웹 서버에서 데이터를 색인하고 검색할 때 직접 HTTP 요청을 보낼 필요 없이 프로그래밍 언어별로 클라이언트 라이브러리가 만들어져 있다. 아래 공식 문서에서 확인할 수 있다.

    Elasticsearch Clients | Elastic

    한글 검색

    기본적으로 Elasticsearch는 띄어쓰기로 단어를 구분하여 색인하기 때문에 뿌리가 깊은 나무는뿌리가”, “깊은”, “나무는으로 검색했을 때 매치된다. nori_tokenizer는 이를 형태소로 분석하여 색인한다. 예를 들어 뿌리가 깊은 나무는뿌리////나무/으로 나눠진다.

    root 계정에 접속한 후 아래 명령어로 nori_tokenizer를 설치한다.

    $ su
    $ /usr/share/elasticsearch/bin/elasticsearch-plugin install analysis-nori

    nori_tokenizer를 사용하는 새로운 인덱스 nori_sample을 생성한다.

    curl -X PUT "elastic:<password>@localhost:9200/nori_sample?pretty" -H 'Content-Type: application/json' -d'
    {
      "settings": {
        "index": {
          "analysis": {
            "tokenizer": {
              "nori_tokenizer_set": {
                "type": "nori_tokenizer",
                "decompound_mode": "mixed",
                "discard_punctuation": "false"
              }
            },
            "analyzer": {
              "my_analyzer": {
                "type": "custom",
                "tokenizer": "nori_tokenizer_set"
              }
            }
          }
        }
      },
      "mappings" : {
        "properties" : {
          "title" : {
            "type" : "text",
            "analyzer": "my_analyzer"
          },
          "body": {
            "type" : "text",
            "analyzer": "my_analyzer"
          }
        }
      }
    }'

    nori_sample 인덱스에 데이터를 색인한다.

    curl -X POST "elastic:<password>@localhost:9200/nori_sample/_doc" -H 'Content-Type: application/json' -d'
    {
      "title": "용비어천가",
      "body": "뿌리 깊은 나무는"
    }'

    body 필드가 나무document를 검색하면, "나무는"으로 검색하지 않아도 document가 검색되는 걸 확인할 수 있다.

    curl -X GET "elastic:<password>localhost:9200/nori_sample/_search" -H 'Content-Type: application/json' -d'
    {
      "query": {
        "term": {
          "body": "나무"
        }  
      }
    }'

    "나무"를 찾은 검색 결과

    이외 다양한 필터

    "analyzer": {
      "my_analyzer": {
        "type": "custom",
        "char_filter": ["html_strip"],
        "tokenizer": "nori_tokenizer_set",
        "filter": ["lowercase", "stop"]
      }
    ...

    위 는 analyzer 예시로 tokenizer 이외에도 html 태그를 제거해주는 html_strip 캐릭터 필터, 알파벳을 소문자로 통일해주는 lowercase 토큰 필터, 문장 부호를 제거해주는 stop 토큰 필터 등 다양한 텍스트 분석 기능이 있다. 아래 공식 문서와 이외의 참고 자료에서 확인할 수 있다.

    Text analysis | Elasticsearch Guide [7.16] | Elastic

     

    더 자세한 내용들은 공식 가이드 문서와 참고 자료들을 확인했으면 한다.

    참고자료

    Elasticsearch Guide [7.16] | Elastic : 공식 영문 가이드 문서

    처음부터 시작하는 Elastic - YouTube : Elasticsearch를 처음 시작하는 사람들을 위한 매우 좋은 동영상 강의이다. Elastic社 협력 대학교 학생들을 위해 제작된 것을 다른 사람들에게 도움을 주고자 공개했다고 한다.

    Elastic 가이드 북 - Elastic 가이드북 (kimjmin.net) : 위 동영상 강의를 만드신 분이 집필한 Elasticsearch 한글 가이드북이다.

     

    728x90
    개 댓글