문서번호 : 11-3249307
Document Information
•
최초 작성일 : 2025.02.25
•
최종 수정일 : 2025.02.25
•
이 문서는 아래 버전을 기준으로 작성되었습니다.
◦
Singlestore : 8.9.5
Goal
Full Text Search2 의 Korean(Nori) Analyzer 에 대해 알아본다.
Solution
사전정보
<Java Lucene 의 특징>
•
기존 버전1 C-Lucene 의 Score 계산 방식은 TF-IDF 알고리즘을 사용하지만, 버전2는 Java Lucene을 사용하며 이는 기본적으로 bm-25 알고리즘을 사용합니다. 이는TF-IDF를 개선한 정교한 알고리즘입니다.
•
full text search version2 관련 쿼리를 처음 실행할 때, 프로세스가 생성됩니다.
<BM-25 알고리즘의 특징>
•
문서 길이 정규화를 통해 문서의 길이를 고려하여 점수를 조정합니다.
•
흔한 단어는 점수가 낮게 계산되지만, 드문 단어는 높은 점수로 계산합니다.
•
상한이 없으며 높을수록 높은 품질의 매칭임을 의미합니다.
<SingleStore : Korean(Nori) Analyzer>
•
Feature Request PM-3143 을 통해 SingleStore 8.9.3 부터 Built-in ver Korean Analyzer를 지원 시작했으며, 8.9.5부터 Custom ver Korean Filter와 Tokenizer를 제공합니다.
•
해당 기능은 Nori 한글 형태소 분석기를 사용하며 기존 CJK와는 별도의 Analyzer 입니다.
<Built-in vs Custom version>
Built in Analyzer : 사전 설정된 규칙과 기능으로 텍스트 데이터를 처리하며, 별도의 설정 없이 사용 가능합니다. 다른 옵션(예: Tokenizer 변경, UserDirectory 생성 등)은 불가능합니다.
FULLTEXT USING VERSION 2 (col) INDEX_OPTIONS'{"analyzer": "korean"}'
SQL
복사
예시
Custom Analyzer : 사용자가 분석기의 옵션을 직접 구성할 수 있으며, Tokenizer 및 Token filter를 선택하여 텍스트 처리 과정을 맞춤화할 수 있습니다.
'{ "analyzer": { "custom": { "tokenizer": { "korean": {
"userDictionary": ["싱글스토어"],
"decompoundMode": none,
"outputUnknownUnigrams": true,
"discardPunctuation": true } },
"token_filters": ["korean_part_of_speech",
"korean_reading_form",
"korean_number"] } } }'
Shell
복사
예시 : FULLTEXT USING VERSION 2 (col) INDEX_OPTIONS
Custom Analyzer의 구성 요소
1.
Character Filters
텍스트를 변환하거나 수정하며, 0개 이상 선택적으로 사용 가능합니다.
현재 8.9.5v 에서 Korean character filter는 존재하지 않습니다.
예: HTML 태그 제거, cjk_width
2.
Tokenizer
텍스트를 개별 토큰으로 분리하며 필수적으로 1개의 토크나이저를 선택해야 합니다.
예 : Korean : 한국어용 토크나이저 (userDictionary, decompoundMode, outputUnknownUnigrams, discardPunctuation의 옵션을 포함)
3.
Token Filters
토큰을 추가하거나 제거 또는 수정하며 0개 이상 선택적으로 사용 가능합니다.
예: 특정 품사(조사, 감탄사, ..) 토큰 제거, 한자 한글로 변환, 한글 숫자로 변환 ..
처리 순서
Character Filters → Tokenizer → Token Filters
<NOTE>
현재 문서의 문법 예시는 Built in Analyzer 기준으로 작성되었으며,
Custom Analyzer의 Tokenizer 및 Token Filter는 마지막 단락에서 다룹니다.*
<CJK vs Korean(Nori)>
CJK Analyzer는 전각문자를 반각 문자로 변환하는데 중점을 두며, 문맥을 고려하지 않고 단순 문자 단위로 토큰화합니다.
이는 영어 및 일어, 중어, 한국어 모두 지원하지만 전각문자가 주로 쓰이는 중국어와 일본어에서 유리합니다.
단어의 조합을 2개이상 검색하여 조회가 가능합니다.
중국어와 일본어는 stopword를 지정할 수 있지만 한글은 불가능합니다.
예시1: ワサビテスト。(전각) -> ワ, サ, ヒ, テ, ス, ト (반각 문자로 변환 후 문자 단위 분리)
조회 : ワサ, サヒ, ヒテス, スト ..
예시2 : 서울에 있는 강아지들 백마리 -> (서,울,에), (있,는), (강,아,지,들), (백,마,리)
조회 : 서울, 울에, 서울에, 있는, 강아, 아지, 지들, 강아지, 아지들, 백마, ..
SQL
복사
Korean(Nori) Analyzer는 한국어 전용 형태소 분석기입니다.
한국어의 복잡한 문법과 문맥을 이해하며, 이를 기반으로 텍스트를 한국어 형태소 단위로 분리합니다.
어간 및 접사를 제거하며, 복합어 분리 및 처리가 가능합니다.
stopword 지정이 불가능하며 후에 User-Dictionary 기능 추가 예정입니다.
예시 : 서울에 있는 강아지들 백마리 -> 서울, 있다, 강아지, 백, 마리
조회 : 서울, 백, 마리, 강아지, 있다, 있음, 있으며, 있죠, ..
SQL
복사
<Korean Analyzer : Built-in 테이블 생성 구문>
CREATE TABLE novel (
title varchar(30), content varchar(200),
FULLTEXT USING VERSION 2 (content) INDEX_OPTIONS
'{ "analyzer": "korean"}' );
SQL
복사
FULLTEXT USING VERSION 2 (title, content) INDEX_OPTIONS
SQL
복사
인덱싱할 컬럼을 두 개 이상 지정하여 테이블 생성도 가능합니다.
<테스트 데이터 INSERT 및 FLUSH>
INSERT INTO novel (title, content) VALUES
('토지', '한 가족의 삶과 한국 근대사의 변화를 그린 이야기입니다.'),
('난장이가 쏘아올린 작은 공', '산업화로 인한 소외와 인간성을 다룬 단편입니다.'),
('데미안', '청소년의 성장과 자아 발견을 철학적으로 탐구한 이야기입니다.'),
('우리들의 일그러진 영웅', '권력과 정의를 사회적으로 조명한 이야기입니다.'),
('그 많던 싱아는 누가 다 먹었을까', '한국 전쟁과 성장의 기억을 담은 작품입니다.'),
('동백꽃', '농촌에서의 순박한 사랑 이야기를 담은 작품입니다.'),
('운수 좋은 날', '일제강점기 하층민의 삶을 묘사한 작품입니다.'),
('소나기', '첫사랑의 감정과 순수를 그린 이야기입니다.'),
('빨간 머리 앤', '소녀의 성장과 희망을 그린 이야기입니다.'),
('채식주의자', '내면의 폭력과 억압을 탐구한 이야기입니다.');
OPTIMIZE TABLE novel FLUSH;
SQL
복사
<MATCH, BM25, BM25_GLOBAL>
Korean Analyze 에서 인덱싱된 데이터는 MATCH, BM25, BM25_GLOBAL 세개의 함수를 사용하여 텍스트 점수를 계산할 수 있습니다.
<MATCH>
segment-level에서 BM25 알고리즘을 사용하여 점수가 계산됩니다.
구문 : MATCH(TABLE <table_name>) AGAINST(<expression>)
예 : SELECT MATCH(TABLE novel) AGAINST("content:(이야기)") FROM novel;
SQL
복사
<BM25>
partition-level에서 BM25 알고리즘을 사용하여 점수가 계산됩니다.
각 파티션 내에서 데이터가 어떻게 분포되는지에 따라 점수가 달라질 수 있으며,
텍스트가 모든 파티션에 (row 수가 아닌) 텍스트가 균등하게 분산되어있다면 BM25_GLOBAL 만큼 정확한 점수를 생성 가능합니다.
구문 : BM25(<table_name>, <expression>)
예 : SELECT BM25(novel,"content:(이야기)") FROM novel;
SQL
복사
<BM25_GLOBAL>
Table-level 에서 BM25 알고리즘을 사용하여 점수가 매겨집니다.
모든 행에 대해 정확한 점수를 보장합니다.
구문 : BM25_GLOBAL(<table_name>, <expression>)
예 : SELECT BM25_GLOBAL(novel,"content:(이야기)") FROM novel;
SQL
복사
효율성 : MATCH > BM25 > BM25_GLOBAL
정확성 : BM25_GLOBAL > BM25 > MATCH
BM25 계산에 필요한 통계 정보
•
문서의 총 개수
•
전체 토큰 개수
•
특정 용어가 등장하는 문서의 수
•
해당 용어의 총 출현 횟수
<Operators>
Java Lucene의 Full text search2 는 문자열 구문의 연산자가 지원됩니다.
함수 구문의 expression 에서 ("col:(~~)") 형식으로 사용될 때 연산자를 사용할 수 있습니다.
세 함수의 expression 표현 구문은 같습니다.
1.
(No Operator)
단어가 선택적(Optional)으로 포함될 수 있습니다.
예1 : SELECT MATCH(TABLE novel) AGAINST("content:(이야기)") FROM novel;
예2 : SELECT BM25_GLOBAL(novel,"content:(사랑 농촌 이야기)") FROM novel;
SQL
복사
2.
+, AND, &&
특정 단어가 반드시 포함되어야 합니다. AND는 반드시 대문자여야 합니다.
예1 : SELECT MATCH(TABLE novel) AGAINST("content:(탐구 +성장 이야기)") FROM novel;
예2 : SELECT BM25(novel,"content:(탐구 AND 성장 && 이야기)") FROM novel;
SQL
복사
3.
- , NOT, !
특정 단어가 포함되지 않아야 합니다. NOT은 반드시 대문자여야 합니다.
예1 : SELECT MATCH(TABLE novel) AGAINST("content:(탐구 -성장)") FROM novel;
예2 : SELECT BM25(novel,"content:(묘사 !성장 NOT 이야기)") FROM novel;
SQL
복사
4.
OR , ||
두 단어 중 하나라도 포함된 결과를 반환합니다.
예1 : SELECT MATCH(TABLE novel) AGAINST("content:(탐구 OR 성장)") FROM novel;
예2 : SELECT BM25_GLOBAL(novel,"content:(탐구 || 성장 OR 사랑)") FROM novel;
SQL
복사
5.
() (Parentheses)
그룹화하여 우선순위를 지정 가능합니다.
예1 : SELECT MATCH(TABLE novel) AGAINST("content:((탐구 OR 성장) AND 억압)") FROM novel;
예2 : SELECT BM25_GLOBAL(novel,"content:(탐구 && (성장 || 억압))") FROM novel;
SQL
복사
6.
"" (Phrase Matching)
큰따옴표 안의 정확한 문장을 검색합니다. ('col:("~~ ~~")') 의 구문으로 사용 가능합니다.
예1 : SELECT MATCH(TABLE novel) AGAINST('content:("폭력 억압")') FROM novel;
예2 : SELECT BM25(novel,'content:("폭력과 억압")') FROM novel;
SQL
복사
7.
*, ? (Wildcard)
구체적인 단어 대신 여러 단어를 동시에 지정합니다. (MATCH AGAINST 에서만 사용 가능)
* : 0개 이상의 문자를 대체합니다.
? : 정확히 1개의 문자를 대체합니다.
예1 : SELECT , MATCH(TABLE novel) AGAINST("title:(난장)") FROM novel;
예2 : SELECT *, MATCH(TABLE novel) AGAINST("title:(난??)") FROM novel;
SQL
복사
8.
~ (Fuzzy)
철자가 유사한 단어를 검색합니다. (MATCH AGAINST 에서만 사용 가능)
예1 : SELECT *, MATCH(TABLE novel) AGAINST("title:(난장삼사~)") FROM novel;
예2 : SELECT *, MATCH(TABLE novel) AGAINST("title:(난쟁이~)") FROM novel;
SQL
복사
참고
인덱싱할 컬럼을 두 개 이상 지정하여 테이블을 생성했다면, 한 함수안에서 여러 컬럼에 대해 여러 연산자를 사용할 수 있습니다.
예1 : SELECT *, MATCH(TABLE t2) AGAINST("content:(순수) ||
(title:(싱아 OR 데미안) AND content:(성장 +철학))") FROM t2;
예2 : SELECT *, BM25(t2,"content:(순수) ||
(title:(싱아 OR 데미안) AND content:(성장 -철학))") FROM t2;
SQL
복사
<BM25_GLOBAL 함수를 사용한 알고리즘의 특징>
1. 문서 길이 정규화
SELECT round(BM25_GLOBAL(novel,'content:성장'),3) as score, content, title FROM novel order by score desc;
+-------+------------------------------------------------------------------------------+------------------------------+
| score | content | title |
+-------+------------------------------------------------------------------------------+------------------------------+
| 0.559 | 소녀의 성장과 희망을 그린 이야기입니다. | 빨간 머리 앤 |
| 0.526 | 한국 전쟁과 성장의 기억을 담은 작품입니다. | 그 많던 싱아는 누가 다 먹었을까 |
| 0.472 | 청소년의 성장과 자아 발견을 철학적으로 탐구한 이야기입니다. | 데미안 |
| 0.000 | 일제강점기 하층민의 삶을 묘사한 작품입니다. | 운수 좋은 날 |
| 0.000 | 내면의 폭력과 억압을 탐구한 이야기입니다. | 채식주의자 |
| 0.000 | 첫사랑의 감정과 순수를 그린 이야기입니다. | 소나기 |
| 0.000 | 농촌에서의 순박한 사랑 이야기를 담은 작품입니다. | 동백꽃 |
| 0.000 | 한 가족의 삶과 한국 근대사의 변화를 그린 이야기입니다. | 토지 |
| 0.000 | 권력과 정의를 사회적으로 조명한 이야기입니다. | 우리들의 일그러진 영웅 |
| 0.000 | 산업화로 인한 소외와 인간성을 다룬 단편입니다. | 난장이가 쏘아올린 작은 공 |
+-------+------------------------------------------------------------------------------+-------------------------------+
SQL
복사
같은 단어가 검색되어도 점수가 다른 이유
텍스트에 같은 단어가 포함되어있더라도,
텍스트의 길이가 모든 행(BM25_GLOBAL)과 비교해 정규화되기 때문입니다.
빨간머리앤의 점수가 가장 높은 이유
'성장'키워드가 존재하는 텍스트 중 빨간머리앤의 텍스트가 가장 짧기 때문입니다.
빨간머리앤 : [소녀, 성장, 희망, 그린, 이야기] -- 5개
그 많던 싱아는 누가 다 먹었을까 : [한국, 전쟁, 성장, 기억, 담은, 작품] -- 6개
데미안 : [청, 소년, 성장, 자아, 발견, 철학, 탐구, 이야기] -- 8개
2. 단어의 출현율
SELECT round(BM25_GLOBAL(novel,'content:성장'),3) as score, content, title FROM novel order by score desc;
+-------+------------------------------------------------------------------------------+------------------------------+
| score | content | title |
+-------+------------------------------------------------------------------------------+------------------------------+
| 0.559 | 소녀의 성장과 희망을 그린 이야기입니다. | 빨간 머리 앤 |
| 0.526 | 한국 전쟁과 성장의 기억을 담은 작품입니다. | 그 많던 싱아는 누가 다 먹었을까 |
| 0.472 | 청소년의 성장과 자아 발견을 철학적으로 탐구한 이야기입니다. | 데미안 |
| 0.000 | 일제강점기 하층민의 삶을 묘사한 작품입니다. | 운수 좋은 날 |
| 0.000 | 내면의 폭력과 억압을 탐구한 이야기입니다. | 채식주의자 |
| 0.000 | 첫사랑의 감정과 순수를 그린 이야기입니다. | 소나기 |
| 0.000 | 농촌에서의 순박한 사랑 이야기를 담은 작품입니다. | 동백꽃 |
| 0.000 | 한 가족의 삶과 한국 근대사의 변화를 그린 이야기입니다. | 토지 |
| 0.000 | 권력과 정의를 사회적으로 조명한 이야기입니다. | 우리들의 일그러진 영웅 |
| 0.000 | 산업화로 인한 소외와 인간성을 다룬 단편입니다. | 난장이가 쏘아올린 작은 공 |
+-------+------------------------------------------------------------------------------+-------------------------------+
SQL
복사
같은 단어가 포함돼있어도 점수가 다른 이유
(흔한)'성장'키워드는 3개 행에 포함되어 있지만
(드문)'탐구'키워드는 1개의 행에만 포함되어 높은 점수로 계산됩니다.
SELECT round(BM25_GLOBAL(novel,'content:성장,탐구하다'),3) as score, content, title FROM novel order by score desc;
+-------+-------------------------------------------------------------------------------+-----------------------------+
| score | content | title |
+-------+-------------------------------------------------------------------------------+-----------------------------+
| 1.083 | 청소년의 성장과 자아 발견을 철학적으로 탐구한 이야기입니다. | 데미안 |
| 0.723 | 내면의 폭력과 억압을 탐구한 이야기입니다. | 채식주의자 |
| 0.559 | 소녀의 성장과 희망을 그린 이야기입니다. | 빨간 머리 앤 |
| 0.526 | 한국 전쟁과 성장의 기억을 담은 작품입니다. | 그 많던 싱아는 누가 다 먹었을까 |
| 0.000 | 일제강점기 하층민의 삶을 묘사한 작품입니다. | 운수 좋은 날 |
| 0.000 | 첫사랑의 감정과 순수를 그린 이야기입니다. | 소나기 |
| 0.000 | 농촌에서의 순박한 사랑 이야기를 담은 작품입니다. | 동백꽃 |
| 0.000 | 한 가족의 삶과 한국 근대사의 변화를 그린 이야기입니다. | 토지 |
| 0.000 | 권력과 정의를 사회적으로 조명한 이야기입니다. | 우리들의 일그러진 영웅 |
| 0.000 | 산업화로 인한 소외와 인간성을 다룬 단편입니다. | 난장이가 쏘아올린 작은 공 |
+-------+-------------------------------------------------------------------------------+-----------------------------+
SQL
복사
점수해석
'성장'과 '탐구' 키워드가 모두 포함된 "데미안"이 1순위,
'탐구' 키워드가 포함된 2개의 행 중 하나인 "채식주의자"가 2순위,
'성장' 키워드가 포함되어있는 3개의 행 중 토큰화 개수가 더 적은 "빨간머리앤"이 "그 많던 싱아는 누가 다 먹었을까" 보다 점수가 높습니다.
<Korean(Nori) Analyzer 형태소>
korean Analyzer는 Stop Tags(불필요한 품사)에 해당하는 단어를 제거합니다.
텍스트를 아래와 같이 형태소로 따로따로 분리하여 분석합니다. (형태소 대분류 별 정리)
1.
어미 및 연결 요소(Stop Tags)
EC: 연결 어미 (문장을 연결하는 어미, 예: -고, -며)
EF: 종결 어미 (문장을 끝내는 어미, 예: -다, -요)
EP: 선어말 어미 (동사나 형용사에 붙어 시제나 태도 표시, 예: -겠-, -었-)
ETM: 관형형 전성 어미 (동사/형용사가 관형어가 되도록 변형, 예: -는, -던)
ETN: 명사형 전성 어미 (동사/형용사가 명사화, 예: -기, -음)
2.
품사와 관련된 접사 및 조사(Stop Tags)
JC: 접속 조사 (문장을 연결하는 조사, 예: -와, -그리고)
JKB: 부사격 조사 (예: -로, -으로)
JKC: 보격 조사 (보어를 나타내는 조사, 예: -이다)
JKG: 관형격 조사 (예: -의)
JKO: 목적격 조사 (예: -을, -를)
JKQ: 인용 조사 (예: -라고)
JKS: 주격 조사 (예: -이, -가)
JKV: 호격 조사 (예: -야, -여)
JX: 보조사 (예: -도, -만)
3.
부사, 관형사, 감탄사(Stop Tags)
MAG: 일반 부사 (예: 빨리, 많이)
MAJ: 접속 부사 (예: 그러나, 그래서)
MM: 관형사 (예: 새, 이, 그)
IC: 감탄사 (예: 아!, 어머!)
4.
명사 관련 태그
NNG: 일반 명사 (예: 사람, 책)
NNP: 고유 명사 (예: 서울, 노재희)
NNB: 의존 명사 (예: 것, 수)
NNBC: 단위 의존 명사 (예: 명, 개)
NP: 대명사 (예: 나, 너)
NR: 수사 (숫자를 나타내는 단어, 예: 하나, 둘)
5.
동사 및 형용사
VV: 동사 (예: 먹다, 가다)
VA: 형용사 (예: 예쁘다, 크다)
VX: 보조 동사/형용사 (예: -있다, -없다)
VCN: 부정 지정사 (예: 아니다)
VCP: 긍정 지정사 (예: 이다)
6.
기타
SC: 구분자 (·, :, / 등) --Stop Tags
SE: 생략 부호 (...) --Stop Tags
SF: 문장 종결 부호 (?, !, .) --Stop Tags
SL: 외국어 (영어 등 다른 언어 단어)
SH: 한자 (예: 漢字)
SN: 숫자 (예: 1, 2, 3)
SP: 공백 --Stopword
SY: 기타 기호 (특수 문자 등) --Stop Tags
7.
접두사, 접미사, 어근(Stop Tags)
XPN: 접두사 (예: 신- in 신제품)
XR: 어근 (예: 복잡- in 복잡성)
XSA: 형용사 접미사 (예: -답다 in 사람답다)
XSN: 명사 접미사 (예: -들 in 친구들)
XSV: 동사 접미사 (예: -하다 in 공부하다)
이중 4. 명사 관련 태그, 5. 동사 및 형용사, 6. 기타 (외국어, 한자, 숫자) 를 제외한 나머지를 stopword로 지정하여 제거하며 명사,동사,형용사,기타(외국어,한자,숫자)의 분리된 형태소를 검색에 사용합니다.
외국어, 한자, 숫자는 단어가 정확히 일치해야 검색 가능합니다.
<형태소 대분류 별 예시>
아래의 각 예시는
1.
테이블 생성, 텍스트 삽입, Optimize
2.
쿼리
3.
테이블 삭제
순으로 테스트를 반복합니다.
CREATE TABLE t (
content TEXT,
FULLTEXT USING VERSION 2 (content) INDEX_OPTIONS
'{ "analyzer": "korean"}'
);
SQL
복사
테이블 생성
1. 어미 및 연결 요소(Stop Tags)
그녀가 열심히 공부하겠다며 결심했고 나도 노력하는 것이 필요하다고 느꼈다
그녀(NP)가(JKS) 열심히(MAG) 공부(NNG)하(XSV)겠(EP)다(EF)며(EC)
결심(NNG)하(XSV)였(EP)고(EC) 나(NP)도(JX) 노력(NNG)하(XSV)는(ETM) 것(NNB)이(JKC)
필요(NNG)하(XSV)다(EF)고(EC) 느끼(VV)었(EP)다(EF).
Stop Tags 제외 -> [그녀, 공부, 결심, 나, 노력, 것, 필요, 느끼다]
SELECT BM25_GLOBAL(t,'content:그녀가열심히공부하겠다며결심했고나도노력하는 것이필요하다고느꼈다') FROM t; -- 1.046116590499878
SELECT BM25_GLOBAL(t,'content:그녀,공부,결심,나,노력,것,필요,느끼다') FROM t; -- 1.046116590499878
SQL
복사
2. 품사와 관련된 접사 및 조사(Stop Tags)
그녀와 나도 책을 읽고 생각의 차이가 크다고 말했다
그녀(NP) 와(JC) 나(NP) 도(JX) 책(NNG) 을(JKO) 읽(VV) 고(EC) 생각(NNG) 의(JKG)
차이(NNG) 가(JKS) 크(VA) 다(EF) 고(JKQ) 말했(VV) 다(EF)
Stop Tags 제외 -> [그녀, 나, 책, 읽다, 생각, 차이, 크다, 말하다]
SELECT BM25_GLOBAL(t,'content:그녀와나도책을읽고생각의차이가크다고말했다') FROM t; -- 1.046116590499878
SELECT BM25_GLOBAL(t,'content:그녀,나,책,읽다,생각,차이,크다,말하다') FROM t; -- 1.046116590499878
SQL
복사
3. 부사, 관형사, 감탄사(Stop Tags)
아! 그 친구는 빨리 떠났지만 그래서 더 아쉽다
아!(IC) 그(MM) 친구(NNG) 는(JKS) 빨리(MAG) 떠났(VV) 지만(MAJ)
그래서(MAJ) 더(MAG) 아쉽(VA) 다(EF)
Stop Tags 제외 -> [친구, 떠나다, 아쉽다]
SELECT BM25_GLOBAL(t,'content:아그친구는빨리떠났지만그래서더아쉽다') FROM t; -- 0.3922937214374542
SELECT BM25_GLOBAL(t,'content:친구,떠나다,아쉽다') FROM t; -- 0.3922937214374542
SQL
복사
4. 명사 관련 태그
서울에 있는 개미 백마리가 하나 둘 모여 서로 대화를 나누었다.
서울(NNP) 에(JKB) 있는(ETM) 개미(NNG) 백(NR) 마리(NNBC) 가(JKS)
하나(NR) 둘(NR) 모여(VV) 서로(MAG) 대화(NNG) 를(JKO) 나누었다(VV)
Stop Tags 제외 -> [서울, 있다, 개미, 백, 마리, 하나, 둘, 모이다, 대화, 나누다]
SELECT BM25_GLOBAL(t,'content:서울에있는개미백마리가하나둘모여서로대화를 나누었다') FROM t; -- 1.3076457977294922
SELECT BM25_GLOBAL(t,'content:서울,있다,개미,백,마리,하나,둘,모이다,대화, 나누다') FROM t; -- 1.3076457977294922
SQL
복사
5. 동사 및 형용사
그녀가 크다고 했지만 나는 아니다라고 말하며 이것이 맞다고 느낄 수 없었다.
그녀(NP) 는(JKS) 크다(VA) 고(EF) 했(VV) 지만(MAJ)
나(NP) 는(JKS) 아니다(VCN) 라고(JKQ) 말하(VV) 며(EC),
이(NP) 것(NNB) 이(JKS) 맞다(VCP) 고(EF) 느끼(VV) ㄹ(ETM) 수(NNB) 없(VX) 었다(EF)
Stop Tags 제외 -> [그녀, 크다, 했다, 나, 아니다, 말하다, 이것, 맞다, 느끼다, 수, 없다]
SELECT BM25_GLOBAL(t,'content:그녀가크다고했지만나는아니다라고말하며이것이 맞다고느낄수없었다') FROM t; -- 1.4384102821350098
SELECT BM25_GLOBAL(t,'content:그녀,크다,했다,나,아니다,말하다,이것,맞다, 느끼다,수,없다') FROM t; -- 1.4384102821350098
SQL
복사
6. 기타
★회의 일정은 2024년 12월 18일(Wed)에 진행되며, 장소는 독도(獨島) ... 참가비는 30,000원이며, 모든 준비물을 지참해주세요!
★(SY) 회의(NNG) 일정(NNG)은(JX) 18(SN)일(NNB) 2(SN) pm(SL) 에(JKB) 진행되(VV) 며(EC) ,(SC)
장소(NNG)는(JX) 독도(NNP) '('(SC) 獨島(SH) ')'(SC) ...(SE) ,(SC)
참가비(NNG)는(JX) 30(SN) ,(SC) 000(SN) 원(NNB) 을(JKO) 지참하(VV) 아(EF) 주시(VX) 어요(EF)!(SF)
Stop Tags 제외 -> [회의, 일정, 18, 일, 2, pm, 진행, 장소, 독도, 獨島, 참가비, 30, 000, 원, 지참, 해주다]
SELECT BM25_GLOBAL(t,'content:★회의일정은18일2pm에진행되며,장소는독도獨島...,참가비는30,000원을지참해주세요') FROM t; -- 2.059542179107666
SELECT BM25_GLOBAL(t,'content:회의,일정,18,일,2,pm,진행,장소,독도,獨島,참가비,30,000,원,지참,해주다') FROM t; -- 2.059542179107666
SQL
복사
7. 접두사, 접미사, 어근(Stop Tags)
새제품들은 신기술답게 복잡성을 요구한다
새(XPN) 제품(NNG) 들(XSN) 은(JKS) 신(XPN) 기술(NNG) 답(XSA) 게(EC)
복잡(NNG) 성(XSN) 을(JKO) 요구(vv) 한(vv) 다(EF)
Stop Tags 제외 -> [제품, 기술, 복잡, 요구]
SELECT BM25_GLOBAL(t,'content:새제품들은신기술답게복잡성을요구한다') FROM t; -- 0.523058295249939
SELECT BM25_GLOBAL(t,'content:제품,기술,복잡,요구하다') FROM t; -- 0.523058295249939
SQL
복사
<Custom Analyzer : Korean>
8.9.5버전부터 Custom Korean Tokenizer와 token_filters를 제공합니다. 기존 Built-in 버전보다 많은 기능을 커스텀하여 설정할 수 있습니다.
CREATE TABLE t (
content VARCHAR(200),
FULLTEXT USING VERSION 2 (content)
INDEX_OPTIONS '{
"analyzer": {
"custom": {
"tokenizer": {
"korean": {
"userDictionary": ["싱글스토어"],
"decompoundMode": none,
"outputUnknownUnigrams": true,
"discardPunctuation": true
}
},
"token_filters": ["korean_part_of_speech",
"korean_reading_form",
"korean_number"]
}
}
}'
);
SQL
복사
구문 예시
<Tokenizer : Korean>
1.
userDictionary
userDictionary 기능은 v8.9.11 부터 사용 가능합니다.
사용자 정의 단어를 포함하는 사전입니다. 이 배열에 포함된 단어들은 형태소 분석 과정에서 우선적으로 사용되며, 특정 단어를 별도로 정의하거나 처리가능합니다.
1) userDictionary를 사용하지 않았을 때
CREATE TABLE t (
content VARCHAR(200),
FULLTEXT USING VERSION 2 (content)
INDEX_OPTIONS '{
"analyzer": {
"custom": {
"tokenizer": {
"korean": {
}
}
}
}
}');
INSERT INTO t (content) VALUES
('싱글'),
('스토어'),
('싱글스토어');
OPTIMIZE TABLE t FLUSH;
SELECT content, BM25_GLOBAL(t,'content:싱글') as score,
BM25_GLOBAL(t,'content:스토어') as score2,
BM25_GLOBAL(t,'content:싱글스토어') as score3
FROM t;
+-----------------+---------------------+---------------------+---------------------+
| content | score | score2 | score3 |
+-----------------+---------------------+---------------------+---------------------+
| 싱글 | 0.23797652125358582 | 0 | 0.23797652125358582 |
| 스토어 | 0 | 0.23797652125358582 | 0.23797652125358582 |
| 싱글스토어 | 0.17735984921455383 | 0.17735984921455383 | 0.35471969842910767 |
+-----------------+---------------------+---------------------+---------------------+
-- `싱글스토어` 같은 합성어는 두개의 명사로 이뤄져 (싱글+스토어)로 검색된다.
2) userDictionary를 사용할 때
CREATE TABLE t (
content VARCHAR(200),
FULLTEXT USING VERSION 2 (content)
INDEX_OPTIONS '{
"analyzer": {
"custom": {
"tokenizer": {
"korean": {
"userDictionary": ["싱글스토어"]
}
}
}
}
}');
INSERT INTO t (content) VALUES
('싱글'),
('스토어'),
('싱글스토어');
OPTIMIZE TABLE t FLUSH;
SELECT content, BM25_GLOBAL(t,'content:싱글') as score,
BM25_GLOBAL(t,'content:스토어') as score2,
BM25_GLOBAL(t,'content:싱글스토어') as score3
FROM t;
+-----------------+--------------------+--------------------+--------------------+
| content | score | score2 | score3 |
+-----------------+--------------------+--------------------+--------------------+
| 싱글 | 0.4458314776420593 | 0 | 0 |
| 스토어 | 0 | 0.4458314776420593 | 0 |
| 싱글스토어 | 0 | 0 | 0.4458314776420593 |
+-----------------+--------------------+--------------------+--------------------+
-- `싱글스토어` 가 userDictionary에 추가되어 하나의 독립적인 단어로 정의된다.
SQL
복사
예시
2.
decompoundMode
토크나이저가 복합명사를 어떻게 토큰화할지 방식을 결정합니다. (default : discard)
"custom": {"tokenizer": {"korean": {"decompoundMode": none } } }
<예 : 요리경연대회>
none : 복합명사로 분리하지 않는다. > [요리경연대회]
discard : 복합명사로 분리하고 원본 데이터는 삭제한다. > [요리, 경연, 대회]
mixed : 복합명사로 분리하고 원본 데이터는 유지한다. > [요리, 경연, 대회, 요리경연대회]
SQL
복사
3.
outputUnknownUnigrams
토크나이저가 알 수 없는 단어를 만났을 때, 이를 개별 유니그램(글자단위)로 나누어 토큰화할지 여부를 결정합니다. (default : false)
"custom": {"tokenizer": {"korean": {"outputUnknownUnigrams": true } } }
<예 : 갹냔댣>
true : 알 수 없는 단어를 개별 글자 단위로 토큰화 > [갹,냔,댣]
false: 알 수 없는 단어를 토큰화하지 않음. > [갹냔댣]
SQL
복사
4.
discardPunctuation
구두점(예: . , ! ? ) 과 같은 특수문자를 출력 결과에서 제외할지 결정합니다. (default : true)
"custom": {"tokenizer": {"korean": {"discardPunctuation": false } } }
INSERT INTO custom (content) VALUES
('안녕.'),('안녕..'),('안녕...'),('안녕....'),('안녕.!'),('안녕.!!'),
('안녕!'),('안녕!!');
OPTIMIZE TABLE custom FLUSH;
<discardPunctuation: true> (Default)
SELECT *, BM25_global(custom,"content:('안녕.')") as score FROM custom
+------------+-----------------------------+
| content | score |
+------------+-----------------------------+
| 안녕.. | 0.025981096550822258 |
| 안녕... | 0.025981096550822258 |
| 안녕.!! | 0.025981096550822258 |
| 안녕! | 0.025981096550822258 |
| 안녕.... | 0.025981096550822258 |
| 안녕. | 0.025981096550822258 |
| 안녕!! | 0.025981096550822258 |
| 안녕.! | 0.025981096550822258 |
+------------+-----------------------------+
<discardPunctuation: false>
SELECT *, BM25_global(custom2,"content:('안녕.')") as score
FROM custom2 order by score desc;
+------------+------------------------------+
| content | score |
+------------+------------------------------+
| 안녕.. | 0.2233678698539734 |
| 안녕. | 0.1957390159368515 |
| 안녕... | 0.16766490042209625 |
| 안녕.! | 0.16766490042209625 |
| 안녕.... | 0.16766490042209625 |
| 안녕.!! | 0.16766490042209625 |
| 안녕! | 0.029243838042020798 |
| 안녕!! | 0.02504950389266014 |
+------------+------------------------------+
SQL
복사
<Token Filters : Korean>
1. korean_part_of_speech
특정 품사(part_of_speech) 태그에 해당하는 토큰을 제거합니다. 형태소 분석 결과에서 필요없는 품사(조사, 감탄사 등)를 걸러내는 데 사용합니다.
CREATE TABLE custom (
content VARCHAR(200),
FULLTEXT USING VERSION 2 (content)
INDEX_OPTIONS '{
"analyzer": {
"custom": {
"tokenizer": {
"korean": {} },
"token_filters": ["korean_part_of_speech"] } } }' );
INSERT INTO custom (content) VALUES
('아! 그 친구는 빨리 떠났지만 그래서 더 아쉽다');
OPTIMIZE TABLE custom FLUSH;
SELECT BM25_GLOBAL(custom,"content:(아)") as A,
BM25_GLOBAL(custom,"content:(빨리)") as B,
BM25_GLOBAL(custom,"content:(떠나다)") as C,
BM25_GLOBAL(custom,"content:(떠났지만)") as D
FROM custom;
+---+---+----------------------+------------------------+
| A | B | C | D |
+---+---+----------------------+------------------------+
| 0 | 0 | 0.130764573812 | 0.130764573812 |
+---+---+----------------------+------------------------+
SQL
복사
예시1
분석에 필요하지 않은 품사(감탄사, 조사, 부사 등)를 토큰화하지 않았음을 확인할 수 있습니다.
하지만 해당 token filter를 사용하지 않은 아래 예시2의 경우 모든 품사를 토큰화합니다.
CREATE TABLE custom2 (
content VARCHAR(200),
FULLTEXT USING VERSION 2 (content)
INDEX_OPTIONS '{
"analyzer": {
"custom": {
"tokenizer": {
"korean": {} } } } }' );
INSERT INTO custom2 (content) VALUES
('아! 그 친구는 빨리 떠났지만 그래서 더 아쉽다');
OPTIMIZE TABLE custom2 FLUSH;
SELECT BM25_GLOBAL(custom2,"content:(아)") as A,
BM25_GLOBAL(custom2,"content:(빨리)") as B,
BM25_GLOBAL(custom2,"content:(떠나다)") as C,
BM25_GLOBAL(custom2,"content:(떠났지만)") as D
FROM custom2;
+-----------------------+-----------------------+----------------------+----------------------+
| A | B | C | D |
+-----------------------+-----------------------+----------------------+----------------------+
| 0.130764573812 | 0.130764573812 | 0.26152914762 | 0.39229372143 |
+-----------------------+-----------------------+----------------------+----------------------+
SQL
복사
예시2
2. korean_reading_form
한자로 작성된 텍스트를 한글로 변환합니다.
CREATE TABLE custom (
content VARCHAR(200),
FULLTEXT USING VERSION 2 (content)
INDEX_OPTIONS '{
"analyzer": {
"custom": {
"tokenizer": {
"korean": {} },
"token_filters": ["korean_reading_form"] } } }' );
INSERT INTO custom (content) VALUES('學生');
OPTIMIZE TABLE custom FLUSH;
SELECT BM25_GLOBAL(custom,"content:(學生)") as A,
BM25_GLOBAL(custom,"content:(학생)") as B FROM custom;
+-----------------------+-----------------------+
| A | B |
+-----------------------+-----------------------+
| 0.130764573812 | 0.130764573812 |
+-----------------------+-----------------------+
DELETE FROM custom;
INSERT INTO custom (content) VALUES('학생');
OPTIMIZE TABLE custom FLUSH;
SELECT BM25_GLOBAL(custom,"content:(學生)") as A,
BM25_GLOBAL(custom,"content:(학생)") as B,
BM25_GLOBAL(custom,"content:(學牲)") as C FROM custom;
+-----------------------+-----------------------+---+
| A | B | C |
+-----------------------+-----------------------+---+
| 0.130764573812 | 0.130764573812 | 0 |
+-----------------------+-----------------------+---+
SQL
복사
예시1
한자와 일치하는 한글텍스트는 한자와 동일한 점수를 갖습니다.
이의 반대도 마찬가지이며, 소리는 같으나 의미가 다른 한자는 점수를 갖지 않습니다.
3. korean_number
한국어 숫자 표현을 아라비아 숫자로 정규화합니다.
CREATE TABLE custom (
content VARCHAR(200),
FULLTEXT USING VERSION 2 (content)
INDEX_OPTIONS '{
"analyzer": {
"custom": {
"tokenizer": {
"korean": {} },
"token_filters": ["korean_number"] } } }' );
INSERT INTO custom (content) VALUES('오만사천삼백이십일');
OPTIMIZE TABLE custom FLUSH;
SELECT BM25_GLOBAL(custom,"content:(54321)") as A,
BM25_GLOBAL(custom,"content:(54,321)") as B,
BM25_GLOBAL(custom,"content:(54,321,000)") as C,
BM25_GLOBAL(custom,"content:(오만사천삼백이십일)") as D
FROM custom;
+-------------------------------+--------------------------------+---+-------------------------------+
| A | B | C | D |
+-------------------------------+--------------------------------+---+-------------------------------+
| 0.13076457381248474 | 0.13076457381248474 | 0 | 0.13076457381248474 |
+-------------------------------+--------------------------------+---+-------------------------------+
SQL
복사
예시1
한글과 일치하는 숫자는 동일한 점수를 가지며, 천단위 구분자(comma)를 포함한 숫자도 역시 동일한 점수를 갖습니다.
DELETE FROM custom;
INSERT INTO custom (content) VALUES('54,321원'); --54321원도 같은 결과
OPTIMIZE TABLE custom FLUSH;
SELECT '54321' as label, BM25_GLOBAL(custom,"content:(54321)") as score FROM custom
UNION ALL SELECT '54321원', BM25_GLOBAL(custom,"content:(54321원)") FROM custom
UNION ALL SELECT '54,321', BM25_GLOBAL(custom,"content:(54,321)") FROM custom
UNION ALL SELECT '54,321원', BM25_GLOBAL(custom,"content:(54,321원)") FROM custom
UNION ALL SELECT '오만사천삼백이십일', BM25_GLOBAL(custom,"content:(오만사천삼백이십일)") FROM custom
UNION ALL SELECT '오만사천삼백이십일원', BM25_GLOBAL(custom,"content:(오만사천삼백이십일원)") FROM custom
UNION ALL SELECT '321', BM25_GLOBAL(custom,"content:(321)") FROM custom
UNION ALL SELECT '321원', BM25_GLOBAL(custom,"content:(321원)") FROM custom
UNION ALL SELECT '삼백이십일', BM25_GLOBAL(custom,"content:(삼백이십일)") FROM custom
UNION ALL SELECT '삼백이십일원', BM25_GLOBAL(custom,"content:(삼백이십일원)") FROM custom
UNION ALL SELECT '54', BM25_GLOBAL(custom,"content:(54)") FROM custom
UNION ALL SELECT '54원', BM25_GLOBAL(custom,"content:(54원)") FROM custom
UNION ALL SELECT '오십사', BM25_GLOBAL(custom,"content:(오십사)") FROM custom
UNION ALL SELECT '오십사원', BM25_GLOBAL(custom,"content:(오십사원)") FROM custom
+--------------------------------+---------------------+
| label | score |
+--------------------------------+---------------------+
| 54321 | 0.13076457381248474 |
| 54321원 | 0.2615291476249695 |
| 54,321 | 0.13076457381248474 |
| 54,321원 | 0.2615291476249695 |
| 오만사천삼백이십일 | 0.13076457381248474 |
| 오만사천삼백이십일원 | 0.2615291476249695 |
| 321 | 0 |
| 321원 | 0.13076457381248474 |
| 삼백이십일 | 0 |
| 삼백이십일원 | 0.13076457381248474 |
| 54 | 0 |
| 54원 | 0.13076457381248474 |
| 오십사 | 0 |
| 오십사원 | 0 |
+--------------------------------+---------------------+
SQL
복사
예시2
반대로 숫자와 일치하는 한글 텍스트 역시 동일한 점수를 가지며, 천단위 구분자(comma)를 포함한 숫자도 역시 동일한 점수를 갖습니다.
해당 필터를 사용하면 built-in 버전과는 다르게 천단위 구분자로 숫자가 나뉘지 않습니다.
References
History
일자 | 작성자 | 비고 |
25.02.25 | won | 최초작성 |
25.03.04 | won | - Tokenizer : Korean - UserDictionary 추가
- 인덱스 리빌딩 없이 userDictionary 수정 사용 기능요청 : PM-3279 |