오늘은 Elasticsearch의 내장 기능 중 More Like This (MLT) 쿼리를 활용하여 유사도가 높은 데이터를 조회하는 기능을 구현해보겠습니다. "유사도"는 문서에 포함된 단어들의 출현 빈도(Term Frequency) 등을 복합적으로 계산하여 결정됩니다. 이 MLT 기능을 활용하여, 사용자가 특정 뉴스 기사를 보고 있을 때 그와 내용이 비슷한 다른 뉴스 기사들을 추천해 주는 기능을 구현해 보겠습니다.
우리는 웹, 앱 어디서든 검색 결과와 관련된 '추천' 콘텐츠를 쉽게 마주합니다. 아래 이미지와 같이 벨로그에서도 포스팅 하단에 '관심 있을 만한 포스트'를 제공합니다. Elasticsearch의 MLT 쿼리는 바로 이러한 추천 기술을 가능하게 합니다.

MLT 쿼리는 말 그대로 "이것과 비슷한" 문서를 찾아주는 쿼리입니다. 사용자가 특정한 문서(또는 텍스트)를 선택하면 그것와 내용이 유사한 다른 것들을 추천해주는 기능을 합니다.
MTL 쿼리 방식은 아주 간단하게 아래와 같이 동작합니다.
또한 아래와 같이 쿼리를 작성하여 검색 내용을 설정할 수 있습니다.
GET news/_search
{
"query": {
"more_like_this": { // MLT 쿼리를 쓰겠다
"fields": ["title", "content"], // 어떤 필드에서 비교할지(제목과 콘텐츠)
"like": [ // 이 문서와 비슷한 걸 찾아라
{
"_index": "news", // news로 인덱싱 되어 있고,
"_id": "1" // id가 1인 문서
}
],
"min_term_freq": 1, // 최소 몇 번은 나와야 키워드로 쓸지(1번 이상)
"min_doc_freq": 1 // 최소 몇 개 문서에 등장해야 쓸지(1번 이상)
}
}
}
해당 쿼리는 아주 기본적인 MLT 쿼리 작성 방식이며, 아래의 공식 문서 참고하여 추가적으로 검색 설정을 고급화할 수 있습니다.
진행하고 있는 프로젝트에 이미 검색 엔진으로 사용 중인 Elasticsearch 의 내장 기능 More Like This 쿼리를 활영하여 뉴스 목록 검색 시 관련 뉴스도 함께 검색되도록 구현해보겠습니다.
@Override
public List<NewsResponseDto> searchSimilarNews(Long newsId) {
Query esQuery = new Query.Builder()
.moreLikeThis(mlt -> mlt
.fields("title", "description")
.like(l -> l.document(d -> d.index("news").id(String.valueOf(newsId))))
.minTermFreq(1)
.minDocFreq(1)
)
.build();
return searchElasticsearch(esQuery);
}
//엘라스틱서치 검색
private List<NewsResponseDto> searchElasticsearch(Query esQuery) {
NativeQuery nativeQuery = new NativeQueryBuilder().withQuery(esQuery).build();
SearchHits<NewsDocument> searchHits = elasticsearchOperations.search(nativeQuery, NewsDocument.class);
List<Long> newsIds = new ArrayList<>();
//searchHits에서 newsIds를 반환
for(SearchHit<NewsDocument> hit : searchHits.getSearchHits()) {
NewsDocument content = hit.getContent();
newsIds.add(content.getNewsId());
}
return orderResults(newsIds);
}
// 결과 유사도 정렬 유지
private List<NewsResponseDto> orderResults(List<Long> newsIds) {
if (newsIds.isEmpty()) {
return Collections.emptyList();
}
Map<Long, NewsResponseDto> newsDtoMap = new HashMap<>();
List<NewsResponseDto> newsByIds = newsMapper.findNewsByIds(newsIds);
for (NewsResponseDto newsDto : newsByIds) {
newsDtoMap.put(newsDto.getNewsId(), newsDto);
}
List<NewsResponseDto> newsList = (List<NewsResponseDto>) newsDtoMap.values();
String userId = checkUserId();
matchEsResultsWithRedisData(newsList, userId);
List<NewsResponseDto> resultList = new ArrayList<>();
//검색결과 유사도 정렬 유지
for (Long newsId : newsIds) {
NewsResponseDto newsDto = newsDtoMap.get(newsId);
if (newsDto != null) {
resultList.add(newsDto);
}
}
return resultList;
}
moreLikeThisQuery를 사용하여 Elasticsearch에 요청할 쿼리를 생성합니다. // 유사 뉴스 추천
@GetMapping("/news/{newsId}/similar")
public List<NewsResponseDto> findSimilarNews(@PathVariable Long newsId) {
return newsService.findSimilarNews(newsId);
}
/api/news/{newsId}/similar 라는 API 엔드포인트를 만듭니다.위과 같이 백엔드 코드를 추가 후 화면에서 '관련 뉴스'가 표출될 수 있도록 vue.js 코드를 수정 및 추가하여 테스트를 진행하였습니다.
const news = ref([])
const similarNews = ref([]);
const searchQuery = ref("")
const searchNews = async () => {
const accessToken = userStore.token;
if (!accessToken) {
alert("로그인이 필요합니다.");
router.push('/');
return;
}
if (!searchQuery.value.trim()) {
fetchNews();
similarNews.value = [];
return;
}
//뉴스 초기화
news.value = [];
similarNews.value = [];
try {
// 뉴스 검색
const response = await apiRequest(`/api/news/search?q=${encodeURIComponent(searchQuery.value)}`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
});
if (response.status === 401 || response.status === 403) {
alert("인증이 필요합니다. 로그인 페이지로 이동합니다.");
router.push('/');
return;
}
if (!response.ok) {
throw new Error('뉴스 검색 API 호출 실패');
}
const searchData = await response.json();
news.value = searchData.map(item => ({
...item,
likeCount: item.likeCount || 0,
isLiked: item.liked || false,
}));
// 유사 뉴스 검색
if (news.value.length > 0) {
const firstNewsId = news.value[0].newsId; // 첫 번째 뉴스 ID 추출
const similarNewsResponse = await apiRequest(`/api/news/${firstNewsId}/similar`, {
method: 'GET',
headers: { Authorization: `Bearer ${accessToken}` },
});
if (similarNewsResponse.ok) {
const similarNewsData = await similarNewsResponse.json();
similarNews.value = similarNewsData.map(item => ({
...item,
likeCount: item.likeCount || 0,
isLiked: item.liked || false,
}));
} else {
console.error('유사 뉴스 API 호출 실패');
similarNews.value = [];
}
}
} catch (err) {
console.error('오류 발생:', err);
alert(err.message || '뉴스 검색 중 오류가 발생했습니다.');
news.value = [];
similarNews.value = [];
}
};

Elasticsearch의 More Like This 쿼리를 활용해 복잡한 추천 알고리즘 없이도 간단하게 유사 뉴스 추천 기능을 구현할 수 있었습니다. 처음 접했을 때는 다소 낯설고 어렵게 느껴졌지만, 엘라스틱서치가 제공하는 간단한 설정만으로 서비스에 꼭 필요한 ‘추천’ 기능을 손쉽게 구현할 수 있음을 확인했습니다.
오늘 구현한 작업을 바탕으로 추천 결과에 사용자 행동 데이터(예: 조회수, 좋아요, 클릭 이력)를 반영하거나, 가중치를 조정하는 방식으로 기능을 고도해볼 계획입니다.