OpenSearch Analyzer를 활용한 검색기능 알아보기

OpenSearch Analyzer를 활용한 검색기능 알아보기

시작하며

안녕하세요, 카카오페이손해보험 플랫폼기술팀의 리즈입니다.

여러분은 데이터베이스 엔진을 몇 가지나 알고 계시는가요? 최초의 데이터베이스인 IDS가 1960년대에 설계1된 이후, 2025년인 지금까지 MySQL, Oracle, MongoDB, Redis, PostgreSQL 등 다양한 데이터베이스가 개발되어 사용되고 있습니다.

그렇기 때문에 우리가 서비스를 구축할 때는 단순히 데이터를 저장하는 목적을 넘어, 각 데이터베이스의 특징과 장단점을 비교하며 서비스에 가장 적합한 데이터베이스를 선택하는 것이 중요합니다.

이번 글에서는 검색 기능을 갖춘 데이터베이스이자 검색 엔진인 OpenSearch를 소개하고, 카카오페이손해보험의 플랫폼기술팀에서 어떻게 OpenSearch를 활용하고 있는지 함께 살펴보겠습니다.

OpenSearch와 Analyzer를 알아보자

OpenSearch란?

OpenSearch는 검색 기능을 갖춘 NoSQL 데이터베이스이자 검색 엔진입니다.

먼저, OpenSearch의 역사부터 간략히 알아보겠습니다. OpenSearch는 처음부터 새롭게 개발된 데이터베이스가 아니라 Elasticsearch에서 파생된 데이터베이스입니다. Elasticsearch는 Elastic 사에서 2010년에 출시된 검색 엔진2으로, 2021년에 Elastic사가 오픈소스였던 라이선스를 부분적으로 유료화3하면서, AWS가 Elasticsearch의 마지막 오픈소스 버전을 포크하여 독자적으로 개발한 프로젝트가 OpenSearch4입니다. 이 때문에 Elasticsearch와 OpenSearch는 핵심 기능과 구조가 매우 유사하며, 기존에 Elasticsearch를 사용해 본 경험이 있다면 OpenSearch도 쉽게 사용할 수 있습니다.

다음으로, OpenSearch의 주요 특징을 살펴보겠습니다. 가장 큰 특징은 Apache Lucene을 검색 엔진으로 사용하여 뛰어난 검색 기능을 제공한다는 것입니다. Apache Lucene은 강력한 문서 전체 검색 및 인덱싱 기능을 제공하는 Java 기반의 검색 라이브러리로, 대량의 데이터를 효율적으로 인덱싱하고 빠르고 정교한 검색 결과를 제공합니다. 따라서 검색 서비스가 필요한 시스템을 구축할 때 OpenSearch는 매우 유용한 선택지가 될 수 있습니다.

또한 OpenSearch는 JSON 기반의 문서(Document)를 저장하고 관리하기 때문에 반정형 데이터를 효율적으로 다룰 수 있습니다. 그리고 동적 스키마와 매핑 설정을 통해 데이터의 필드 타입을 유연하게 정의할 수 있어, 데이터 구조의 변경에도 유연하게 대응할 수 있는 장점이 있습니다.

마지막으로, OpenSearch의 구성 요소는 RDBMS에서 사용하는 용어와 다소 차이가 있습니다.

아래 표를 통해 간략하게 비교해 보겠습니다.

OpenSearchRDBMS
IndexTable
DocumentRow
FieldColumn
MappingSchema

OpenSearch에서 Index는 Table과 유사합니다. Document는 Index에 저장되는 데이터 Row를 의미하며, Document에 저장하는 각 Field는 데이터 타입을 관리합니다. Mapping은 테이블의 Schema와 유사하며 적재할 Document의 Field, Type, Analyzer를 설정할 때 활용됩니다.

OpenSearch Analyzer 활용하기5

Analyzer기반으로 데이터 저장부터 검색까지

앞서 OpenSearch의 특징으로 Apache Lucene을 활용한 뛰어난 검색 기능에 대해 말씀드렸습니다.

OpenSearch는 입력된 데이터를 Analyzer로 일정한 규칙에 따라 토큰(token)으로 분리한 뒤 인덱싱하며, 이를 통해 검색의 정확도와 효율성을 높입니다.

다음으로 Analyzer가 데이터를 어떻게 변환하여 저장하는지, 또 이 과정이 실제 검색에서는 어떻게 활용되는지 간단한 예제를 통해 살펴보겠습니다.



OpenSearch의 데이터 적재부터 검색까지의 진행 과정

  1. 우선 인덱스를 생성하고, 각 필드에 대해 매핑(mapping) 정보를 설정합니다.
  2. 데이터를 적재하면 Analyzer를 통해 데이터가 토큰(token) 단위로 분리되어 인덱싱됩니다.
  3. 사용자가 검색어를 입력하면, 인덱싱된 토큰을 기반으로 해당 검색어와 일치하는 문서를 조회하여 결과로 반환합니다.

이제 예제를 통해 단계별로 살펴보겠습니다.

대량의 책 데이터를 적재하여 검색 서비스를 구현한다고 가정하고, 아래와 같은 검색 요구 사항을 만족해야 합니다.

제목 (예: “기초부터 시작하는 Java!“)

  • 전체 단어가 아닌 일부 단어만 입력해도 검색할 수 있다.
  • 대소문자 구분 없이 검색할 수 있다.
  • 검색어에 특수문자가 포함되지 않아도 검색할 수 있다.

저자 (예: “김철수”)

  • 전체 단어를 정확하게 입력한 경우에만 검색할 수 있다.

출판일 (예: 2024-12-01)

  • 검색어로 활용되지 않는다.

이제 이러한 검색 요구 사항을 만족시키기 위해 OpenSearch에서 Analyzer를 어떻게 활용하는지 자세히 설명해 보겠습니다.

예제를 진행하면서 OpenSearch의 쿼리를 실행해야 하는데, OpenSearch에서 제공하는 방법은 다음과 같습니다.

  1. REST API 활용
  2. OpenSearch Dashboards의 Dev Tools Console에서 DSL(Query DSL) 활용
  3. Java, Python 등 언어별 OpenSearch 라이브러리 활용

이번 설명에서는 OpenSearch Dashboards에서 DSL을 활용하여 진행하겠습니다.

1. 인덱스 생성

OpenSearch에서 데이터를 관리하기 위해서는 먼저 인덱스를 생성해야 합니다. 인덱스를 생성할 때는 매핑 정보를 정의하며, 이 과정에서 각 필드와 필드의 타입 등 여러 가지 옵션을 설정할 수 있습니다.

주요 필드 타입과 특징은 다음과 같습니다.

  • keyword : 원본 데이터를 그대로 인덱싱합니다. 주로 집계(aggregation), 필터링, 정렬 등의 연산에 적합합니다.
  • text : 원본 데이터를 Analyzer로 토큰(token) 단위로 분리하여 인덱싱합니다. 토큰화를 위해 Analyzer를 설정할 수 있으며, Analyzer는 원본 데이터의 전처리 및 토큰 분리 규칙을 정의합니다.
  • integer, long, float, double, boolean, date 등 : 주로 필터링, 정렬, 범위 검색에 사용되며, 별도의 토큰화 과정 없이 그대로 인덱싱됩니다.

한편, Analyzer를 필드에 적용하면 추가적인 자원이 소모됩니다. 따라서 효율적인 자원 관리를 위해 검색이 필요한 필드에만 Analyzer를 적용하는 것이 좋습니다.



예제 기준으로 타입과 Analyzer를 설정하면 다음과 같습니다.

  • 제목: text 타입으로 지정 + book_name_analyzer 분석기를 적용
  • 저자: keyword 타입으로 지정
  • 출판일: date 타입으로 지정

인덱스 생성 질의는 아래와 같습니다.

PUT /book_liz
{
  "mappings": {
    "properties": {
      "bookName": { # 제목
        "type": "text",
        "analyzer": "book_name_analyzer" # 적재 분석기
      },
      "author": { # 저자
        "type": "keyword"
      },
      "publishDate": { # 출판일
        "type": "date"
      }
    }
  },
  "settings": {
    "max_ngram_diff": "20",
    "number_of_shards": "1",
    "analysis": {
      "analyzer": {
        "book_name_analyzer": {
          "type": "custom",
          "char_filter": [
            "remove_whitespace",
            "remove_special_chars"
          ],
          "tokenizer": "ngram_tokenizer",
          "filter": [
            "lowercase"
          ]
        }
      },
      "char_filter": {
        "remove_whitespace": {
          "type": "pattern_replace",
          "pattern": """\s+""",
          "replacement": ""
        },
        "remove_special_chars": {
          "type": "pattern_replace",
          "pattern": "[^a-zA-Z0-9가-힣]",
          "replacement": ""
        }
      },
      "tokenizer": {
        "ngram_tokenizer": {
          "type": "ngram",
          "token_chars": [
            "letter",
            "digit"
          ],
          "min_gram": "1",
          "max_gram": "20"
        }
      }
    }
  }
}

2. 데이터 인덱싱

다음은 데이터를 적재할 때 인덱싱되는 과정에 대해 알아보겠습니다.

우선, 데이터를 인덱싱하는 이유를 설명하기 위해 전공 서적을 예로 들어보겠습니다. 두껍고 무거운 Java 책에서 JVM에 대한 정보를 찾으려고 한다고 가정해 보겠습니다. 목차를 보면 대략적인 위치를 알 수 있지만, 결국에는 페이지를 넘기며 원하는 내용이 있는지 직접 확인해야 합니다. 그러나 이 책에 색인(인덱싱) 기능이 제공된다면, 책의 마지막 페이지에 주요 용어와 해당 페이지 번호가 정리되어 있어 원하는 정보를 훨씬 빠르게 찾을 수 있습니다.

OpenSearch의 인덱싱도 이와 같은 원리로 동작합니다. 데이터를 적재하면 문서가 추가되고, OpenSearch는 inverted index(역색인)를 생성하여 특정 검색어(토큰)와 이를 포함하는 문서 ID들을 매핑합니다. 이후 사용자가 검색어를 입력하면 OpenSearch는 인덱스를 활용해 해당 검색어가 포함된 문서를 빠르게 찾아 반환합니다.



이제 예제를 통해 필드별 데이터 타입에 따라 인덱싱이 어떻게 처리되는지 알아보겠습니다.

먼저 데이터를 적재하면 데이터는 document에 인덱싱되어 저장됩니다. 그리고 내부적으로 Analyzer를 통해 토큰화되어 역색인 됩니다.

책 제목의 검색 요구 사항은 다음과 같습니다.

  1. 전체 단어가 아닌 일부 단어만 입력해도 검색할 수 있어야 한다.
  2. 대소문자 구분 없이 검색할 수 있어야 한다.
  3. 검색어에 특수 문자가 포함되지 않아도 검색할 수 있어야 한다.

이 요구 사항을 만족하려면 기본으로 제공되는 analyzer만으로는 처리가 어렵기 때문에, custom analyzer 기능을 활용하여 직접 구성해야 합니다.

이를 위해 book_name_analyzer라는 analyzer를 정의하고, settings에서 옵션을 설정합니다.

analyzer를 통한 데이터 토큰화는 char_filter → tokenizer → filter 순서로 진행됩니다.



1. char_filter : 데이터 전처리

데이터가 역색인 되기 전에 가공할 수 있으며, 필요 없는 문자를 제거하거나 다른 문자로 변경하는 등의 처리를 수행할 수 있습니다. 여기서는 데이터에서 특수 문자와 공백을 제거하는 필터를 추가해 보겠습니다.

  • remove_whitespace 필터를 정의하여 정규식을 활용해 공백을 제거하도록 구성합니다.
  • remove_special_chars 필터를 정의하여 영어 대소문자, 숫자, 한글을 제외한 모든 문자를 제거하도록 설정합니다.
"char_filter": {
  "remove_whitespace": {
    "type": "pattern_replace",
    "pattern": """\s+""",
    "replacement": ""
  },
  "remove_special_chars": {
    "type": "pattern_replace",
    "pattern": "[^a-zA-Z0-9가-힣]",
    "replacement": ""
  }
},

이를 통해, ‘기초부터 시작하는 Java!’ 라는 문자열은 ‘기초부터시작하는Java’ 로 변환됩니다.



2. tokenizer : 데이터 토큰 생성

tokenizer에는 여러 종류가 있는데, 일부 단어만 입력해도 검색할 수 있어야 한다는 요구 사항을 충족하기 위해 ngram tokenizer를 사용해 보겠습니다.



※ ngram tokenizer란?6

텍스트를 일정한 길이의 연속된 문자 조각(n-gram)으로 분리하는 기능을 제공합니다.
예를 들어, “java”라는 텍스트를 ngram tokenizer로 토큰화하면 n-gram 단위로 분리됩니다.

n = 1 일 때: j, a, v, a
n = 2 일 때: ja, av, va
n = 3 일 때: jav, ava
n = 4 일 때: java

ngram tokenizer를 활용하면 부분 문자열이 역색인되어 데이터 일부만 입력해도 검색할 수 있기 때문에, 검색 기능을 구현하는 데 유용하게 활용됩니다. 이번 예제에서는 ngram Tokenizer를 사용했지만, OpenSearch 공식 매뉴얼에 다양한 Tokenizer가 있으니, 요구사항에 맞게 선택할 수 있습니다.



다시 돌아와서 예제를 진행하기 위해 제목 필드의 tokenizer 타입을 ngram으로 지정하고, 토큰화할 문자 종류를 letter와 digit으로 설정합니다. 또한, 검색어의 길이를 고려하여 min_gram과 max_gram 값을 조정합니다.

"tokenizer": {
  "ngram_tokenizer": {
    "type": "ngram",
    "token_chars": [
      "letter",
      "digit"
    ],
    "min_gram": "1",
    "max_gram": "20"
  }
}

이렇게 구성된 tokenizer를 통해 “기초부터시작하는Java”를 토큰화하면 ‘기초’, ‘시작’, ‘Java’ 등 총 78개의 토큰이 생성됩니다.



3. filter : 데이터 후처리

대소문자 구분 없이 검색할 수 있도록 filter에 ‘lowercase’ 옵션을 지정하여 모든 문자를 소문자로 변환합니다.
이렇게 설정하면 ‘Java’, ‘시작하는Java’와 같은 토큰이 ‘java’, ‘시작하는java’로 변경됩니다.
데이터를 적재하면 먼저 인덱스에 문서(Document) 형태로 저장됩니다. 문서 ID(_id)는 편의상 숫자로 설정하겠습니다.



결과적으로 제목 필드의 데이터 인덱싱은 아래와 같이 처리됩니다.

데이터가 인덱싱되면, 인덱스에 문서 ID와 함께 저장됩니다. 이후 OpenSearch 내부에서 역색인(Inverted Index) 처리가 수행되며, 각 토큰이 포함된 문서의 매핑 정보가 저장되어 빠른 검색이 가능해집니다.

그 외에 저자 필드는 keyword 타입이므로 문자열 자체가 인덱싱됩니다.

3. 인덱싱된 데이터 검색

마지막으로, 데이터 검색 단계입니다. 검색어를 포함한 쿼리를 실행하면 OpenSearch는 역색인(Inverted Index)에서 검색어와 일치하는 토큰을 찾아 매핑된 문서를 반환합니다.

예제 기반으로 검색할 때, 제목에 “java”를 검색하면 아래와 같은 흐름을 따라 검색 결과가 반환됩니다.

## 검색쿼리
GET /book_liz/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "bookName": "java"
          }
        }
      ]
    }
  }
}

또한, 응답된 결과에는 _score필드가 포함되어 있습니다. _score는 Apache Lucene의 주요 기능 중 하나로, 검색된 데이터의 연관성 점수를 나타냅니다. OpenSearch는 검색 결과의 연관성을 평가하기 위해 BM25 알고리즘을 사용하며, 점수가 높을수록 검색 결과의 상위에 위치합니다.

BM25 알고리즘에 따라 점수가 높게 산출되는 기준 중 일부는 다음과 같습니다.

  • 검색어가 포함된 텍스트의 길이가 짧을수록 높은 점수를 받습니다.
  • 검색어가 텍스트의 앞부분에 위치할수록 높은 점수를 받습니다.

또한, OpenSearch에서는 검색할 때 커스텀 스크립트를 작성하여 원하는 출력 우선순위를 정의할 수도 있습니다.
이제까지 OpenSearch의 개념, 인덱싱 방식, 그리고 Analyzer를 활용한 검색 과정이 어떻게 진행되는지 살펴보았습니다.
다음으로, 카카오페이손해보험에서 OpenSearch 활용 사례를 소개해 드리겠습니다.

카카오페이손해보험의 OpenSearch 활용사례

카카오페이손해보험의 검색서비스

카카오페이손해보험에서 운영 중인 보험서비스 중에는 외부 데이터 세트를 활용하여 업무 프로세스에 활용하는 경우가 있는데요. 플랫폼기술팀에서는 공통으로 활용할 수 있는 ETL 플랫폼을 구성하여 검색서비스로 운영하고 있습니다. 신규 보험상품에서 데이터 검색 기능이 필요하면, 서비스 담당자와 해당 데이터를 어떤 포맷으로 관리할지, 어떤 값으로 검색할지, 어떤 정렬조건이 필요한지를 설계한 후, OpenSearch에 데이터를 구축하여 검색서비스를 운영합니다.

구축한 데이터는 API를 통해 데이터를 제공하고 있는데, 검색 API URL에 ’/_doc’ 또는 ’/_search’와 같은 Path를 추가하여 검색 기능을 제공합니다. Path 별 검색 방식을 살펴보면 다음과 같습니다.

  • /_doc : keyword 타입 필드 사용. 정확히 일치하는 데이터로 검색해야 하는 경우 호출됩니다.
  • /_search : text 타입 필드 사용. 검색어 일부만 일치해도 조회가 필요한 경우 호출됩니다.

어떤 보험상품에서 활용할까

다음으로, 위 검색서비스를 이용하는 보험서비스 사례 2가지를 소개하겠습니다.

사례 1) 해외여행자보험에 가입한 고객은 출국 항공편이 일정 시간 이상 지연되면, 공항 내 식당, 카페 등 편의시설을 이용한 후 영수증을 제출하여 보험금을 청구할 수 있습니다. 이후, 보상 서비스는 보험금 지급 심사를 위해 OpenSearch에 저장된 실시간 비행기 이력 데이터를 조회하여 심사 프로세스에 활용합니다.

사례 2) 골프보험 서비스 가입씬에서는 고객이 예약한 골프장 이름을 조회하여 선택한 후 프로세스를 진행해야 합니다. 이때, 골프장명 검색 및 정렬을 위해 OpenSearch를 활용하고 있습니다. 아래 이미지는 골프장 검색씬입니다.

마치며

지금까지 OpenSearch와 Analyzer, 그리고 카카오페이손해보험에서의 활용 사례를 소개해 드렸습니다. OpenSearch는 강력한 검색 기능을 제공하며, Analyzer를 적절히 활용하면 더욱 정교하고 효율적인 검색 환경을 구축할 수 있습니다. 이 글을 읽으신 분들께 유익한 내용이 되면 좋겠습니다.

읽어주셔서 감사합니다.

참고문헌

Footnotes

  1. 최초의 데이터베이스 - https://en.wikipedia.org/wiki/Integrated_Data_Store

  2. ElasticSearch 출시 - https://en.wikipedia.org/wiki/Elasticsearch

  3. ElasticSearch 라이센스 변경 - https://www.elastic.co/blog/licensing-change

  4. OpenSearch 출시 - https://en.wikipedia.org/wiki/OpenSearch_(software)

  5. OpenSearch Analyzer Docs - https://opensearch.org/docs/latest/analyzers/

  6. OpenSearch N-Gram Tokenizer Docs - https://opensearch.org/docs/latest/analyzers/tokenizers/ngram/

liz.diaz
liz.diaz

카카오페이손해보험 플랫폼기술팀 개발자 리즈입니다. 백엔드 업무를 담당하며, 좋은 서비스를 만들기 위해 노력하고 있습니다.