
RAG(검색 증강 생성) 시스템에서 최신 문서가 자꾸 밀려나는 이유는 벡터 유사도 점수만으로 랭킹을 매기기 때문입니다. 어제 올라온 릴리스 노트든 3년 전에 쓴 가이드든, 임베딩 공간에서 질문과 의미적으로 가깝기만 하면 똑같이 상위에 뜹니다. 이 글에서는 벡터 유사도 점수에 시간 가중치를 더해서 최신 문서를 우선 노출시키는 구체적인 공식과, LangChain·Elasticsearch에서 실제로 이걸 어떻게 구현하는지 코드로 짚어보겠습니다.
RAG 최신성 저하는 벡터 유사도 점수만 봐서 생깁니다
일반적인 RAG 파이프라인은 질문 임베딩과 문서 임베딩 사이의 코사인 유사도를 계산해서 상위 k개 문서를 뽑습니다. 이 과정에는 문서가 언제 만들어졌는지, 언제 마지막으로 갱신됐는지에 대한 정보가 전혀 반영되지 않습니다.
예를 들어 “이번 분기 요금제 변경 사항 알려줘”라는 질문을 던지면, 의미적으로는 작년 요금제 안내 문서가 이번 달 공지사항보다 더 유사도가 높게 나올 수 있습니다. 문장 구조나 단어 선택이 더 정형화돼 있기 때문입니다. 결과적으로 챗봇은 낡은 정보를 근거로 답변을 만들어냅니다. 검색 증강 생성(Retrieval-Augmented Generation)이 원래 노리는 목표, 즉 모델 학습 시점 이후의 정보를 실시간으로 반영한다는 취지가 무색해지는 지점입니다.
시간 가중치는 어떤 공식으로 점수에 섞을까요?
가장 널리 쓰이는 방식은 지수 감쇠(exponential decay)입니다. 문서가 오래될수록 점수를 지수적으로 깎는 방식인데, 스탠퍼드·구글의 “Generative Agents” 논문에서 에이전트 기억을 회상할 때 쓴 공식이 대표적입니다. LangChain의 TimeWeightedVectorStoreRetriever도 이 방식을 그대로 구현체로 제공합니다.
핵심 공식은 이렇습니다.
score = (1 - decay_rate) ** hours_passed + vector_relevance

vector_relevance는 기존 코사인 유사도 점수(보통 0~1 범위)이고, 앞부분은 시간이 지날수록 1에서 0으로 줄어드는 감쇠 항입니다. decay_rate가 랭킹에 미치는 영향을 반감기(half-life)로 환산해보면 감이 잘 옵니다. (1-r)^h = 0.5를 h에 대해 풀면 h = ln(0.5) / ln(1-r)인데, decay_rate=0.01이면 반감기가 약 69시간(2.9일)이고, decay_rate=0.1이면 약 6.6시간으로 뚝 떨어집니다. 뉴스나 이슈 트래킹처럼 하루 단위로 신선도가 중요한 도메인이라면 기본값 0.01은 너무 완만해서, 감쇠율을 직접 올려줘야 체감되는 랭킹 변화를 만들 수 있습니다.
LangChain TimeWeightedVectorStoreRetriever 실습
LangChain에서는 위 공식을 그대로 감싼 리트리버 클래스를 제공합니다. 벡터스토어(FAISS 등)와 함께 감쇠율만 지정하면 됩니다.
from langchain.retrievers import TimeWeightedVectorStoreRetriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain.docstore import InMemoryDocstore
from langchain_core.documents import Document
from datetime import datetime, timedelta
import faiss
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
vectorstore = FAISS(OpenAIEmbeddings(), index, InMemoryDocstore({}), {})
retriever = TimeWeightedVectorStoreRetriever(
vectorstore=vectorstore,
decay_rate=0.3, # 하루 이내 문서에 가중치를 크게 주고 싶을 때
k=4,
)
yesterday = datetime.now() - timedelta(days=1)
retriever.add_documents([
Document(page_content="어제 배포된 릴리스 노트", metadata={"last_accessed_at": yesterday})
])
retriever.add_documents([
Document(page_content="작년에 작성된 초기 사용 가이드")
])
docs = retriever.get_relevant_documents("이번 업데이트에서 바뀐 점")
여기서 실무자가 자주 놓치는 부분이 있습니다. 이 클래스는 문서 생성 시점이 아니라 last_accessed_at, 즉 마지막으로 검색에 걸린 시점을 기준으로 감쇠를 계산합니다. 즉 자주 조회되는 오래된 문서는 계속 시간이 갱신되면서 “최신”처럼 취급받을 수 있습니다. 순수하게 발행일 기준 최신성만 반영하고 싶다면 last_accessed_at을 조회 시마다 갱신하지 않도록 로직을 오버라이드하거나, 발행일을 별도 메타데이터로 두고 직접 점수를 계산하는 편이 안전합니다. 자세한 파라미터와 기본 동작은 LangChain 공식 가이드에 정리돼 있습니다.
검색엔진 랭킹에도 적용할 수 있을까요?
이미 Elasticsearch나 OpenSearch로 색인을 운영 중이라면, 벡터스토어를 따로 두지 않고도 function_score 쿼리의 decay 함수로 같은 효과를 낼 수 있습니다. 날짜 필드를 기준으로 gauss(정규분포형), exp(지수형), linear(선형) 중 하나를 고를 수 있습니다.
GET /docs/_search
{
"query": {
"function_score": {
"query": { "match": { "content": "RAG 랭킹" } },
"functions": [
{
"gauss": {
"published_at": {
"origin": "now",
"scale": "30d",
"decay": 0.5
}
}
}
],
"score_mode": "multiply",
"boost_mode": "multiply"
}
}
}

scale: 30d, decay: 0.5는 발행 후 30일이 지난 문서의 점수를 원래 점수의 절반으로 낮춘다는 뜻입니다. 뉴스성 콘텐츠라면 scale을 7d나 3d로 줄여서 감쇠를 더 가파르게 만들면 됩니다. 정확한 파라미터 정의는 Elasticsearch의 function_score decay 함수 문서에서 확인할 수 있습니다.
두 방식을 비교하면 아래와 같습니다.
| 도구 | 감쇠 방식 | 점수 결합 | 적합한 상황 |
|---|---|---|---|
| LangChain TimeWeightedVectorStoreRetriever | 지수 감쇠, decay_rate 직접 지정 | 덧셈(additive) | 프로토타입, In-memory 벡터스토어 |
| Elasticsearch / OpenSearch function_score | gauss / exp / linear 중 선택 | 곱셈 또는 덧셈 선택 가능 | 이미 색인 운영 중, 대량 문서 랭킹 |
| Pinecone·Weaviate 등 + 후처리 | 자체 구현 필요 | 원하는 방식으로 설계 | 벡터DB에 decay 기능이 없을 때 |
감쇠율을 높이면 무조건 좋을까요?
그렇지 않습니다. 시간 가중치를 세게 걸면 최신 문서는 잘 뜨지만, 법률 조항이나 API 레퍼런스처럼 “오래됐어도 여전히 정답인” 문서가 하위로 밀려나는 부작용이 생깁니다. 갱신 빈도가 낮은 참고 문서 위주의 지식베이스라면 감쇠율을 아예 걸지 않거나, 특정 카테고리(공지사항·뉴스)에만 선택적으로 적용하는 편이 안전합니다.
또 하나 중요한 전제는 메타데이터의 정확성입니다. 크롤링으로 모은 문서 중 발행일이 누락됐거나, 스크래핑 시점을 발행일처럼 잘못 기록한 경우 시간 가중치는 오히려 랭킹을 왜곡합니다. 실제 발행일·최종 수정일이 신뢰할 수 있는 소스에서만 이 기법을 적용하는 것이 전제 조건입니다.
지금 바로 적용해볼 조합은 이렇습니다
프로토타입 단계라면 LangChain의 TimeWeightedVectorStoreRetriever로 감쇠율을 0.05~0.3 사이에서 바꿔가며 반감기를 체감해보는 것부터 시작하면 됩니다. 이미 Elasticsearch나 OpenSearch 색인이 있다면 function_score의 gauss decay를 발행일 필드에 걸고 scale 값만 조정하는 편이 더 적은 코드로 빠르게 적용할 수 있습니다.
어느 쪽이든 핵심은 하나입니다. 유사도 점수 하나만으로 랭킹을 정하지 말고, 문서의 시점 정보를 별도 점수로 만들어 더하거나 곱하는 것입니다. 다만 감쇠율은 도메인마다 다르게 튜닝해야 하고, 시점이 중요하지 않은 참고 문서 카테고리에는 적용을 빼는 예외 처리도 함께 설계해야 실제 서비스에서 부작용 없이 쓸 수 있습니다.
