본문 바로가기

SearchDeveloper/ElasticSearch

[ElasticSearch] synonym_graph 의 start_offset, end_offset, position, position_length 이해하기

엘라스틱서치의 _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

 

글 읽어주셔서 언제나 감사합니다. 좋은 피드백, 개선 피드백 너무나도 환영합니다.