엘라스틱서치의 _analyze
는 analyzer 분석 결과를 확인할 수 있는 API 이다. explain:true
옵션을 주면 토큰들의 위치 관계를 파악할 수 있는데, 특히 synonym_graph
토큰 필터가 적용된 analyzer 에서는 동의어가 포함된 토큰 관계를 이해하는데 유용하다. explain:true
에 포함된 필드 중 start_offset
, end_offset
, position
, position_length
를 분석해 토큰들 간의 연결 관계를 파악하는 법을 확인보려한다.
이 블로그 글은 ES 공식 문서 초반 부분에 대한 자세한 설명이 될 것이다.
In order to properly handle multi-word synonyms this token filter creates a graph token stream during processing. For more information on this topic and its various complexities, please read the Lucene’s TokenStreams are actually graphs blog post.
먼저 synonym_graph
토큰 필터가 적용된 인덱스를 생성하고 분석 결과를 확인해보자.
3개의 케이스를 확인해볼 것이며 치환
, 동의어
, 치환&동의어
순이다.
<치환>
인덱스 생성
PUT elsboo
{
"settings": {
"analysis": {
"filter": {
"replace": {
"type": "synonym_graph",
"synonyms": [
"뮷즈 => 뮤지컬 굿즈"
]
}
},
"analyzer": {
"elsboo_analyzer": {
"filter": [
"replace"
],
"type": "custom",
"tokenizer": "standard"
}
}
}
}
}
"뮷즈 => 뮤지컬 굿즈"
: 뮷즈
로 검색하면 내부적으로 뮤지컬 굿즈
로 치환되어 검색된다. 반대로 뮤지컬 굿즈
를 검색한다고 해서 뮷즈
로 검색하지 않는다.
분석 결과 확인
Request
GET elsboo/_analyze
{
"text": ["뮷즈"],
"analyzer": "elsboo_analyzer",
"explain": true
}
explain: true
옵션을 추가하면 start_offset
, end_offset
, position
, position_length
를 확인할 수 있다.
Response
{
"detail" : {
...
"tokenfilters" : [
{
"name" : "replace",
"tokens" : [
{
"token" : "뮤지컬",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0,
"bytes" : "[eb ae a4 ec a7 80 ec bb ac]",
"positionLength" : 1,
"termFrequency" : 1
},
{
"token" : "굿즈",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0,
"bytes" : "[ea b5 bf ec a6 88]",
"positionLength" : 1,
"termFrequency" : 1
}
]
}
]
}
}
“tokenfilters”
이외 부분은 필요없으므로 생략하였다.
analyzer 에 의해 쪼개진 토큰들은 서로 연결되어 그래프의 형태로 나타낼 수 있다. 4개 필드가 그 단서가 된다. 일단 보기 좋게 표로 먼저 정리해보자
token | start_offset | end_offset | position | positionLength |
뮤지컬 | 0 | 2 | 0 | 1 |
굿즈 | 0 | 2 | 0 | 1 |
start_offset
, end_offset
은 검색어 뮷즈
에서의 글자 기준의 위치를 나타낸다. 뮤지컬
, 굿즈
는 모두 검색어 뮷즈
를 가리키므로 end_offset
은 2 글자를 의미하는 2이고, start_offset
이 0 으로 검색어의 처음을 의미한다.
position
, positionLength
로 그래프를 그리면 다음과 같다.
포지션은 node, 토큰은 선(arc) 으로 표현한다. 이렇게 토큰이라는 선을 통해 노드를 거치면서 토큰 간의 그래프를 만들 수 있다.
position
: 토큰의 시작 포지션 위치positionLength
: 시작 포지션으로부터 건넌 포지션 개수
즉, postion
: 0, positionLength
:1 의 의미는 뮤지컬
, 굿즈
둘 다 처음인 0 번 포지션에서 시작해서 1개를 포지션을 건넜단 뜻이다.
<동의어>
인덱스 생성
PUT elsbooe
{
"settings": {
"analysis": {
"filter": {
"synonym": {
"type": "synonym_graph",
"synonyms": [
"뮷즈, 뮤지컬 굿즈"
]
}
},
"analyzer": {
"elsboo_analyzer": {
"filter": [
"synonym"
],
"type": "custom",
"tokenizer": "standard"
}
}
}
}
}
"뮷즈, 뮤지컬 굿즈"
: 뮷즈
로 검색하면 뮤지컬 굿즈
로도 검색한다. 반대로 뮤지컬 굿즈
로 검색해도 뮷즈
로도 검색한다.
분석 결과 확인
Request
GET elsbooe/_analyze
{
"text": ["뮤지컬 굿즈"],
"analyzer": "elsboo_analyzer",
"explain": true
}
Response
{
"detail" : {
...
"tokenfilters" : [
{
"name" : "synonym",
"tokens" : [
{
"token" : "뮷즈",
"start_offset" : 0,
"end_offset" : 6,
"type" : "SYNONYM",
"position" : 0,
"positionLength" : 2,
"bytes" : "[eb ae b7 ec a6 88]",
"positionLength" : 2,
"termFrequency" : 1
},
{
"token" : "뮤지컬",
"start_offset" : 0,
"end_offset" : 3,
"type" : "<HANGUL>",
"position" : 0,
"bytes" : "[eb ae a4 ec a7 80 ec bb ac]",
"positionLength" : 1,
"termFrequency" : 1
},
{
"token" : "굿즈",
"start_offset" : 4,
"end_offset" : 6,
"type" : "<HANGUL>",
"position" : 1,
"bytes" : "[ea b5 bf ec a6 88]",
"positionLength" : 1,
"termFrequency" : 1
}
]
}
]
}
}
token | start_offset | end_offset | position | positionLength |
뮷즈 | 0 | 6 | 0 | 2 |
뮤지컬 | 0 | 3 | 0 | 1 |
굿즈 | 4 | 6 | 1 | 1 |
검색어는 뮤지컬 굿즈
이므로 공백을 포함하여 총 6 글자 이다.
뮷즈
는 검색어 전체를 통괄하므로 start_offset
이 0, end_offset
이 6이다.
뮤지컬
, 굿즈
는 standard tokenizer 에 의해 공백 기준으로 쪼개져서 둘의 끝, 시작 offset 이 연결된다.
position
은 시작 포지션의 값이므로
뮷즈
는 0 번에서 시작해positionLength
값 2 만큼 건너갔다.뮤지컬
은 0 번에서 시작해positionLength
값 1 만큼 건너갔다.굿즈
는 1 번에서 시작해positionLength
값 1 만큼 건너갔다.
<치환 & 동의어>
마지막으로 치환과 동의어를 동시에 적용해보자
인덱스 생성
PUT elsbooel
{
"settings": {
"analysis": {
"filter": {
"replace": {
"type": "synonym_graph",
"synonyms": [
"캘박 => 캘린더 박제"
]
},
"synonym": {
"type": "synonym_graph",
"synonyms": [
"캘린더, 달력, 켈린더"
]
}
},
"analyzer": {
"elsboo_analyzer": {
"filter": [
"replace",
"synonym"
],
"type": "custom",
"tokenizer": "standard"
}
}
}
}
}
분석 결과 확인
Request
GET elsbooel/_analyze
{
"text": ["캘박"],
"analyzer": "elsboo_analyzer",
"explain": true
}
Response
{
"detail" : {
...
"tokenfilters" : [
{
"name" : "replace",
"tokens" : [
{
"token" : "캘린더",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0,
"bytes" : "[ec ba 98 eb a6 b0 eb 8d 94]",
"positionLength" : 1,
"termFrequency" : 1
},
{
"token" : "박제",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 1,
"bytes" : "[eb b0 95 ec a0 9c]",
"positionLength" : 1,
"termFrequency" : 1
}
]
},
{
"name" : "synonym",
"tokens" : [
{
"token" : "달력",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0,
"bytes" : "[eb 8b ac eb a0 a5]",
"positionLength" : 1,
"termFrequency" : 1
},
{
"token" : "켈린더",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0,
"bytes" : "[ec bc 88 eb a6 b0 eb 8d 94]",
"positionLength" : 1,
"termFrequency" : 1
},
{
"token" : "캘린더",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 0,
"bytes" : "[ec ba 98 eb a6 b0 eb 8d 94]",
"positionLength" : 1,
"termFrequency" : 1
},
{
"token" : "박제",
"start_offset" : 0,
"end_offset" : 2,
"type" : "SYNONYM",
"position" : 1,
"bytes" : "[eb b0 95 ec a0 9c]",
"positionLength" : 1,
"termFrequency" : 1
}
]
}
]
}
}
토큰 필터는 순서대로 수행되므로 마지막 토큰 필터인 synonym
의 결과가 최종 결과이다.
token | start_offset | end_offset | position | positionLength |
달력 | 0 | 2 | 0 | 1 |
켈린더 | 0 | 2 | 0 | 1 |
캘린더 | 0 | 2 | 0 | 1 |
박제 | 0 | 2 | 1 | 1 |
달력
은 0 번에서 시작해positionLength
값 1 만큼 건너갔다.켈린더
은 0 번에서 시작해positionLength
값 1 만큼 건너갔다.캘린더
은 0 번에서 시작해positionLength
값 1 만큼 건너갔다.박제
는 1 번에서 시작해positionLength
값 1 만큼 건너갔다.
레퍼런스
https://blog.mikemccandless.com/2012/04/lucenes-tokenstreams-are-actually.html
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/analysis-synonym-graph-tokenfilter.html
https://www.elastic.co/guide/en/elasticsearch/reference/7.17/token-graphs.html
글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.
'SearchDeveloper > ElasticSearch' 카테고리의 다른 글
[Elasticsearch] action.auto_create_index 를 설정하자 (5) | 2024.11.22 |
---|---|
[ElasticSearch] update_by_query version_conflict_engine_exception & update 쿼리 비교 (1) | 2024.07.14 |
노리 형태소 분석기 이해하기 (2/2) (2) | 2023.04.11 |
노리 형태소 분석기 이해하기 (1/2) (0) | 2023.04.01 |
[트러블슈팅] Too many dynamic script compilations within, max: [75/5m] (0) | 2022.11.16 |