[OpenSearch] 영양제 검색 품질 개선기 Nori Tokenizer

조제·2025년 8월 25일
0

문제 배경

  1. 최근 우리 서비스에서 "아쉬아간다" 같은 영양제명을 검색할 때, 원하는 제품 대신 관련 없는 제품이 노출되는 문제가 있었습니다.

    예시:

    • 검색어: "아쉬아간다"
    • 검색 결과: "히든 핏 가르시니아 포 우먼"
  2. 그리고 "힌트" 가 포함된 제품이 2개 있었는데 1개만 나오는 문제가 있었습니다.

    • 제품명: "힌트 리스펙타 질 유산균"

원인을 분석한 결과:

  • 문제점: 부분 문자열 매칭 때문에 관련 없는 제품까지 검색 결과에 포함됨
  • Char filter 설정 일부가 공백을 제거하거나 불필요하게 모든 특수문자를 제거하여 토큰화에 영향을 줌

문제 분석

Ngram Tokenizer 검토

  • Ngram은 부분 문자열 검색에 적합하지만, 영양제처럼 정확한 제품명을 찾는 경우 오히려 부작용 발생
  • "아쉬아간다""아쉬", "쉬아", "아간" 등으로 분해되어 관련 없는 제품까지 검색 결과에 포함됨

Char Filter 문제

  • 기존 설정 1: 의미 없는 특수문자만 제거 → 공백 유지
  • 기존 설정 2: [^\uAC00-\uD7A3a-zA-Z0-9] → 공백까지 제거
  • Nori tokenizer + mixed 모드에서 공백이 제거되면 단어 경계가 무너짐
    • 예: "힌트 리스펙타 질 유산균""힌트리스펙타질유산균"
    • "힌트" 검색 시 매칭 실패

개선 전략

Nori tokenizer + mixed 모드 사용

  • 합성어 분해 지원 → "혼합비타민C"["혼합", "비타민", "C"]
  • 정확 단어 매칭 가능
  • Ngram 제거 → 불필요한 부분 문자열 매칭 방지

Char Filter 최적화

  • 공백은 유지하고, 의미 없는 특수문자만 제거
  • 예시 설정:
char_filter:
  remove_extra_whitespace:
    type: pattern_replace
    pattern: "\\s+"
    replacement: " "
  remove_noise_chars:
    type: pattern_replace
    pattern: "[!@#$%^&*()=~`{}\\[\\]|\\\\:;\"'<>,?/]"
    replacement: ""
  • 효과:
    • "힌트 리스펙타 질 유산균"["힌트", "리스펙타", "질", "유산균"]
    • "힌트" 검색 시 정확히 매칭 가능

검색 쿼리 최적화

  • 단순 matchQuery → 의도치 않은 매칭 발생 가능
  • 개선: match_phraseQuery 또는 keyword/termQuery 사용
Query matchPhraseQuery = Query.of(q ->
    q.matchPhrase(mp ->
        mp.field(searchType.getKey())
          .query(FieldValue.of(keyword))
    )
);
  • 정확히 단어 포함 여부만 검색 가능
  • fuzzy/부분 문자열 매칭 방지

적용 확인

Term Vectors

GET http://localhost:29200/supplement/_termvectors/264?fields=supplement_name&term_statistics=true
  • 제품명 "힌트 리스펙타 질 유산균"의 id=264
  • 특정 문서(_id = 264)의 필드가 어떻게 이미 색인돼 있는지를 확인하는 용도
  • 색인된 토큰의 빈도, 위치, 통계까지 확인 가능
  • 하지만 문서가 없다면 결과가 비어 있음 (term_vectors: {})

개선 전

"힌트리스펙타질유산균" 에 대한 색인 결과

{
  ...
  "term_vectors":{
    "supplement_name":{
      "field_statistics":{
        "sum_doc_freq":54439,
        "doc_count":9177,
        "sum_ttf":55237
      },
      "terms":{
        "ᆯ":{
          "doc_freq":185,
          "ttf":186,
          "term_freq":1,
          "tokens":[
            {
              "position":6,
              "start_offset":8,
              "end_offset":10
            }
          ]
        },
        "균":{
          "doc_freq":405,
          "ttf":405,
          "term_freq":1,
          "tokens":[
            {
              "position":8,
              "start_offset":12,
              "end_offset":13
            }
          ]
        },
        "어":{
          "doc_freq":283,
          "ttf":292,
          "term_freq":1,
          "tokens":[
            {
              "position":4,
              "start_offset":8,
              "end_offset":10
            }
          ]
        },
        "유산":{
          "doc_freq":404,
          "ttf":404,
          "term_freq":1,
          "tokens":[
            {
              "position":7,
              "start_offset":10,
              "end_offset":12
            }
          ]
        },
        "유산균":{
          "doc_freq":403,
          "ttf":403,
          "term_freq":1,
          "tokens":[
            {
              "position":7,
              "start_offset":10,
              "end_offset":13
            }
          ]
        },
        "지":{
          "doc_freq":274,
          "ttf":285,
          "term_freq":1,
          "tokens":[
            {
              "position":5,
              "start_offset":8,
              "end_offset":10
            }
          ]
        },
        "질":{
          "doc_freq":18,
          "ttf":18,
          "term_freq":1,
          "tokens":[
            {
              "position":4,
              "start_offset":8,
              "end_offset":10
            }
          ]
        },
        "타":{
          "doc_freq":42,
          "ttf":44,
          "term_freq":1,
          "tokens":[
            {
              "position":3,
              "start_offset":6,
              "end_offset":8
            }
          ]
        },
        "트리스":{
          "doc_freq":1,
          "ttf":1,
          "term_freq":1,
          "tokens":[
            {
              "position":1,
              "start_offset":1,
              "end_offset":5
            }
          ]
        },
        "펙":{
          "doc_freq":8,
          "ttf":8,
          "term_freq":1,
          "tokens":[
            {
              "position":2,
              "start_offset":5,
              "end_offset":6
            }
          ]
        },
        "힌":{
          "doc_freq":1,
          "ttf":1,
          "term_freq":1,
          "tokens":[
            {
              "position":0,
              "start_offset":0,
              "end_offset":1
            }
          ]
        }
      }
    }
  }
}

개선 후

"힌트 리스펙타 질 유산균" 에 대한 색인 결과

{
  ...
  "term_vectors":{
    "supplement_name":{
      "field_statistics":{
        "sum_doc_freq":55092,
        "doc_count":9177,
        "sum_ttf":56014
      },
      "terms":{
        "ㄹ":{
          "doc_freq":236,
          "ttf":240,
          "term_freq":1,
          "tokens":[
            {
              "position":6,
              "start_offset":8,
              "end_offset":9
            }
          ]
        },
        "균":{
          "doc_freq":408,
          "ttf":408,
          "term_freq":1,
          "tokens":[
            {
              "position":8,
              "start_offset":12,
              "end_offset":13
            }
          ]
        },
        "다":{
          "doc_freq":46,
          "ttf":46,
          "term_freq":1,
          "tokens":[
            {
              "position":4,
              "start_offset":6,
              "end_offset":7
            }
          ]
        },
        "리스":{
          "doc_freq":8,
          "ttf":8,
          "term_freq":1,
          "tokens":[
            {
              "position":1,
              "start_offset":3,
              "end_offset":5
            }
          ]
        },
        "유산":{
          "doc_freq":406,
          "ttf":406,
          "term_freq":1,
          "tokens":[
            {
              "position":7,
              "start_offset":10,
              "end_offset":12
            }
          ]
        },
        "유산균":{
          "doc_freq":406,
          "ttf":406,
          "term_freq":1,
          "tokens":[
            {
              "position":7,
              "start_offset":10,
              "end_offset":13
            }
          ]
        },
        "지":{
          "doc_freq":223,
          "ttf":228,
          "term_freq":1,
          "tokens":[
            {
              "position":5,
              "start_offset":8,
              "end_offset":9
            }
          ]
        },
        "질":{
          "doc_freq":18,
          "ttf":18,
          "term_freq":1,
          "tokens":[
            {
              "position":5,
              "start_offset":8,
              "end_offset":9
            }
          ]
        },
        "타":{
          "doc_freq":39,
          "ttf":41,
          "term_freq":1,
          "tokens":[
            {
              "position":3,
              "start_offset":6,
              "end_offset":7
            }
          ]
        },
        "펙":{
          "doc_freq":8,
          "ttf":8,
          "term_freq":1,
          "tokens":[
            {
              "position":2,
              "start_offset":5,
              "end_offset":6
            }
          ]
        },
        "하":{
          "doc_freq":452,
          "ttf":455,
          "term_freq":1,
          "tokens":[
            {
              "position":3,
              "start_offset":6,
              "end_offset":7
            }
          ]
        },
        "힌트":{			// <- "힌트" 추가됨
          "doc_freq":2,
          "ttf":2,
          "term_freq":1,
          "tokens":[
            {
              "position":0,
              "start_offset":0,
              "end_offset":2
            }
          ]
        }
      }
    }
  }
}

적용 결과

  • "아쉬아간다" 검색 시 정확히 포함된 제품만 반환
  • "힌트" 검색 시 "힌트 리스펙타 질 유산균" 정상 노출
  • 불필요한 부분 문자열 매칭 제거 → 검색 품질 개선
profile
조제

0개의 댓글