Database/Opensearch

Opensearch Tokenizer, Analyzer와 Custom Analyzer 적용

daeunnniii 2024. 11. 20. 21:28
728x90
반응형

1. Opensearch Analyzer

  • Opensearch에 텍스트 입력 시 필드를 인덱싱하고 Documents화 할 때 Lucene 엔진에 의해 텍스트가 분석되어 입력됨.
  • 이때 텍스트를 분석하는 엔진을 Analyzer라고 함.
  • Lucene 에서 제공하는 Analyzer는 하나의 Tokenizer와 다수의 Filter로 구성

1.1 Filter의 종류

  • CharFilter와 TokenFilter 2가지가 존재
  • CharFilter는 입력된 문자열에서 불필요한 문자를 normalization하기 위해 사용
  • TokenFIilter는 tokenizer에 의해 분해된 token에 대한 Filter 처리를 함.

  • 기본적으로 CharFilter에 의해 공백 콤마 등의 문자를 삭제하며, 예시로 문서의 유형이 xml일 경우 <..>의 문자열들을 삭제하게 된다.
  • Tokenizer는 CharFilter와 비슷한 일을 하지만, CharFilter는 입력이 character stream이고 Tokenizer는 token stream을 사용하게 된다.

  • 처음 Opensearch를 설치하면 standard analyzer를 사용하게 되는데 이것은 CharFilter를 사용하지 않고, 모든 값이 바로 Tokenizer로 전달된다.
  • 그리고 기본 설치된 standard analyzer는 단순히 whitespace를 기준으로 토큰을 자른다.
  • 그리고 기본 standard token filter는 단순히 모든 대문자를 소문자로 바꾸는 역할만 한다.

1.2 Opensearch Tokenizer 테스트

  • 아래와 같이 테스트를 진행. standard analyzer의 Tokenizer까지만 사용하여 결과를 조회해본다.
POST _analyze
{
  "tokenizer": "standard",
  "text":"The quick brown fox jumps over the quick-dog!"
}
  • Tokenizer까지만 사용했으므로 공백과 문자를 파싱하였고, 대문자를 소문자로 바꿔 저장하진 않았다.
{
  "tokens": [
    {
      "token": "The",
      "start_offset": 0,
      "end_offset": 3,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "quick",
      "start_offset": 4,
      "end_offset": 9,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "brown",
      "start_offset": 10,
      "end_offset": 15,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "fox",
      "start_offset": 16,
      "end_offset": 19,
      "type": "<ALPHANUM>",
      "position": 3
    },
    {
      "token": "jumps",
      "start_offset": 20,
      "end_offset": 25,
      "type": "<ALPHANUM>",
      "position": 4
    },
    {
      "token": "over",
      "start_offset": 26,
      "end_offset": 30,
      "type": "<ALPHANUM>",
      "position": 5
    },
    {
      "token": "the",
      "start_offset": 31,
      "end_offset": 34,
      "type": "<ALPHANUM>",
      "position": 6
    },
    {
      "token": "quick",
      "start_offset": 35,
      "end_offset": 40,
      "type": "<ALPHANUM>",
      "position": 7
    },
    {
      "token": "dog",
      "start_offset": 41,
      "end_offset": 44,
      "type": "<ALPHANUM>",
      "position": 8
    }
  ]
}

 

  • 이번에는 Tokenizer까지 사용하는게 아니라 Analyzer를 모두 사용한다.
POST _analyze
{
  "analyzer": "standard",
  "text":"The quick brown fox jumps over the quick-dog!"
}
  • 이 경우 CharFilter와 TokenFilter를 다 적용하게 되므로 아래와 같이 영문자가 소문자로 바뀌게 된다.
{
  "tokens": [
    {
      "token": "the",
      "start_offset": 0,
      "end_offset": 3,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "quick",
      "start_offset": 4,
      "end_offset": 9,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "brown",
      "start_offset": 10,
      "end_offset": 15,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "fox",
      "start_offset": 16,
      "end_offset": 19,
      "type": "<ALPHANUM>",
      "position": 3
    },
    {
      "token": "jumps",
      "start_offset": 20,
      "end_offset": 25,
      "type": "<ALPHANUM>",
      "position": 4
    },
    {
      "token": "over",
      "start_offset": 26,
      "end_offset": 30,
      "type": "<ALPHANUM>",
      "position": 5
    },
    {
      "token": "the",
      "start_offset": 31,
      "end_offset": 34,
      "type": "<ALPHANUM>",
      "position": 6
    },
    {
      "token": "quick",
      "start_offset": 35,
      "end_offset": 40,
      "type": "<ALPHANUM>",
      "position": 7
    },
    {
      "token": "dog",
      "start_offset": 41,
      "end_offset": 44,
      "type": "<ALPHANUM>",
      "position": 8
    }
  ]
}

2. Index

  • 위와 같이 Analyzer를 사용하여 Token을 빼고나면 이를 이용해서 Indexing을 한다.
  • 아래와 같이 역 인덱스(Inverted Index) 구조를 만들어 저장한다.
  • 역 인덱스 구조로 만들어 저장하여 검색 시 해당 Token이 어느 Document에 포함되었는지 쉽게 찾을 수 있다.

https://www.researchgate.net/figure/The-geographic-scopes-inverted-index-The-inverted-index-is-used-for-fast-document_fig3_266863129

 

3. CharFilter

  • 기본적으로 3가지 CharFilter를 제공

1) HTML Strip Character Filter (html_strip)

  • 텍스트에서 HTML element를 제거하고 HTML entity를 디코딩된 값으로 바꾼다. (ex. &를 &로 변환 , <p> 제거 등등)

2) Mapping Character Filter (mapping)

  • key와 동일한 문자열을 만나면 해당 key에 대한 value 값으로 대체
  • 예시)
I'm so _sad_
 
I'm so :(

3) Pattern Replace Filter (pattern_replace)

  • 정규 표현식을 사용하여 지정된 대체 문자열로 대체해야하는 문자를 일치시킴.
  • 예시)
Pattern: ([a-zA-Z0-9]+)(-?)
Replacement:$1
 
aaa-bbb-ccc
aaabbbccc

 

4. Tokenizer

 

Word Tokenizer

단순히 단어 수준에서 작동한다. 일반적으로 Full text를 tokenizing하는데 사용한다.

  • standard Tokenizer: 공백 기반으로 tokenizing하며 특수문자는 제외된다.
It’s fun to contribute a brand-new PR or 2 to OpenSearch! → [ It’s, fun, to, contribute, a, brand, new, PR, or, 2, to, OpenSearch]
  • whitespace Tokenizer: 정확하게 공백으로만 tokenizing 된다.
It’s fun to contribute a brand-new PR or 2 to OpenSearch! → [ It’s, fun, to, contribute, a, brand-new, PR, or, 2, to, OpenSearch!]
  • uax_url_email Tokenizer: standard Tokenizer와 유사하지만, 추가로 URL과 이메일 주소를 하나의 토큰으로 남겨둔다는 특징이 있다.
It’s fun to contribute a brand-new PR or 2 to OpenSearch opensearch-project@github.com! → [ It’s, fun, to, contribute, a, brand, new, PR, or, 2, to, OpenSearch, opensearch-project@github.com]

 

 

Partial word tokenizer

부분 문자열 기반으로 Tokenizing 한다.

  • N-Gram Tokenizer (ngram): 모든 부분 문자열들로 Tokenizing한다.
Red wine → [Re, Red, ed, wi, win, wine, in, ine, ne]

 

  • Edge N-Gram Tokenizer (edge_ngram): 첫 문자를 포함하는 부분 문자열들로 Tokenizing한다.
    • 보통 검색에서 단어를 suggest 시 사용한다.
Red wine → [Re, Red, wi, win, wine]

Structured Text Tokenizer

e-mail, zip-code, ID 등의 정형화된 Text를 Tokening 할 때 사용한다.

  • Keyword Tokenizer (keyword): 전체 문자열을 변경하지 않고 키워드로 저장함.
My repo → My repo

 

  • Pattern Tokenizer (pattern): 정규 표현식 패턴을 사용하여 텍스트를 단어 구분 기호로 구문 분석하거나 일치하는 텍스트를 용어로 캡처함.
https://opensearch.org/forum → [https, opensearch, org, forum]

 

Partial word tokenizer

부분 문자열 기반으로 Tokenizing 한다.

  • N-Gram Tokenizer (ngram): 모든 부분 문자열들로 Tokenizing한다.
Red wine → [Re, Red, ed, wi, win, wine, in, ine, ne]
  • Edge N-Gram Tokenizer (edge_ngram): 첫 문자를 포함하는 부분 문자열들로 Tokenizing한다.
    • 보통 검색에서 단어를 suggest 시 사용한다.
Red wine → [Re, Red, wi, win, wine]

 

Structured Text Tokenizer

e-mail, zip-code, ID 등의 정형화된 Text를 Tokening 할 때 사용한다.

  • Keyword Tokenizer (keyword): 전체 문자열을 변경하지 않고 키워드로 저장함.
My repo → My repo
  • Pattern Tokenizer (pattern): 정규 표현식 패턴을 사용하여 텍스트를 단어 구분 기호로 구문 분석하거나 일치하는 텍스트를 용어로 캡처함.
https://opensearch.org/forum → [https, opensearch, org, forum]

 

5. TokenFilter

[The, quick, brown, fox, jumps, over, the, quick, dog] → [The, quick, brown, fox, jumps, over, the, quick, dog]
  • Lowercase Token Filter (lowercase) : 입력된 Token input을 소문자로 변경
[The, quick, brown, fox, jumps, over, the, quick, dog] → [the, quick, brown, fox, jumps, over, the, quick, dog]
  • Stop Token Filter (stop) : Stopword(불용어)를 제거해줌.
[The, quick, brown, fox, jumps, over, the, quick, dog] → [quick, brown, fox, jumps, over, quick, dog]
  • Stemmer Token Filter(stemmer) : Stem word 기반으로 분리하는 Token 필터
[I'm,in,the,mood,for,drinking,semi,dry,red,wine] → [I'm,mood,drink,semi,dry,red,wine]
  • Snowball Token Filter (snowball) : Stem word 기반으로 분리하는 Token 필터인데 Snowball이라는 알고리즘 기반으로 분리해준다고 함.
[I'm,in,the,mood,for,drinking,semi,dry,red,wine] → [I'm,mood,drink,semi,dry,red,wine]
  • Synonym Token Filter (synonym) : 동의어를 넣어주는 Token 필터
[I, am, very, happy] → [I, am,very, happy/delighted]

 

6. Analyzer

  • 앞서 다룬 Filter와 Tokenizer를 모두 포함하고 있는 컴포넌트이다.
  • 더 많은 Analyzer 참고: https://opensearch.org/docs/latest/analyzers/
  • Standard Analyzer (standard) : 기본적으로 텍스트를 단어 경계로 단어를 분리하고 구두점을 제거하고 소문자로 변경한다.
It’s fun to contribute a brand-new PR or 2 to OpenSearch! → [it’s, fun, to, contribute, a,brand, new, pr, or, 2, to, opensearch]
  • Simple Analyzer (simple) :  기본적으로 텍스트를 단어 경계로 단어를 분리하고 구두점을 제거하고 소문자로 변경한다.
It’s fun to contribute a brand-new PR or 2 to OpenSearch! → [it, s, fun, to, contribute, a,brand, new, pr, or, to, opensearch]
  • Stop Analyzer (stop) : Simple Analyzer + Stop Word 제거
It’s fun to contribute a brand-new PR or 2 to OpenSearch! → [s, fun, contribute, brand, new, pr, opensearch]
  • Keyword Analyzer (keyword) : 전체 문자열을 변경하지 않고 출력한다.
It’s fun to contribute a brand-new PR or 2 to OpenSearch! → [It’s fun to contribute a brand-new PR or 2 to OpenSearch!]

 

7. Custom Analyzer 생성하여 Index에 적용

7.1 Custom Analyzer 생성

  • 아래와 같이 custom_analyzer를 설정하여 Index 생성 시 적용하였다.
  • tokenizer는 whitespace로 공백을 기준으로 분리하였다.
  • tokenfilter는 length_filter를 추가해서 3글자 이상인 토큰만 유지되도록 지정하였다. 이외에 소문자 변환 lowercase, 각 토큰의 앞뒤 공백 제거 trim, 불용어 제거 stop을 추가했다.
INDEX_NAME = "pdf_documents"
 
# 인덱스가 존재하지 않으면 생성
if not client.indices.exists(INDEX_NAME):
    index_body = {
        "settings": {
            "analysis": {
                "filter": {
                    "length_filter": {
                        "type": "length",
                        "min": 3  # 3글자 이상인 토큰만 유지
                    }
                },
                "analyzer": {
                    "custom_analyzer": {
                        "type": "custom",
                        "tokenizer": "whitespace",
                        "filter": ["lowercase", "trim", "stop", "length_filter"]    # 각 토큰에서 앞뒤 공백 제거 및 불용어 필터
                    }
                }
            }
        },
        "mappings": {
            "properties": {
                "content": {
                    "type": "text",
                    "analyzer": "custom_analyzer"
                }
            }
        }
    }
    response = client.indices.create(index=INDEX_NAME, body=index_body)
    print(f"Index created: {response}")
else:
    print(f"Index {INDEX_NAME} already exists")

7.2 PDF 문서를 Opensearch에 인덱싱

  • 예제로 security advisory PDF 파일을 사용하여 Opensearch에 인덱싱한다.
from pdfminer.high_level import extract_text
import os
 
PDF_DATASET_PATH = 'pdf_datasets'
 
def pdf_to_text(pdf_path):
    return extract_text(pdf_path)    # PDF에서 텍스트 추출
 
pdf_texts = []
file_list = os.listdir(PDF_DATASET_PATH)
for file in file_list:
    pdf_t = pdf_to_text(f'{PDF_DATASET_PATH}/{file}')
    pdf_texts.append(pdf_t)
 
# PDF 파일을 Opensearch에 로드
for pdf_text in pdf_texts:
    doc_body = {
        "content": pdf_text
    }
    response = client.index(index=INDEX_NAME, body=doc_body)
    print(f"Document indexed: {response['_id']}")

 

7.3 Custom Analyzer 적용 결과 확인

 

Dataset of pdf files

Dataset of pdf for testing

www.kaggle.com

  • custom analyzer를 적용할 pdf 문서 첫번째 페이지는 다음과 같다.

  • 일반적인 search()로 조회 시에는 Analyzer 적용 토큰이 아닌 인덱스에 저장된 pdf 문서 원본 내용이 그대로 반환된다.
def fetch_data(index_name):
    try:
        query = {
            "query": {
                "match_all": {}
            }
        }
        # 데이터 검색
        response = client.search(index=index_name, body=query)
         
        # 검색 결과 출력
        hits = response['hits']['hits']
        for hit in hits:
            print(hit['_source'])  # 각 문서의 내용을 출력
         
    except Exception as e:
        print("Error occurred:", e)
 
fetch_data(INDEX_NAME)                  # search API는 PDF 내용 원본 텍스트를 반환함
 
'''
###############  결과 ##################
{'content': 'THE CENTRE FOR HUM ANITARIAN DATA\n\nGUIDANCE NOTE SERIES\n\nDATA RESPONSIBILIT Y IN HUMANITARIAN AC TION\n\nDATA INCIDENT MANAGEMENT\n\nKE Y TAKE AWAYS:\n\n•  Humanitarian data incidents are events involving the management of data that have caused \nharm or have the potential to cause harm to crisis-affected people, organizations and their \noperations, and other individuals or groups.\n\n•  Examples of humanitarian data incidents include physical breaches of infrastructure, \n\nunauthorised disclosure of data, and the use of beneficiary data for non-humanitarian purposes, \namong others.\n\n•  A data incident has four aspects: (i) a threat source, (ii) a threat event, (iii) a vulnerability and (iv) \n\nan adverse impact. \n\n•  There are five steps to responding to data incidents: (i) notification, (ii) classification, (iii) \n\ntreatment, and (iv) closure of the incident, as well as (v) learning.\n\nWHAT IS A DATA INCIDENT IN HUMANITARIAN RESPONSE ?\n\nIn the humanitarian sector, data incidents are events involving the management of data that have caused \nharm or have the potential to cause harm to crisis affected populations, humanitarian organisations \nand their operations, and 
... (생략) ...
 
'''

  • analyze API를 사용하여 custom analyzer 동작을 확인할 수 있다.
analyze_body = {
    "analyzer": "custom_analyzer",              # 위에서 정의한 custom_analyzer 이름
    "text": pdf_texts[0]                                    # 분석할 텍스트
}
 
response = client.indices.analyze(index=INDEX_NAME, body=analyze_body)                  # analyze API를 사용하여 custom analyzer 동작 확인
print(response)
 
'''
###############  결과 ##################
 
{'tokens': [
    {'token': 'centre', 'start_offset': 4, 'end_offset': 10, 'type': 'word', 'position': 1},
    {'token': 'hum', 'start_offset': 15, 'end_offset': 18, 'type': 'word', 'position': 3},
    {'token': 'anitarian', 'start_offset': 19, 'end_offset': 28, 'type': 'word', 'position': 4},
    {'token': 'data', 'start_offset': 29, 'end_offset': 33, 'type': 'word', 'position': 5},
    {'token': 'guidance', 'start_offset': 35, 'end_offset': 43, 'type': 'word', 'position': 6},
    {'token': 'note', 'start_offset': 44, 'end_offset': 48, 'type': 'word', 'position': 7},
    {'token': 'series', 'start_offset': 49, 'end_offset': 55, 'type': 'word', 'position': 8},
    {'token': 'data', 'start_offset': 57, 'end_offset': 61, 'type': 'word', 'position': 9},
    {'token': 'responsibilit', 'start_offset': 62, 'end_offset': 75, 'type': 'word', 'position': 10},
    {'token': 'humanitarian', 'start_offset': 81, 'end_offset': 93, 'type': 'word', 'position': 13},
    {'token': 'tion', 'start_offset': 97, 'end_offset': 101, 'type': 'word', 'position': 15},
    {'token': 'data', 'start_offset': 103, 'end_offset': 107, 'type': 'word', 'position': 16},
    {'token': 'incident', 'start_offset': 108, 'end_offset': 116, 'type': 'word', 'position': 17},
    {'token': 'management', 'start_offset': 117, 'end_offset': 127, 'type': 'word', 'position': 18},
    {'token': 'take', 'start_offset': 134, 'end_offset': 138, 'type': 'word', 'position': 21},
    {'token': 'aways:', 'start_offset': 139, 'end_offset': 145, 'type': 'word', 'position': 22},
    {'token': 'humanitarian', 'start_offset': 150, 'end_offset': 162, 'type': 'word', 'position': 24},
    {'token': 'data', 'start_offset': 163, 'end_offset': 167, 'type': 'word', 'position': 25},
    {'token': 'incidents', 'start_offset': 168, 'end_offset': 177, 'type': 'word', 'position': 26},
    {'token': 'events', 'start_offset': 182, 'end_offset': 188, 'type': 'word', 'position': 28},
    {'token': 'involving', 'start_offset': 189, 'end_offset': 198, 'type': 'word', 'position': 29},
    {'token': 'management', 'start_offset': 203, 'end_offset': 213, 'type': 'word', 'position': 31},
    {'token': 'data', 'start_offset': 217, 'end_offset': 221, 'type': 'word', 'position': 33},
    {'token': 'have', 'start_offset': 227, 'end_offset': 231, 'type': 'word', 'position': 35},
    ... (생략) ...
 
'''

 

Todo

  • pdf에서 텍스트 추출 시 공백 문자 후처리
728x90
반응형