Post는 N개의 태그를 가질 수 있도록 설계를 하였다.
tag는 중복된 이름이 많을 것을 대비해, 기존의 태그를 사용할 수 있도록 post와 tag 사이 post_tag라는 중간 테이블을 추가하였다.
RDB 기반 태그 검색의 장점과 한계
실제 테스트를 위해 약 100만 건의 데이터를 대상으로 RDB와 Elasticsearch의 검색 속도를 비교해본 결과, RDB 쪽이 더 빠른 성능을 보였다.
(물론 이는 내가 아직 Elasticsearch를 완전히 최적화하지 못했기 때문일 수도 있다.)
하지만, 다음과 같은 상황에서는 RDB 방식에 한계가 있었다.
- 부분 검색 (LIKE '%태그%')
→ 인덱스를 활용하지 못해 검색 속도가 급격히 느려짐 - 철자가 조금씩 다른 경우
→ 예: "운!동", "운 동", "운동기록" 등 - 연관 태그 검색
→ 예: "운동"과 함께 자주 쓰이는 "헬스", "다이어트" 등의 태그를 함께 보여주고 싶을 때 RDB로는 구현이 어려움
Elasticsearch 도입
Post-service
- 게시글 업로드, 업데이트, 삭제 요청을 처리하는 핵심 서비스
- 처리된 요청을 기반으로 Kafka에 이벤트 발행
Kafka
- 게시글 관련 이벤트를 비동기적으로 전파하는 메시지 브로커
Post-Search-service
- Kafka로부터 수신한 이벤트에 따라 Elasticsearch Document를 생성/수정/삭제하여 검색 인덱스를 유지함
- 태그 기반 검색 요청을 처리하고, Elasticsearch에서 검색한 게시글 목록을 클라이언트에 반환
photogram_index & mapping
{
"settings": {
"index": {
"max_ngram_diff": 10
},
"analysis": {
"tokenizer": {
"ngram_tokenizer": {
"type": "ngram",
"min_gram": 2,
"max_gram": 10,
"token_chars": [
"letter",
"digit",
"whitespace",
"punctuation",
"symbol"
]
}
},
"analyzer": {
"ngram_analyzer": {
"type": "custom",
"tokenizer": "ngram_tokenizer",
"filter": [
"lowercase"
]
},
"fuzzy_analyzer": {
"tokenizer": "standard",
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
},
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"thumbnailUrl": {
"type": "keyword"
},
"isVisible": {
"type": "boolean"
},
"writerId": {
"type": "keyword"
},
"tags": {
"type": "text",
"analyzer": "ngram_analyzer",
"search_analyzer": "ngram_analyzer",
"fields": {
"keyword": {
"type": "keyword"
},
"fuzzy": {
"type": "text",
"analyzer": "fuzzy_analyzer"
}
}
}
}
}
}
tags 필드 매핑
- 기본 필드
- n_gram analyzer -> 부분 검색 지원용
- tags.keyword
- 정확한 일치 검색용
- tags.fuzzy
- fuzzy_analyzer -> 오타 대응용
Spring boot data elasticsearch repository 검색 구현
class PostDocCustomRepositoryImpl(
private val elasticsearchClient: ElasticsearchClient
) : PostDocCustomRepository {
override fun searchByTags(keyword: String, lastPostId: Long, size: Int): SearchResponse<PostDocument> {
val exactMatch = TermQuery.Builder()
.field("tags.keyword")
.value(keyword)
.boost(3.0f)
.build()._toQuery()
val fuzzyMatch = MatchQuery.Builder()
.field("tags.fuzzy")
.query(keyword)
.fuzziness("AUTO")
.build()._toQuery()
val ngramMatch = MatchQuery.Builder()
.field("tags")
.query(keyword)
.build()._toQuery()
val visibilityFilter = TermQuery.Builder()
.field("isVisible")
.value(true)
.build()._toQuery()
val boolQuery = BoolQuery.Builder()
.should(listOf(exactMatch, fuzzyMatch, ngramMatch))
.minimumShouldMatch("1")
.filter(listOf(visibilityFilter))
.build()._toQuery()
val request = SearchRequest.Builder()
.index("photogram_posts")
.query(boolQuery)
.size(size)
.sort { s ->
s.field { f -> f.field("id").order(SortOrder.Desc) }
}
.searchAfter(lastPostId)
.build()
return elasticsearchClient.search(request, PostDocument::class.java)
}
}
'project > Photogram' 카테고리의 다른 글
[Photogram] Redis + Resilience4j로 MSA 장애 전파 막기 (0) | 2025.04.23 |
---|---|
Photogram Push 기반 피드 구현 (0) | 2025.04.14 |
Photogram 인증 구현하기 (1) | 2025.04.13 |
Photogram 아키텍처 (0) | 2025.04.13 |
댓글