[한국] 스푼라디오 사용자 검색개선

임용근
Spoon Radio
Published in
14 min readMay 12, 2023

안녕하세요.

스푼라디오 Discovery Team에서 검색과 빌링개발업무를 담당하고 있는 Whale(임용근) 입니다. 🐳

얼마전 스푼라디오의 검색서비스 개선작업을 수행하였고, 앞으로도 몇차례 단계를 거쳐 진행해야하지만, 1차 완료된 시점에 대해 공유드립니다.

또한, 본 포스트는 개선사항에 대한 회고이므로, 기술적인 접근 및 설정에 대한 자세한 설명은 다음 포스트를 통해 공유하겠습니다.

NOTE

AWS Opensearch와 Elasticsearch는 엄연히 다른 서비스로 분류하지만, 포스트 작성의 편의를
위해 Opensearch와 Elasticsearch(ES)는 동일 서비스로 취급하고 있습니다.

스푼라디오 검색 서비스 진단하기

스푼라디오의 검색서비스 기능에 대해, Score가 전반적으로 높지않았고 그에 따른 Report 기록도 살펴보았다.

느낀점 :암울하다. 😱

스푼라디오의 다른 검색 서비스보다, 개인적으로 사용자 검색이 가장 시급하다 생각되었다.
개인적으로 문제를 파악해보니 스푼라디오 이용자들은 Nickname을 결정할때, 개성이 뚜렷한것을 볼 수 있다.

특수문자 / 이모지를 이용하여 Nickname을 꾸미고 있다.
DJ들만의 팬(크루)들이 형성되고, Nickname으로 표현하고 있다.

이와 같은 특징으로 만들어지는 Nickname은 스푼라디오의 문화로 자리 잡고 있다.

어떻게 해야할지 대략적인 구상을 통해, 사용자검색부터 손보기로 한다.

검색서비스 환경

스푼라디오에서는 검색서비스는 AWS OpenSearch(ES 6.7) 를 이용하고 있다.
AWS의 OpenSearch는 .. 할말하않.. 하지만 장인은 도구를 탓하지 않는다. 😤

SSPL은 차치하더라도, 기능적으로 접근했을때 Plugin / 사전관리 등 개인적으로 불편한 요소가 많다.

사용자 INDEX의 Documents는 차포떼고 약 500만건이다.

개선방향

고급기능확장을 보다 쉽게 할 수 있도록 처음부터 다시..하고싶지만 이미 서비스 운영중이기 때문에 전면개편이 쉽지 않아, 하위호환을 고려해야하는 상황이다.

기존 ES Mapping과 Query부터 확인해보자

MAPPING

... 생략 ..
"properties": {
"follower_count": {
"type": "long"
},
... 중간 생략 ...
"tag": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
},
"analyzer": "search_analyzer"
},
"username": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
}
}
... 중간 생략 ...
"settings": {
"index": {
"analysis": {
"analyzer": {
"search_analyzer": {
"filter": [
"lowercase"
],
"type": "custom",
"tokenizer": "whitespace"
}
}
}
... 생략 끝.

검색QUERY

{
"_source": ["필드들"],
"query": {
... 필터 ...
"query_string": {
"fields": ["tag", "nickname"],
"query": keywordArray.push(`*${k}*`).join(' AND ')
}
},
"sort": [
{"is_live": {"order": "desc"}},
{"follower_count": {"order": "desc"}},
{"nickname": {"order": "asc"}}
],
"from": 0,
"size": 50
}

검색서비스 히스토리 추적

  • 서비스 개발 당시, 급하게 검색엔진을 도입.
  • 서비스관점에서 검색기능에 대한 우선순위가 낮음.
  • 기타등등

개선사항

사용되던 Mapping / Query를 보면 시급하게 개선해야할 부분이 크게 두가지가 보인다.

  1. 검색 대상 필드에 대한 분석처리
  2. wildcard 검색 지양

사용자 검색에 개선해야할 항목

  1. 특수문자/아스키문자 구분없이 검색이 되어야 한다.
  2. 영문/한글 오타에도 검색가능하도록 한다.
  3. 초성검색이 가능하도록 한다.
  4. 닉네임 전체검색이 되어야 함
  5. 태그 검색 시 1건만 나오도록 해야함
  6. 검색속도 올리기

설정에 변화주기

위 개선항목을 충족하기 위해서는 Mapping과 Query의 수정이 불가피하다.

MAPPING

{
"settings": {
"number_of_shards": "XX",
"number_of_replicas": "XX",
"max_ngram_diff": 255,
"analysis": {
"analyzer": {
"replace_ngram_keyword_analyzer": {
"type": "custom",
"tokenizer": "keyword",
"filter": [ "lowercase", "replace_SpaceToBlank", "full_autocomplete_filter_min_1" ]
},
"ngram_keyword_analyzer": {
"type": "custom",
"tokenizer": "keyword",
"filter": ["lowercase", "autocomplete_filter"]
},
"whitespace_ngram_analyzer": {
"type": "custom",
"tokenizer": "whitespace",
"filter": ["lowercase", "autocomplete_filter"]
},
"full_autocomplete_filter_min_1": {
"type": "custom",
"tokenizer": "keyword",
"filter": ["lowercase", "full_autocomplete_filter_min_1"]
}
},
"tokenizer": {
...은전한닢 설정 생략...
},
"filter": {
"replace_SpaceToBlank": {
"type": "pattern_replace",
"pattern": " ",
"replacement": ""
},
"autocomplete_filter": {
"type": "edge_ngram",
"min_gram": 1,
"max_gram": 255
},
"full_autocomplete_filter_min_1": {
"type": "ngram",
"min_gram": 1,
"max_gram": 255
}
}
}
},
"mappings": {
"[TYPE명]": {
"properties": {
...생략...
"nickname": {
"type": "text",
"analyzer": "replace_ngram_keyword_analyzer"
},
"clean_nickname": {
"type": "text",
"analyzer": "full_autocomplete_filter_min_1"
},
"en_clean_nickname": {
"type": "text",
"analyzer": "ngram_keyword_analyzer"
},
"composite_nickname": {
"type": "text",
"analyzer": "whitespace_ngram_analyzer"
},
...생략 ...
}
}
}
}

검색QUERY

{
"_source": {
"includes": ["결과에 출력될 FIELDS"]
},
"query": {
"bool": {
"minimum_should_match": 1,
...필터...
"should": [
{
"match": {
"nickname": {
"query": originKeyword.trim(),
"analyzer": "keyword"
}
}
},
{
"match": {
"tag": {
"query": originKeyword,
"analyzer": "keyword"
}
}
},
{
"match": {
"clean_nickname": {
"query": originKeyword.replace(/ /g, ''),
"analyzer": "keyword"
}
}
},
{
"match": {
"en_clean_nickname": {
"query": convertKeyword.replace(/ /g, ''),
"analyzer": "keyword"
}
}
},
{
"match": {
"composite_nickname": {
"query": originKeyword.trim().toLowerCase(),
"analyzer": "keyword"
}
}
}
]
}

}
...생략...
from: [XX],
size: [YY]
}
Mapping 과 Search Query는 검색키워드 품질과는 관련없는 설정값은 스크롤압박이 심해 생략하였다.  
(Boost, Highlight, Filter, Sort 등)

혼자서는 힘들다!

Mapping 설정 만으로 위 조건을 충족하기 어려워, CONTEN-SYNCHRONIZER (이하 CS) 라는 Application을 개발하여 운영중이다.

  • CS는 검색 Data에 변형을 주기 위한 Event가 발생하면, 해당 Event를 Consume 및 ES로 동기화를 진행하는 Application이다.
  • ELK 관점에서 보면 L(Logstash)에 해당하는 Application이다.
  • 사용자의 상태변경이 발생할경우, 다른 필드를 포함하여 Nickname(원본)을 기준으로 각종 키워드를 생성한다.
CS 에서 발췌한 코드
  • 한글 / 일본어 등을 처리할 수 있도록 Extractor라는 Library를 별도로 개발하여 관리한다.

NICKNAME 키워드는 어떻게 바뀌는가? 🤔

EXTRACTOR의 역할
  • Index Term(색인어) 추출만으로 검색이 원활히 되지는 않는다.
  • 검색대상이 nickname field 뿐이었다면, 수정 이후 총 4개의 fields로 확장 되었다.
  • 해당 fields에 부합하도록 사용자 검색키워드도 전처리 작업을 진행한다.
  • 검색쿼리에 analyzer를 지정하는것도 좋은방법이지만, 한글 -> 영문자(Keyboard 배열)의 analyzer가 없다.

검색QUERY KEYWORD의 전처리

사용자 검색 키워드의 전처리 과정

TIP 💡

convertKeyword는 단순히 영문자오타를 위한 필드로만 활용되지는 않는다.

ISSUE

실제 ‘스푼라디오’를 검색 시 타이핑하는 과정에 ‘스푼랃’이라는 키워드를 거치기 마련이다.
따라서, ‘스푼랃’ 키워드로 검색을 할 경우 해당키워드는 색인어에는 존재 하지않으므로 결과는 나오지 않으며, 자동완성 표현에도 끊김이 발생한다.

HOW

clean_nickname field에 “스푼라디오” 라는 text가 있고, en_clean_nickname field에 “tmvnsfkeldh” 라는 text가 있을경우, 두 필드에 OR 조건으로 검색을 진행하게 되면 “스푼랃 → tmvnsfke” 가 검색이 되어지는 원리이다.

결과보기

위 언급한 “사용자 검색에 개선해야할 항목”에 대해 검증을 해보도록 한다.

NOTE

검색 수정 전/후를 비교하기 위해 검색 테스트 화면으로 진행하며, 좌(NEW) / 우(OLD)로 구분한다.

특수문자/아스키문자 구분없이 검색

“rt”검색 결과 ᴿᵀ ☞ ʷₜʸ낮달

영문/한글 오타 검색

키워드 “vlsdpf” (“핀엘”의 오타)
키워드 “신” (神 검색) , tls(오타), 神으로 검색해도 같은 결과를 볼 수 있다.
키워드 “패혇” (“vogue”의 오타)

초성검색

키워드 “ㅎㅁㄴ” -> 할머니

NICKNAME 전체검색

키워드 “연 하 영 𝒴.𝒽𝒶❁”

TAG 검색

키워드 “spoon_youngja”

속도개선

위 결과에 time에 해당하는 항목이 검색속도이다. 수십배에서 수백배까지 속도향상을 확인할 수 있다.
wildcard 검색을 대체하여, 각 필드에 색인어를 추출 후 검색하도록 수정한 결과이다. 💪

최종결과

직접 사용해보기 ( https://www.spooncast.net )

개선해야할 부분

1. 분리 된 Field의 통합

사용자 검색에 대한 개선 포인트는, ‘어떻게 검색품질을 높일까?’ 였다.
이 과정에 nickname을 기반으로 다양한 키워드 생성을 하는데, 모두 별개의 field로 구성되어 있다.
이를 nested, object, flattend 등을 사용하여, mapping에 대한 설정을 변경할 예정이다.

2. 자동완성과 검색의 Query 분리

현재는 자동완성과 검색의 결과가 같다. 이는 하나의 쿼리로 사용하도록 되어있기 때문이다.
‘스푼라디오’에 대한 자동완성과 검색결과는 같지만, ‘라디오 스푼’이라는 키워드로 검색을 하게 되면 원하는 결과를 얻지 못할것이다. (‘스푼라디오’도 나와야 할것같은데..)
이를 해결할 수 있는 방법은 그다지 어려운 과제는 아니다.
문제점이라고 인식하기보다, 더 나은방향으로 가기 위한 발판마련과 동시에 과도기라고 생각하고싶다.

3. 그밖에..

ICU Analyzer를 이용할 수 있는 고급기능이 많다. (오타교정 / 다국어검색 등..)
다만 스푼라디오의 사용자검색과는 성격이 맞지 않아 차용하지 않았지만, 전문(FullText)검색에는 충분히 활용할 수 있는 부분이 있어, 추후 활용해보고자 한다.

FAQ

Q. ICU 설정은 ES에서 직접 못하나요?

할 수 있습니다. 다만 한글의 경우 영문자오타/초성추출의 기능이 AWS Opensearch에서는 어려운점을 감안하여, 별도의 Application을 통해 키워드를 생성하고 있습니다. 이 과정에 Unicode → Ascii 처리를 함께 하기 때문에, Mapping 에는 해당 설정을 제외 한것입니다. (다음 포스트 인 일본어처리에서는 설정하는 방법도 함께 다룹니다.)

Q. 키워드 전처리 과정을 Opensearch의 plugin으로 대체할 수 없나요?

네. 현재(2023.05.01)는 불가능합니다. AWS Opensearch에는 별도의 plugin 설치가 되지 않습니다.

Q. Logstash나 Fluentd를 사용하지 않고 CS를 사용한 이유가 있나요?

Redis / Mongodb와 같은 저장소의 Data 동기화(CRUD)를 위해 만들어진 Application인데, ES도 추가되면서 자연스럽게 지금까지 사용하게 되었습니다.
현재도 문제없이 운영되고 있으며, 분리해야하는 상황이 있을경우 Plugin 개발을 통해 Logstash나 Fluentd로 관리할 예정입니다.

마치며.

아직 끝나지 않았습니다. 검색 서비스 개선에 대한 회고는 계속 됩니다.

2부는 “[일본] 스푼라디오 사용자 검색개선"입니다.

부족한 내용이지만 끝까지 읽어주셔서 감사합니다.

--

--