GeekNews 각 토픽 하단의 관련 글 추천을 임베딩, HNSW, 후보군 사전 계산, 품질 필터로 어떻게 구성했는지 정리했습니다.
함께 보면 좋은 글: 관련 글 추천을 운영 가능한 형태로 만들기
GeekNews에 “함께 보면 좋은 글” 기능을 추가했습니다. 각 토픽 하단에 관련 글 5개를 보여주어, 지금 읽고 있는 주제와 연결된 다른 글을 더 쉽게 발견할 수 있도록 돕는 기능입니다.
처음에는 간단한 임베딩 기반 검색 기능 정도로 보였습니다. 하지만 실제로 만들어보니, 이 기능은 단순히 “비슷한 글을 찾아 붙인다”보다 훨씬 운영적인 문제였습니다. 웹 페이지를 열 때마다 임베딩을 만들거나 벡터 검색을 돌릴 수는 없고, 추천 품질이 낮으면 독자의 흐름을 오히려 방해합니다. 새 글이 올라올 때마다 기존 글의 관련 후보도 조금씩 달라질 수 있습니다.
그래서 GeekNews의 관련 글 추천은 실시간 추천 API가 아니라, 미리 계산된 후보군을 웹에서 빠르게 읽어오는 구조로 만들었습니다.
왜 관련 글인가
검색은 사용자가 찾고 싶은 단어를 알고 있을 때 강합니다. List는 커뮤니티 반응이 큰 글을 다시 발견하게 합니다. 하지만 사용자가 어떤 글을 읽고 있을 때, 그 주제와 자연스럽게 이어지는 다른 글을 보여주는 방식은 또 다른 문제입니다.
예를 들어 데이터베이스 운영 글을 읽는 사용자는 과거의 장애 회고나 SQLite 관련 글에도 관심이 있을 수 있습니다. AI 도구 관련 글을 읽는 사용자는 개발 워크플로우나 생산성 도구 글을 함께 읽고 싶을 수 있습니다.
“함께 보면 좋은 글”은 이런 연결을 만들기 위한 기능입니다.
GeekNews에는 수년 동안 많은 글이 쌓였습니다. 최신 글을 보는 경험은 여전히 중요하지만, 오래전에 올라온 좋은 글을 다시 찾는 일도 점점 중요해졌습니다. 특히 기술 글은 시간이 지나도 가치가 남는 경우가 많습니다. 새로운 도구가 나왔을 때의 첫 반응, 몇 년 뒤 성숙해진 뒤의 평가, 비슷한 문제를 해결하려던 다른 프로젝트까지 같이 읽을 수 있으면 뉴스는 단순한 시간순 목록을 넘어 작은 기술 아카이브가 됩니다.
실시간 추천이 아니라 후보군
관련 글 추천을 가장 단순하게 만들면, 사용자가 토픽 페이지를 열 때 현재 글을 임베딩하고, 벡터 검색을 돌리고, 결과를 바로 보여주는 방식이 됩니다. 데모로는 좋지만 운영 서비스에는 부담이 큽니다. 페이지 응답 시간이 흔들리고, 모델 장애나 API 지연이 그대로 사용자 경험으로 이어집니다. 비용과 장애 지점도 웹 요청 수에 비례해 커집니다.
GeekNews에서는 이 경로를 분리했습니다.
- 워커가 토픽별 임베딩을 만듭니다.
- HNSW 인덱스로 유사 후보를 찾습니다.
- 품질 필터와 재정렬을 거쳐 후보군을 저장합니다.
- 웹 서버는 저장된 후보군에서 관련 글 5개만 빠르게 읽어옵니다.
이 구조의 장점은 명확합니다. 추천 계산이 느려도 웹 페이지는 느려지지 않습니다. 모델을 바꾸거나 임계값을 조정해도 웹 코드가 크게 흔들리지 않습니다. 장애가 나도 이미 계산된 후보군은 계속 보여줄 수 있습니다.
특히 GeekNews는 7년동안 운영된 올드 웹 서비스와 별도의 Python 워커가 함께 움직이는 구조입니다. NLP나 GPT 호출을 웹 서버 안으로 끌어들이지 않고, 추천 계산을 독립된 배치와 워커로 분리한 것이 가장 중요한 설계 판단이었습니다.
임베딩 입력을 어떻게 만들었나
처음에는 토픽의 제목과 본문 앞부분을 정리해서 임베딩 입력으로 사용했습니다. HTML 태그, 엔티티, URL, 코드 블록을 걷어내고 공백을 정리한 뒤, 제목과 URL, 본문 일부를 묶는 방식입니다.
하지만 뉴스 토픽의 본문은 추천용 텍스트로 항상 좋지는 않습니다. 원문 일부를 붙여온 글도 있고, 사용자의 짧은 코멘트만 있는 글도 있습니다. 어떤 글은 제목이 핵심이고, 어떤 글은 링크된 도메인이나 본문 맥락이 중요합니다. 원문 텍스트를 그대로 임베딩하면 “읽을 만한 관련성”보다 “표면적으로 비슷한 단어”에 끌려가는 경우가 생깁니다.
그래서 이후에는 검색과 추천에 맞춘 짧은 요약을 별도로 만들고, 제목 + 요약을 임베딩 입력으로 쓰는 방향을 더했습니다. 이 요약은 공개용 요약이라기보다 검색용 표현에 가깝습니다. 글의 핵심 주제, 다루는 기술, 문제 영역, 비교 대상이 무엇인지를 모델이 더 잘 잡을 수 있도록 정리하는 중간 산출물입니다.
요약과 임베딩에는 해시를 붙였습니다. 입력이 바뀌지 않았다면 다시 만들지 않습니다. 프롬프트 버전과 모델명도 함께 저장해두었습니다. 추천 품질을 보려면 “어떤 모델과 어떤 프롬프트로 만든 후보인가”를 나중에 추적할 수 있어야 하기 때문입니다.
벡터DB 없이 HNSW로
이 기능을 만들면서 별도의 벡터DB를 바로 도입하지는 않았습니다. GeekNews의 관련 글 추천 규모에서는 기존 저장소에 임베딩을 보관하고, Python 워커가 HNSW 인덱스를 만들어 검색하는 방식으로도 충분했습니다.
임베딩은 토픽 ID, 모델명, 차원, 벡터, 입력 해시와 함께 저장합니다. HNSW 인덱스는 모델별로 따로 만들 수 있게 했습니다. 벡터는 정규화한 뒤 inner product 공간에서 검색해 cosine similarity처럼 다루었습니다.
이 선택은 꽤 실용적이었습니다. 벡터DB를 운영하면 쿼리 기능과 확장성은 좋아지지만, 운영해야 할 시스템도 하나 늘어납니다. 반대로 HNSW 인덱스를 워커가 파일로 관리하면 배포와 장애 범위가 단순해집니다. 웹 서버는 벡터 검색을 몰라도 되고, 추천 결과가 저장된 후보군만 읽으면 됩니다.
유사도만 믿지 않기
임베딩 유사도만으로 관련 글을 고르면 생각보다 이상한 결과가 섞입니다. 제목이 짧은 글은 문맥이 부족하고, 같은 도메인의 글이 여러 개 몰릴 수도 있습니다. 최신 글만 과하게 유리해져도 안 되고, 너무 오래된 글이 맥락 없이 올라와도 어색합니다.
그래서 후보군을 만든 뒤 한 번 더 걸렀습니다.
- 삭제되었거나 비활성화된 토픽, 신고가 있는 토픽은 제외합니다.
- 최소 유사도와 상대 유사도 기준을 함께 봅니다.
- 제목이 짧고 애매한 글은 더 높은 유사도를 요구합니다.
- 후보가 너무 적으면 임계값을 완화하되, 그 사실을 로그로 남깁니다.
- 최근 글에는 작은 보너스를 주지만, 특정 날짜 경계에서 점수가 튀지 않게 완만하게 줄입니다.
- 같은 도메인의 글이 한 추천 영역을 과하게 채우지 않도록 제한합니다.
- 제목 토큰이 겹치거나 같은 출처의 글이면 작은 보정 점수를 더합니다.
이런 규칙은 모델보다 덜 멋져 보이지만, 실제 품질에는 큰 영향을 줍니다. 추천 기능에서 중요한 것은 “가장 비슷한 벡터”를 찾는 일이 아니라, 독자가 납득할 수 있는 5개를 안정적으로 보여주는 일입니다.
새 글이 생겼을 때 오래된 글도 바뀝니다
관련 글 추천에서 놓치기 쉬운 부분이 있습니다. 새 글이 하나 올라오면 그 새 글의 관련 후보만 만들면 되는 것처럼 보이지만, 사실은 기존 글의 후보도 바뀔 수 있습니다.
예를 들어 오늘 SQLite 관련 좋은 글이 올라왔다면, 과거의 SQLite 운영 글이나 로컬 퍼스트 앱 관련 글에서도 오늘 글이 관련 글로 떠야 할 수 있습니다. 그래서 새 토픽을 처리할 때 해당 토픽의 후보군만 만드는 것이 아니라, 가까운 이웃 토픽 일부의 후보군도 다시 계산하도록 했습니다.
전체 재계산은 배치 작업으로 돌리고, 새 글은 이벤트 워커가 처리합니다. 새 토픽 이벤트가 들어오면 요약과 임베딩을 만들고, HNSW 인덱스에 추가하고, 후보군을 저장합니다. 최근 글 후보군은 별도 배치로 다시 만들 수 있게 해두었습니다. 이렇게 이벤트 처리와 배치를 함께 두면, 새 글 반영 속도와 전체 품질 보정을 둘 다 가져갈 수 있습니다.
로그인 사용자에게 먼저 보이는 이유
처음부터 전체 사용자에게 크게 노출하기보다, 실제 사용 데이터와 운영 피드백을 보며 조정하기로 했습니다. 추천 기능은 편리하지만, 품질이 낮으면 오히려 독자의 흐름을 방해할 수 있기 때문입니다.
추천 기능은 품질을 천천히 확인해야 합니다. 관련이 낮은 글이 자주 보이면 사용자는 추천 영역을 무시하게 됩니다. 그래서 처음에는 로그인 사용자에게만 노출해 실제 사용 흐름을 보고, 이상한 추천이나 반복되는 추천을 줄이는 방향으로 조정하기로 했습니다.
또한 로그인 사용자는 Favorite이나 Vote 같은 행동을 통해 GeekNews를 조금 더 적극적으로 사용하는 분들입니다. 초기 피드백을 받기에 더 적합한 사용자층이기도 합니다.
운영하면서 배운 것
이 기능을 만들며 다시 확인한 점은, AI 기능도 결국 운영 기능이라는 것입니다. 모델을 붙이는 것보다 중요한 것은 계산을 어디서 할지, 실패했을 때 무엇을 보여줄지, 품질을 어떻게 조정할지, 이전 결과를 어떻게 추적할지였습니다.
추천 결과를 바로 만들지 않고 후보군으로 저장해두면 조정할 수 있는 여지가 생깁니다. 모델을 바꿔도 같은 웹 화면에서 비교할 수 있고, 임계값을 바꿔도 후보군 재생성만 하면 됩니다. 특정 글에서 이상한 추천이 나오면 유사도, 필터 통계, 도메인 제한, 최신성 보너스를 따로 볼 수 있습니다.
작은 서비스에서 모든 것을 처음부터 거대한 추천 시스템처럼 만들 필요는 없습니다. 다만 “나중에 품질을 고칠 수 있는 구조”는 필요합니다. GeekNews의 관련 글 추천은 그 정도의 현실적인 선을 찾는 작업이었습니다.
쌓인 콘텐츠를 다시 살리기
하다 스튜디오는 GeekNews를 오래 운영하면서 축적의 문제를 계속 보고 있습니다. 글이 많아질수록 새 글을 잘 보여주는 것만으로는 부족합니다. 좋은 글이 필요한 순간에 다시 발견되어야 합니다.
List, GeekLists, 관련 글 추천은 모두 같은 방향을 향합니다. GeekNews를 단순한 최신 뉴스 사이트가 아니라, 시간이 지나도 참고할 수 있는 기술 지식의 얇지만 넓은 아카이브로 만드는 것입니다.
원래 공지: 함께 보면 좋은 글 기능을 추가했습니다