엔지니어링

Elasticsearch의 언어 식별 기능을 사용하여 다국어 검색

Elasticsearch7.6에서 머신 러닝 추론 수집 프로세서와 더불어 언어 식별 기능을 릴리즈한다는 기쁜 소식을 알려드립니다. 이번 릴리즈로, 다국어 코퍼스 검색을 위한 몇 가지 사용 사례와 전략을 설명하고 언어 식별이 여기에서 어떤 역할을 하는지 살펴보고자 합니다. 이러한 주제 중 일부는 이전에 다루었으며 오늘은 이를 기반으로 다음과 같은 몇 가지 예제를 설명하겠습니다.

동기

오늘날과 같이 상호연결성이 높은 세계에서는 문서와 기타 정보가 다양한 언어로 제공됩니다. 이는 많은 검색 애플리케이션에서 문제가 됩니다. 이러한 문서들을 적절히 분석하고 가능한 최상의 검색 경험을 제공하려면 각 문서의 언어를 이해할 필요가 있습니다. 즉, 언어 식별이 필요한 것이죠.

언어 식별은 이러한 다국어 코퍼스에서 전반적인 검색 정확도를 개선하는 데 사용됩니다. 어떤 언어가 사용되었는지 아직 모르는 일련의 문서가 있더라도 해당 문서를 효율적으로 검색할 수 있어야 합니다. 문서에는 하나 또는 여러 언어가 포함되어 있을 수 있습니다. 전자는 영어가 의사소통의 주요 언어인 컴퓨터 과학과 같은 분야에서 일반적이며, 후자는 라틴어 용어가 영어와 빈번하게 섞여 있는 생물학 및 임상 시험에서 흔히 볼 수 있습니다.

언어별 분석을 적용하면 문서의 용어를 적절하게 이해, 색인 및 검색함으로써 정확도(정밀도와 재현율 모두)를 높일 수 있습니다. Elasticsearch의 언어별 분석기 제품군을 사용하면(기본 제공 분석기 및 추가 플러그인을 통해) 다음과 같은 향상된 토큰화, 토큰 필터링 및 용어 필터링을 제공할 수 있습니다.

유사한 이유로, 더 일반적인 자연어 처리(NLP) 파이프라인에 언어 식별을 적용하는 것을 매우 정밀한 언어별 알고리즘 및 모델을 사용하기 위한 첫 번째 처리 단계 중 하나로 보고 있습니다. 예를 들어 Google의 BERTALBERT 또는 OpenAI의 GPT-2와 같은 사전 훈련된 NLP 모델은 일반적으로 언어별 코퍼스 또는 주요 언어 코퍼스를 사용해 훈련되며 문서 분류, 감정 분석, 명명된 엔터티 인식(NER) 등과 같은 작업에 적합하게 미세 조정됩니다.

다음 예제와 전략의 경우, 달리 명시되지 않는 한 문서에 하나의 언어 또는 주요 언어가 포함되어 있다고 가정하겠습니다.

언어별 분석의 이점

계속해서 동기를 부여하기 위해 언어별 분석기의 몇 가지 이점을 간단히 살펴보겠습니다.

분해: 독일어에서 명사는 종종 다른 명사와 합성되어 아름답지만 길고 읽기 어려운 복합어를 만들어 냅니다. 간단한 예로 “Jahr”(“연도”)는 “Jahrhunderts”(“세기”), “Jahreskalender”(“연간 달력”) 또는 “Schuljahr”(“학년”)와 같이 다른 단어와 결합될 수 있습니다. 이러한 단어들을 분해할 수 있는 사용자 정의 분석기가 없다면 “jahr”를 검색할 때 “Schuljahr”(학년)에 대한 문서를 찾을 수 없을 것입니다. 또한 독일어는 복수형 및 여격 형식에서 다른 라틴어와는 다른 규칙을 가지고 있습니다. 즉, “jahr”를 검색하면 “Jahre”(복수형) 및 “Jahren”(복수형 여격)도 찾을 수 있어야 한다는 의미입니다.

공통 용어: 일부 언어에서는 공통 용어 또는 분야별 용어를 사용합니다. 예를 들어 “computer”는 다른 언어에서도 있는 그대로 사용될 때가 많은 단어입니다. “computer”를 검색할 때 영어 이외의 언어로 작성된 문서에도 관심이 있을 수 있습니다. 알려진 일련의 언어 전체에서 검색하면서 여전히 공통 용어를 찾을 수 있는 기능은 흥미로운 사용 사례가 될 수 있습니다. 다시 독일어를 예로 들어보겠습니다. 여러 언어로 된 컴퓨터 보안 관련 문서가 있을 수 있습니다. 독일어로는 “Computersicherheit”(“sicherheit”가 “보안” 또는 “안전”을 의미함)라고 하며 독일어 분석기를 사용하는 경우에만 영어와 독일어에서 “computer”와 일치하는 항목을 검색할 수 있습니다.

비 라틴어 스크립트: 표준 분석기는 대부분의 라틴어 스크립트 언어(서유럽 언어)에서 상당히 잘 작동합니다. 그러나 키릴 문자 또는 CJK(중국어/일본어/한국어)와 같은 비 라틴어 스크립트에서는 효과가 없습니다. 이전 블로그 시리즈에서는 CJK 언어가 어떻게 형성되는지와 언어별 분석기의 필요성에 대해 살펴보았습니다. 예를 들어 한국어에는 명사 및 대명사에 추가되어 의미를 바꾸는 접미사인 조사가 있습니다. 때로는 표준 분석기로도 검색어와 일치하는 항목을 찾을 수 있지만 찾은 항목에 점수를 매기는 데는 효과적이지 않습니다. 즉, 문서의 재현율은 높을 수 있으나 정밀도가 떨어집니다. 다른 경우에는 표준 분석기가 검색어와 일치하는 항목을 찾지 못하며 정밀도와 재현율이 모두 떨어집니다.

“Winter Olympics”의 작업 예를 살펴보겠습니다. 한국어로 “동계올림픽대회는”은 “winter season”을 의미하는 “동계”, “Olympics” 또는 “Olympic competition”을 의미하는 “올림픽대회”, 그리고 주격 조사(주제를 나타내는 단어에 추가되는 접미사)인 “는”으로 구성됩니다. 표준 분석기로 정확하게 이 문구를 검색하면 완벽하게 일치하는 항목을 찾을 수 있지만, “Olympics”를 의미하는 “올림픽대회”를 검색하면 아무런 결과가 반환되지 않습니다. 하지만 nori 한국어 분석기를 사용하면 일치하는 항목을 찾을 수 있습니다. “동계올림픽대회는”/”Winter Oplypics”가 색인 시 적절하게 토큰화되었기 때문입니다.

언어 식별 시작하기

데모 프로젝트

검색에서 언어 식별의 사용 사례와 전략을 설명하는 데 도움이 되도록 작은 데모 프로젝트를 구성했습니다. 여기에는 이 블로그 게시물의 모든 예제와 더불어 WiLI-2018을 색인화하고 검색할 수 있는 일부 도구가 포함되어 있습니다. WiLI-2018은 다국어 검색 실험을 위한 참조 및 작업 예제로 사용할 수 있는 다국어 코퍼스입니다. 다음 예제에서는 따라 해 보길 원할 경우 문서를 색인화하고 데모 프로젝트를 실행하는 것이 유용합니다(반드시 필요한 것은 아님).

다음 실험을 따라 하려면 Elasticsearch 7.6을 로컬로 설치하거나 Elasticsearch Service의 무료 체험판을 시작하면 됩니다.

첫 번째 실험

언어 식별은 Elasticsearch의 기본 배포로 제공되는 사전 훈련된 모델입니다. 수집 파이프라인에서 추론 프로세서를 설정할 때 model_idlang_ident_model_1을 지정하면 추론 수집 프로세서와 함께 사용할 수 있습니다.

{ 
  "inference": { 
    "model_id": "lang_ident_model_1", 
    "inference_config": {}, 
    "field_mappings": {} 
  } 
}

나머지 구성은 다른 모델과 동일하므로 출력할 최상위 클래스 수, 예측을 포함할 출력 필드, 우리 사용 사례에서 가장 중요한 사용할 입력 필드와 같은 설정을 지정할 수 있습니다. 기본적으로 이 모델은 text라는 필드에 입력이 포함될 것으로 예상합니다. 다음 예제에서는 일부 단일 필드 문서에 파이프라인의 _simulate API를 사용합니다. 추론을 위해 입력 콘텐츠 필드를 텍스트 필드에 매핑합니다. 이 매핑은 파이프라인의 다른 프로세서에는 영향을 주지 않습니다. 그런 다음 검사를 위해 상위 3개 클래스를 출력합니다.

# 기본 추론 설정 시뮬레이션

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

출력은 각 문서와 더불어 _ml.lang_ident 필드의 일부 추가 정보를 보여줍니다. 여기에는 _ml.lang_ident.predicted_value에 저장된 상위 3개 언어 및 최상위 언어에 대한 확률이 각각 포함되어 있습니다.

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "Das Leben ist kein Ponyhof",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "predicted_value" : "de",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810179Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "The rain in Spain stays mainly in the plains",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810185Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "predicted_value" : "en",
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:38:13.810189Z"
        }
      }
    }
  ]
}

좋습니다! 첫 번째 문서에서는 독일어가, 두 번째 문서에서는 영어가, 세 번째 문서에는 라틴어가 약간 섞여 있었는데도 불구하고 영어가 식별되었습니다.

검색에서 언어 식별의 전략

언어 식별의 기본 예를 살펴보았으니 이제 색인 및 검색 전략에 언어 식별을 적용할 차례입니다.

사용할 기본적인 색인 전략 두 가지는 언어별 필드와 언어별 인덱스입니다. 언어별 필드 전략에서는 언어별 필드 세트로 단일 인덱스를 생성하고 각 언어에 맞는 분석기를 사용합니다. 검색 시 알려진 언어 필드를 검색하거나 모든 언어 필드를 검색한 다음 가장 일치하는 필드를 선택할 수 있습니다. 언어별 인덱스 전략에서는 각기 다른 매핑으로 언어별 인덱스 세트를 생성하여 색인된 필드에 해당 언어용 분석기가 적용되도록 합니다. 검색 시 언어별 필드와 유사한 접근 방식을 취하여 단일 언어 인덱스를 검색하거나 검색 요청의 인덱스 패턴에 따라 여러 인섹스를 검색하도록 선택할 수 있습니다.

오늘 수행할 작업, 즉 동일한 스트링을 언어별 분석기를 사용하여 필드 또는 인덱스에 각각 색인하는 작업을 통해 이 두 가지 전략을 비교해 보겠습니다. 이 접근 방식이 효과가 있지만 중복이 너무 많이 발생하여 쿼리 속도가 느려지고 필요 이상으로 저장 공간을 많이 사용하게 됩니다.

색인

색인 전략에 따라 사용할 수 있는 검색 전략이 결정되므로 이 두 가지 색인 전략을 각각 살펴보겠습니다.

언어별 필드

언어별 필드 전략에서는 수집 파이프라인에 언어 식별의 출력과 일련의 프로세서를 사용하여 입력 필드를 언어별 필드에 저장합니다. 언어별로 특정 분석기를 설정해야 하므로 한정된 언어 세트(독일어, 영어, 한국어, 일본어 및 중국어)만 지원합니다. 지원되는 언어에 속하지 않는 문서는 표준 분석기를 통해 기본 필드에서 색인됩니다.

전체 파이프라인 정의는 데모 프로젝트 config/pipelines/lang-per-field.json에서 확인할 수 있습니다.

이 색인 전략을 지원하기 위한 매핑은 다음과 같습니다.

{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "supported": {
            "type": "boolean"
          },
          "default": {
            "type": "text",
            "analyzer": "default",
            "fields": {
              "icu": {
                "type": "text",
                "analyzer": "icu_analyzer"
              }
            }
          },
          "en": {
            "type": "text",
            "analyzer": "english"
          },
          "de": {
            "type": "text",
            "analyzer": "german_custom"
          },
          "ja": {
            "type": "text",
            "analyzer": "kuromoji"
          },
          "ko": {
            "type": "text",
            "analyzer": "nori"
          },
          "zh": {
            "type": "text",
            "analyzer": "smartcn"
          }
        }
      }
    }
  }
}

(간략하게 하기 위해 독일어 분석기 구성은 위의 예에서 생략되었으며 config/mappings/de_analyzer.json에서 확인할 수 있음)

이전 예제와 마찬가지로 파이프라인의 _simulate API를 사용하여 다음을 살펴보겠습니다.

# 언어별 필드 전략을 시뮬레이션하고 검사를 위해 상위 3개 언어 클래스를 출력

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      },
      {
        "rename": {
          "field": "contents",
          "target_field": "contents.default"
        }
      },
      {
        "rename": {
          "field": "_ml.lang_ident.predicted_value",
          "target_field": "contents.language"
        }
      },
      {
        "script": {
          "lang": "painless",
          "source": "ctx.contents.supported = (['de', 'en', 'ja', 'ko', 'zh'].contains(ctx.contents.language))"
        }
      },
      {
        "set": {
          "if": "ctx.contents.supported",
          "field": "contents.{{contents.language}}",
          "value": "{{contents.default}}",
          "override": false
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "オリンピック大会"
      }
    },
    {
      "_source": {
        "contents": "로마는 하루아침에 이루어진 것이 아니다"
      }
    },
    {
      "_source": {
        "contents": "授人以鱼不如授人以渔"
      }
    },
    {
      "_source": {
        "contents": "Qui court deux lievres a la fois, n’en prend aucun"
      }
    },
    {
      "_source": {
        "contents": "Lupus non timet canem latrantem"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

다음은 언어별 필드의 출력입니다.

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "de" : "Das Leben ist kein Ponyhof",
            "default" : "Das Leben ist kein Ponyhof",
            "language" : "de",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218641Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" : "The rain in Spain stays mainly in the plains",
            "default" : "The rain in Spain stays mainly in the plains",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218646Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "オリンピック大会",
            "language" : "ja",
            "ja" : "オリンピック大会",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" : 0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" : 2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" : 1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218648Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "로마는 하루아침에 이루어진 것이 아니다",
            "language" : "ko",
            "ko" : "로마는 하루아침에 이루어진 것이 아니다",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" : 0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" : 1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218649Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "授人以鱼不如授人以渔",
            "language" : "zh",
            "zh" : "授人以鱼不如授人以渔",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" : 0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.21865Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "Qui court deux lievres a la fois, n’en prend aucun",
            "language" : "fr",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" : 0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" : 3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218652Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "default" : "Lupus non timet canem latrantem",
            "language" : "la",
            "supported" : false
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" : 0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" : 0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" : 0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218653Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "en" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "default" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem",
            "language" : "en",
            "supported" : true
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-22T12:40:03.218654Z"
        }
      }
    }
  ]
}

예상대로 독일어 필드는 contents.de에, 영어는 contents.en에, 한국어는 contents.ko에 저장되었습니다. 지원되지 않는 프랑스어와 라틴어도 섞여 있는 것을 볼 수 있는데요. 프랑스어와 라틴어는 지원되는 플래그가 제공되지 않고 기본 필드에서만 검색이 가능한 것을 확인할 수 있습니다. 라틴어 예제에서 상위 예측 클래스도 살펴보겠습니다. 모델이 라틴어라고 생각하지만(라틴어가 맞습니다) 확신하지 못하고 2순위로 프랑스어를 예측하고 있습니다.

이는 언어 식별이 적용된 수집 파이프라인의 기본 예에 불과하지만 언어 식별을 활용하여 어떤 작업이 가능한지에 대한 아이디어를 제공합니다. 수집 파이프라인의 유연성 덕분에 수많은 다양한 시나리오를 구현할 수 있습니다. 게시물 마지막 부분에서 몇 가지 대안을 알아보도록 하겠습니다. 이 예제의 일부 단계는 프로덕션 파이프라인에서 결합되거나 생략될 수 있지만, 좋은 데이터 처리 파이프라인은 줄 수가 적은 것이 아니라 읽고 이해하기 쉬운 파이프라인이라는 점을 기억하시기 바랍니다.

언어별 인덱스

언어별 인덱스 전략에서는 언어별 필드의 파이프라인과 동일한 기본 구성 요소를 사용합니다. 주요 차이점은 언어별 필드에 저장하는 대신 다른 인덱스를 사용한다는 것입니다. 이는 수집 시 문서의 _index 필드를 설정함으로써 기본값을 재정의하여 언어별 인덱스 이름으로 설정할 수 있기 때문에 가능합니다. 지원하지 않는 언어의 경우 해당 단계를 건너뛰고 문서가 기본 인덱스로 색인됩니다. 정말 간단하죠!

전체 파이프라인 정의는 데모 프로젝트 config/pipelines/lang-per-index.json에서 확인할 수 있습니다.

이 색인 전략을 지원하기 위한 매핑은 다음과 같습니다.

{
  "settings": {
    "index": {
      "number_of_shards": 1,
      "number_of_replicas": 0
    }
  },
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "contents": {
        "properties": {
          "language": {
            "type": "keyword"
          },
          "text": {
            "type": "text",
            "analyzer": "default"
          }
        }
      }
    }
  }
}

이 매핑에서는 사용자 정의 분석기를 지정하지 않고 대신 이 파일을 템플릿으로 사용합니다. 각 언어별 인덱스를 생성할 때 해당 언어에 대한 분석기를 설정합니다.

이 파이프라인 시뮬레이션:

# 언어별 인덱스 전략을 시뮬레이션하고 검사를 위해 상위 3개 언어 클래스를 출력

POST _ingest/pipeline/_simulate
{
  "pipeline": {
    "processors": [
      {
        "inference": {
          "model_id": "lang_ident_model_1",
          "inference_config": {
            "classification": {
              "num_top_classes": 3
            }
          },
          "field_mappings": {
            "contents": "text"
          },
          "target_field": "_ml.lang_ident"
        }
      },
      {
        "rename": {
          "field": "contents",
          "target_field": "contents.text"
        }
      },
      {
        "rename": {
          "field": "_ml.lang_ident.predicted_value",
          "target_field": "contents.language"
        }
      },
      {
        "set": {
          "if": "['de', 'en', 'ja', 'ko', 'zh'].contains(ctx.contents.language)",
          "field": "_index",
          "value": "{{_index}}_{{contents.language}}",
          "override": true
        }
      }
    ]
  },
  "docs": [
    {
      "_source": {
        "contents": "Das Leben ist kein Ponyhof"
      }
    },
    {
      "_source": {
        "contents": "The rain in Spain stays mainly in the plains"
      }
    },
    {
      "_source": {
        "contents": "オリンピック大会"
      }
    },
    {
      "_source": {
        "contents": "로마는 하루아침에 이루어진 것이 아니다"
      }
    },
    {
      "_source": {
        "contents": "授人以鱼不如授人以渔"
      }
    },
    {
      "_source": {
        "contents": "Qui court deux lievres a la fois, n’en prend aucun"
      }
    },
    {
      "_source": {
        "contents": "Lupus non timet canem latrantem"
      }
    },
    {
      "_source": {
        "contents": "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
      }
    }
  ]
}

다음은 언어별 인덱스의 출력입니다.

{
  "docs" : [
    {
      "doc" : {
        "_index" : "_index_de",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "de",
            "text" : "Das Leben ist kein Ponyhof"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "de",
                  "class_probability" : 0.9996006023972855
                },
                {
                  "class_name" : "el-Latn",
                  "class_probability" : 2.625873919853074E-4
                },
                {
                  "class_name" : "ru-Latn",
                  "class_probability" : 1.130237050226503E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486009Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" : "The rain in Spain stays mainly in the plains"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9988809847231199
                },
                {
                  "class_name" : "ga",
                  "class_probability" : 7.764148026288316E-4
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 7.968926766495827E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486037Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ja",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ja",
            "text" : "オリンピック大会"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ja",
                  "class_probability" : 0.9993823252841599
                },
                {
                  "class_name" : "el",
                  "class_probability" : 2.6448654791599055E-4
                },
                {
                  "class_name" : "sd",
                  "class_probability" : 1.4846805271384584E-4
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486039Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_ko",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "ko",
            "text" : "로마는 하루아침에 이루어진 것이 아니다"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "ko",
                  "class_probability" : 0.9999939196272863
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 3.0431805047662344E-6
                },
                {
                  "class_name" : "am",
                  "class_probability" : 1.710514725818281E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486041Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_zh",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "zh",
            "text" : "授人以鱼不如授人以渔"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "zh",
                  "class_probability" : 0.9999810103320087
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 1.0390454083183788E-5
                },
                {
                  "class_name" : "ka",
                  "class_probability" : 2.6302271562335787E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486043Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "fr",
            "text" : "Qui court deux lievres a la fois, n’en prend aucun"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "fr",
                  "class_probability" : 0.9999669852240882
                },
                {
                  "class_name" : "gd",
                  "class_probability" : 2.3485226102079597E-5
                },
                {
                  "class_name" : "ht",
                  "class_probability" : 3.536708810360631E-6
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486044Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "la",
            "text" : "Lupus non timet canem latrantem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "la",
                  "class_probability" : 0.614050940088811
                },
                {
                  "class_name" : "fr",
                  "class_probability" : 0.32530021315840363
                },
                {
                  "class_name" : "sq",
                  "class_probability" : 0.03353817054854559
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.486046Z"
        }
      }
    },
    {
      "doc" : {
        "_index" : "_index_en",
        "_type" : "_doc",
        "_id" : "_id",
        "_source" : {
          "contents" : {
            "language" : "en",
            "text" : "This is mostly English but has a touch of Latin since we often just say, Carpe diem"
          },
          "_ml" : {
            "lang_ident" : {
              "top_classes" : [
                {
                  "class_name" : "en",
                  "class_probability" : 0.9997901768317939
                },
                {
                  "class_name" : "ja",
                  "class_probability" : 8.756250766054857E-5
                },
                {
                  "class_name" : "fil",
                  "class_probability" : 1.6980752372837307E-5
                }
              ],
              "model_id" : "lang_ident_model_1"
            }
          }
        },
        "_ingest" : {
          "timestamp" : "2020-01-21T14:41:48.48605Z"
        }
      }
    }
  ]
}

예상하시는 대로 언어 식별 결과는 언어별 인덱스 전략과 동일하며 유일한 차이점은 파이프라인에서 해당 정보를 사용하여 문서를 올바른 인덱스로 라우팅하는 방식입니다.

검색

두 가지 색인 전략을 고려할 때 검색에 가장 좋은 방법은 무엇일까요? 위에서 언급한 바와 같이 각 색인 전략에는 몇 가지 옵션이 있습니다. 공통된 한 가지 질문은 색인된 필드와 일치하도록 쿼리 스트링에 언어별 분석기를 지정하려면 어떻게 해야 하는가입니다. 걱정하실 필요는 없습니다. 검색 시 특정 분석기를 지정할 필요는 없으니까요. 쿼리 DSL에 search_analyzer를 지정하지 않는 한 쿼리 스트링은 일치하는 필드와 동일한 분석기로 분석됩니다. 언어별 필드 예제에서와같이 en 및 de 필드가 있는 경우 쿼리 스트링이 en 필드와 일치하는 경우 english 분석기로 분석되고 de 필드와 일치하는 경우 german_custom 분석기로 분석됩니다.

쿼리 언어

검색 전략을 살펴보기 전에 먼저 사용자의 쿼리 스트링 자체에 언어 식별에 대한 컨텍스트를 설정하는 것이 중요합니다. 이제 색인된 문서의 (주요) 언어를 알았으니 쿼리 스트링에서 언어 식별을 수행하고 해당 필드 또는 인덱스에서 일반 검색을 수행하면 되리라 생각할 수 있습니다. 안타깝게도 검색 쿼리는 짧은 편입니다. 그냥 짧은 것이 아니라 정말 짧습니다! 2001년으로 거슬러 올라가 오래된 Excite 웹 검색 엔진에 대한 연구[1]에 따르면 평균 사용자 쿼리에 단 2.4개의 용어가 사용되는 것으로 나타났습니다. 꽤 오래전 일이었고 대화형 쿼리 및 자연어 쿼리(예: “Elasticsearch를 사용하여 다국어 코퍼스를 검색하려면 어떻게 해야 하나요”)의 등장으로 인해 상황이 많이 바뀌었지만, 언어 식별에 사용하기에는 검색 쿼리가 여전히 너무 짧은 경향이 있습니다. 대부분의 언어 식별 알고리즘은 50자 이상에서 가장 잘 작동합니다[2]. 이 문제에 더해서 검색 쿼리가 “Justin Trudeau”, “푸파이터스” 또는 “족저근막염”과 같은 고유 명사, 개체명 또는 학명일 때도 많습니다. 사용자는 임의의 언어로 된 문서를 원할 수 있지만 이러한 종류의 쿼리 스트링을 분석하는 것만으로는 이를 알 수 없습니다.

따라서 어떤 종류든 쿼리 스트링 단독으로 언어 식별을 사용하는 것은 좋지 않습니다. 사용자의 쿼리 언어를 사용하여 검색 필드 또는 인덱스를 선택하려면 사용자에 대한 암시적 또는 명시적 정보를 사용하는 다른 접근 방식을 고려하는 것이 좋습니다. 예를 들어 암시적 컨텍스트는 웹 사이트 도메인(예: .com 또는 .de) 또는 앱이 다운로드된 앱 스토어 로캘(예: 미국 스토어 또는 독일 스토어)을 사용할 수 있습니다. 그러나 대부분의 경우 사용자에게 물어보는 것이 가장 좋습니다! 많은 사이트에서 새로운 사용자가 처음 사이트를 방문할 때 로캘을 선택하도록 합니다. 또한 사용자가 원하는 언어를 알려주는 데 도움이 되도록 문서 언어에 용어 집계를 통해 패시팅을 사용하는 것도 고려할 수도 있습니다.

언어별 필드

언어별 필드 전략의 경우 언어 하위 필드가 여러 개 있으므로 모든 필드를 동시에 모두 검색하고 최고 점수의 필드를 선택해야 합니다. 색인 파이프라인에서는 단일 언어 필드만 설정하므로 이 작업은 비교적 간단합니다. 따라서 여러 개의 필드를 검색하는 동안 실제로는 하나의 필드만 채워지게 됩니다. 이를 위해 유형을 best_fields(기본값)로 하여 multi_match 쿼리를 사용합니다. 이 조합은 dis_max 쿼리로 실행되며, 여러 필드 전체가 아니라 단일 필드와 일치하는 모든 용어에 관심이 있으므로 이 조합을 사용합니다.

GET lang-per-field/_search 
{ 
  "query": { 
    "multi_match": { 
      "query": "jahr", 
      "type": "best_fields", 
      "fields": [ 
        "contents.de", 
        "contents.en", 
        "contents.ja", 
        "contents.ko", 
        "contents.zh" 
      ] 
    } 
  } 
}

모든 언어를 검색하려면 multi_match 쿼리에 contents.default 필드를 추가할 수도 있습니다. 언어별 필드 전략의 한 가지 이점은 식별된 언어를 사용하여 위에서 설명한 대로 사용자의 언어 또는 로캘과 일치하는 문서를 승격시킬 수 있다는 것입니다. 이를 통해 정확도에 직접적인 영향을 미치는 정밀도와 재현율을 모두 개선할 수 있습니다. 마찬가지로, 사용자의 쿼리 언어를 알고 있을 때처럼 단일 언어를 검색하려는 경우 해당 언어의 언어 필드에 match 쿼리를 사용하면 됩니다(예: contents.de).

언어별 인덱스

언어별 인덱스 전략의 경우 언어 인덱스가 여러 개이지만 각 인덱스에는 동일한 필드 이름이 있습니다. 즉, 간단한 단일 쿼리를 사용할 수 있으며 검색을 요청할 때 인덱스 패턴만 지정하면 됩니다.

GET lang-per-index_*/_search 
{ 
  "query": { 
    "match": { 
      "contents.text": "jahr" 
    } 
  } 
}

모든 언어를 검색하려는 경우 기존 인덱스인 lang-per-index*(밑줄 표시 없음)와 일치하는 인덱스 패턴을 사용하면 되고, 단일 언어를 검색하려는 경우 해당 언어의 인덱스(예: lang-per-index_de)를 사용하면 됩니다.

예제

‘동기’ 섹션에서 설명한 것과 동일한 예제를 사용하여 WiLI-2018 코퍼스를 검색해 볼 수 있습니다. 데모 프로젝트에서 다음 명령을 실행한 후 어떤 일이 일어나는지 확인해 보세요.

분해:

# “jahr”라는 단어와 정확하게 일치하는 항목만 검색 
bin/search --strategy default jahr
# "jahr", "jahre", "jahren", "jahrhunderts" 등과 일치하는 항목도 검색 
bin/search --strategy per-field jahr

공통 용어:

# “computer”라는 단어와 정확하게 일치하는 항목만 검색, 여러 언어가 결과에 나타남 
bin/search --strategy default computer
# "Computersicherheit"(컴퓨터 보안)와 같은 복합 독일어 단어와 일치하는 항목도 검색: 
bin/search --strategy per-field computer

비 라틴어 스크립트:

# 표준 분석기는 “네트워크”/”인터넷”: "网络"와 같이 정밀도가 낮으며 정확하지 않고/일치하지 않는 결과를 반환 
bin/search --strategy default 网络
# ICU 및 언어별 분석은 올바른 결과를 반환하지만 점수가 다름 
bin/search --strategy icu 网络 
bin/search --strategy per-field 网络

비교

두 가지 전략 중 어떤 전략을 사용해야 할까요? 상황에 따라 다릅니다. 결정을 내리는 데 도움이 되도록 각 접근 방식의 장단점을 다음과 같이 정리해 보았습니다.

장점단점
언어별 필드
  • 단일 인덱스 관리 용이
  • 문서당 여러 언어 지원
  • 문서에 여러 언어가 포함된 경우에도 단일 정보 저장소(SSOT)
  • 해당 언어의 문서 승격 허용
  • 더 복잡한 매핑 및 쿼리
  • 지원되는 언어 및 필드 수가 증가함에 따라 성능 저하
언어별 인덱스
  • 간단한 쿼리
  • 각 인덱스에서 쿼리가 실행되므로 검색 속도가 빠름
  • 언어 사용에 따라 인덱스를 개별적으로 확장 가능
  • 관리할 인덱스가 여러 개
  • 문서에 여러 언어가 혼합되어 사용된 경우 단일 문서를 여러 인덱스로 색인하기가 쉽지 않음

그래도 결정하기 어려운 경우 두 가지 방법을 모두 사용해 보고 데이터 세트에 어떤 전략이 적합한지 확인하는 것이 좋습니다. 정확도 레이블의 데이터 세트가 있는 경우 ranking evaluation API를 사용하여 다양한 전략 간에 정확도에 차이가 있는지 확인할 수도 있습니다.

추가 접근 방식

언어 식별 및 인덱스를 사용하고 다국어 코퍼스를 검색하는 두 가지 기본 전략을 살펴보았습니다. 수집 파이프라인의 탁월한 기능 덕분에 다양한 추가 접근 방식 및 수정 사항을 구현할 수 있습니다. 다음과 같이 몇 가지 예를 살펴보겠습니다.

  • 스크립트 공통 언어를 단일 필드에 매핑합니다. 예를 들어 중국어, 일본어 및 한국어를 cjk 필드에 매핑하고 cjk 분석기를 사용하고 표준 분석기를 사용하여 enfrlatin 필드에 매핑합니다(참조: examples/olympics.txt).
  • 알려지지 않은 언어 또는 비 라틴 스크립트를 icu 필드에 매핑하고 icu 분석기를 사용합니다(참조: config/mappings/lang-per-field.json).
  • 프로세서 조건부 또는 스크립트 프로세서를 사용하여 임계값을 초과하는 여러 상위 언어를 필드로 설정합니다(패시팅/필터링용).
  • 언어를 식별하기 위해 문서의 여러 필드를 단일 필드로 연결하고 선택적으로 이를 사용하여 검색하거나(예: all_contents 필드) 언어를 식별한 후 계속 “언어별 필드” 전략을 따릅니다(참조: examples/simulate-concatenation.txtexamples/simulate-concatenation.out.json).
  • 스크립트 프로세서를 사용하여 상위 클래스가 임계값을 초과하거나(예: 60% 또는 50%) 예측된 두 번째 클래스보다 훨씬 높은(예: 50% 초과 두 번째 클래스보다 10% 이상 높음) 경우에만 주요 언어를 선택합니다.

결론

이 블로그를 통해 다국어 검색에 언어 식별을 사용하는 방법에 대한 아이디어를 얻으셨길 바랍니다. 여러분의 의견을 듣고 싶습니다. 토론 포럼에 글을 남겨주세요. 언어 식별을 성공적으로 사용하고 계신지 아니면 문제가 발생했는지 알려주세요.

참고 자료

  1. Amanda Spink, Dietmar Wolfram, Major B. J. Jansen, Tefko Saracevic. 2001. Searching the Web: The Public and Their Queries. American Society for Information Science and Technology 저널. 52판, 3쇄, 페이지 226~234.
  2. A. Putsma. 2001. Applying Monte Carlo Techniques to Language Identification.