본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 25. · 3 Views
Metadata Filtering 완벽 가이드 RAG 시스템의 검색 정확도를 높이는 방법
RAG 시스템에서 메타데이터 필터링을 활용하여 검색 정확도를 극대적으로 높이는 방법을 배웁니다. 문서 속성, 시간 범위, 계층적 구조를 활용한 실전 필터링 기법을 실무 예제와 함께 소개합니다.
목차
1. 메타데이터 설계
어느 날 이씨는 회사에서 문서 검색 시스템을 만들다가 난감한 상황에 처했습니다. 벡터 검색으로 문서를 찾아주는 것까지는 좋았는데, 사용자가 "2023년 영업팀 보고서만 찾아줘"라고 요청하자 모든 시기의 모든 부서 문서가 섞여서 나왔습니다.
메타데이터는 문서에 붙이는 부가 정보 꼬리표입니다. 마치 도서관 책에 붙은 분류 라벨처럼, 문서마다 작성일, 부서명, 문서 유형 같은 속성을 달아둡니다.
이 정보를 잘 설계하면 벡터 검색과 결합하여 원하는 문서만 정확하게 찾아낼 수 있습니다.
다음 코드를 살펴봅시다.
from datetime import datetime
from typing import Dict, Any
# 문서 메타데이터 설계 예제
class DocumentMetadata:
def __init__(self, content: str):
self.content = content
self.metadata: Dict[str, Any] = {}
def add_metadata(self, key: str, value: Any):
# 메타데이터 추가
self.metadata[key] = value
def to_dict(self):
# 벡터DB 저장용 딕셔너리 변환
return {
"content": self.content,
"metadata": self.metadata
}
# 실무 활용 예시
doc = DocumentMetadata("2023년 영업팀 1분기 실적 보고서")
doc.add_metadata("department", "sales")
doc.add_metadata("year", 2023)
doc.add_metadata("quarter", 1)
doc.add_metadata("doc_type", "report")
이씨는 입사 6개월 차 개발자입니다. 회사에서 사내 문서 검색 시스템을 만들라는 임무를 받았습니다.
열심히 벡터 검색을 구현했지만, 실제 사용자들의 반응은 시큰둥했습니다. "이씨, 검색 결과가 너무 많이 나와요.
내가 원하는 건 영업팀 문서인데 인사팀, 재무팀 문서까지 다 섞여 나오네요." 선배 개발자 박시니어 씨가 이씨의 고민을 듣고 말했습니다. "아, 메타데이터 설계를 안 했구나.
벡터 검색만으로는 부족해. 문서에 속성 정보를 붙여야 해." 그렇다면 메타데이터란 정확히 무엇일까요?
쉽게 비유하자면, 메타데이터는 마치 옷에 붙은 세탁 라벨과 같습니다. 셔츠를 볼 때 라벨을 보면 소재, 세탁 방법, 제조국을 알 수 있듯이, 문서도 라벨을 달아두면 작성 부서, 날짜, 종류를 바로 파악할 수 있습니다.
이처럼 메타데이터는 문서의 부가 정보를 체계적으로 관리하는 역할을 담당합니다. 메타데이터가 없던 시절에는 어땠을까요?
개발자들은 문서 내용만으로 검색해야 했습니다. "영업팀 문서"를 찾으려면 문서 본문에 "영업팀"이라는 단어가 있는지 확인해야 했습니다.
문제는 본문에 영업팀이라는 단어가 없으면 검색되지 않았다는 점입니다. 더 큰 문제는 시간 범위 검색이었습니다.
"2023년 문서만"이라고 요청해도, 날짜 정보가 본문에 명확하게 없으면 찾을 수 없었습니다. 바로 이런 문제를 해결하기 위해 메타데이터 설계가 등장했습니다.
메타데이터를 사용하면 정확한 필터링이 가능해집니다. 또한 검색 속도도 빨라집니다.
무엇보다 사용자가 원하는 조건으로 문서를 정확하게 찾을 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 DocumentMetadata 클래스를 정의합니다. 이 클래스는 문서 내용과 메타데이터를 함께 관리하는 역할을 합니다.
add_metadata 메서드에서는 키-값 쌍으로 속성을 추가합니다. 예를 들어 "department"라는 키에 "sales"라는 값을 넣으면 해당 문서가 영업팀 문서라는 정보가 기록됩니다.
실무에서는 어떻게 설계할까요? 회사마다 필요한 메타데이터가 다릅니다.
일반적으로 부서명, 작성일, 문서 유형, 보안 등급, 프로젝트명 같은 속성을 많이 사용합니다. 쇼핑몰 서비스를 예로 들면, 상품 문서에는 카테고리, 가격대, 브랜드, 재고 상태 같은 메타데이터를 붙일 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 메타데이터를 너무 많이 설계하는 것입니다.
"혹시 필요할까 봐" 수십 개의 속성을 미리 만들어두면 오히려 관리가 어려워집니다. 실제로 필터링에 사용될 속성만 선별해서 설계하는 것이 중요합니다.
또 하나 주의할 점은 데이터 타입입니다. 날짜는 문자열이 아닌 datetime 객체로, 숫자는 정수형으로 저장해야 나중에 범위 검색이 가능합니다.
다시 이씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 이씨는 회사 문서에 필요한 메타데이터를 정리했습니다.
부서명, 작성연도, 분기, 문서 유형 이렇게 네 가지만 선별했습니다. 메타데이터 설계를 제대로 하면 검색 시스템의 정확도가 비약적으로 향상됩니다.
여러분도 프로젝트에서 문서를 다룰 때 어떤 속성이 필요한지 먼저 고민해 보세요.
실전 팁
💡 - 실제 사용자 검색 패턴을 분석해서 필요한 메타데이터만 선별하세요
- 날짜와 숫자는 적절한 타입으로 저장해야 범위 검색이 가능합니다
- 메타데이터 키 이름은 팀 내에서 일관된 네이밍 규칙을 정하세요
2. 필터링과 벡터 검색 결합
이씨가 메타데이터 설계를 마치고 나서 다음 고민에 빠졌습니다. "메타데이터로 필터링하는 건 알겠는데, 벡터 검색과 어떻게 함께 쓰지?" 단순히 필터링만 하면 의미 기반 검색의 장점이 사라지고, 벡터 검색만 하면 조건 검색이 안 됩니다.
필터링과 벡터 검색을 결합하면 두 가지 장점을 모두 얻을 수 있습니다. 먼저 메타데이터로 후보 문서를 좁힌 다음, 그 안에서 벡터 유사도가 높은 문서를 찾습니다.
마치 도서관에서 특정 서가로 먼저 가서 그 안에서 원하는 책을 찾는 것과 같습니다.
다음 코드를 살펴봅시다.
from typing import List, Dict, Any
import numpy as np
class HybridSearch:
def __init__(self, documents: List[Dict[str, Any]]):
self.documents = documents
def filter_by_metadata(self, filters: Dict[str, Any]) -> List[Dict[str, Any]]:
# 메타데이터로 문서 필터링
filtered = []
for doc in self.documents:
match = True
for key, value in filters.items():
if doc.get("metadata", {}).get(key) != value:
match = False
break
if match:
filtered.append(doc)
return filtered
def vector_search(self, query_vector: np.ndarray, documents: List[Dict[str, Any]], top_k: int = 3):
# 필터링된 문서 중에서 벡터 유사도 검색
similarities = []
for doc in documents:
# 코사인 유사도 계산 (실제로는 벡터DB가 처리)
sim = np.dot(query_vector, doc["vector"]) / (np.linalg.norm(query_vector) * np.linalg.norm(doc["vector"]))
similarities.append((doc, sim))
# 유사도 높은 순으로 정렬
similarities.sort(key=lambda x: x[1], reverse=True)
return [doc for doc, sim in similarities[:top_k]]
def hybrid_search(self, query_vector: np.ndarray, filters: Dict[str, Any], top_k: int = 3):
# 1단계: 메타데이터 필터링
filtered_docs = self.filter_by_metadata(filters)
# 2단계: 벡터 검색
return self.vector_search(query_vector, filtered_docs, top_k)
이씨는 메타데이터 설계를 끝내고 뿌듯해하며 커피를 마시고 있었습니다. 그런데 문득 의문이 들었습니다.
"메타데이터로 거르기만 하면 벡터 검색의 의미가 없잖아? 둘을 어떻게 합치지?" 옆자리의 최주니어 씨도 같은 고민을 하고 있었습니다.
"저도 그게 궁금했어요. 필터링만 하면 키워드 검색이랑 뭐가 다른지 모르겠어요." 박시니어 씨가 두 사람의 대화를 듣고 설명을 시작했습니다.
"좋은 질문이야. 이 둘은 따로가 아니라 함께 써야 해.
하이브리드 검색이라고 하지." 그렇다면 하이브리드 검색이란 무엇일까요? 쉽게 비유하자면, 하이브리드 검색은 마치 대형 서점에서 책을 찾는 과정과 같습니다.
먼저 컴퓨터 서적 코너로 갑니다. 이것이 메타데이터 필터링입니다.
그 다음 그 코너 안에서 내가 원하는 주제와 가장 비슷한 책을 고릅니다. 이것이 벡터 검색입니다.
이처럼 하이브리드 검색은 단계별로 검색 범위를 좁혀가는 역할을 담당합니다. 필터링과 벡터 검색을 따로 사용하면 어땠을까요?
메타데이터 필터링만 사용하면 정확한 키워드가 있어야만 검색됩니다. "영업 실적"이라고 검색하면 "매출 보고서"는 찾지 못합니다.
반대로 벡터 검색만 사용하면 의미는 비슷하지만 전혀 다른 부서나 시기의 문서가 섞여 나옵니다. 더 큰 문제는 사용자가 원하는 조건을 지정할 방법이 없다는 것이었습니다.
바로 이런 문제를 해결하기 위해 하이브리드 검색이 등장했습니다. 하이브리드 검색을 사용하면 정확성과 유연성을 동시에 얻을 수 있습니다.
또한 검색 속도도 빨라집니다. 전체 문서가 아니라 필터링된 문서 안에서만 벡터 계산을 하기 때문입니다.
무엇보다 사용자가 원하는 조건과 의미를 모두 반영한 결과를 제공할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 filter_by_metadata 메서드를 보면 전달받은 필터 조건과 문서의 메타데이터를 하나씩 비교합니다. 모든 조건이 일치하는 문서만 filtered 리스트에 담습니다.
다음으로 vector_search 메서드에서는 필터링된 문서들의 벡터와 쿼리 벡터의 유사도를 계산합니다. 마지막으로 hybrid_search가 두 단계를 순서대로 실행하여 최종 결과를 반환합니다.
실제 현업에서는 어떻게 활용할까요? 고객 지원 시스템을 예로 들어봅시다.
고객이 "환불 정책"에 대해 문의하면, 시스템은 먼저 "FAQ 카테고리"로 필터링합니다. 그 다음 "환불"과 의미적으로 비슷한 문서들을 찾습니다.
결과적으로 고객이 원하는 정확한 답변을 빠르게 제공할 수 있습니다. 전자상거래 사이트에서도 유용합니다.
사용자가 "겨울 코트"를 검색하면, 카테고리를 "의류"로 필터링하고 계절을 "겨울"로 좁힙니다. 그 다음 "코트"와 유사한 상품들을 벡터 검색으로 찾습니다.
파카, 자켓 같은 유사 상품도 함께 추천할 수 있습니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수는 필터링을 너무 빡빡하게 하는 것입니다. 조건이 너무 많으면 필터링 후 남는 문서가 거의 없어서 벡터 검색이 무의미해집니다.
적절한 균형을 찾는 것이 중요합니다. 또 하나 주의할 점은 순서입니다.
반드시 필터링을 먼저 하고 벡터 검색을 나중에 해야 합니다. 순서를 바꾸면 성능이 크게 떨어집니다.
다시 이씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 이씨는 코드를 수정했습니다.
사용자가 부서와 연도를 선택하면 먼저 필터링하고, 그 안에서 검색어와 유사한 문서를 찾도록 구현했습니다. 하이브리드 검색을 제대로 구현하면 검색 시스템의 품질이 완전히 달라집니다.
여러분도 프로젝트에 적용해 보세요.
실전 팁
💡 - 필터링 조건은 3개 이하로 유지하는 것이 좋습니다
- 벡터 검색 전에 필터링하면 성능이 크게 향상됩니다
- 필터링 결과가 너무 적으면 조건을 완화하는 로직을 추가하세요
3. 계층적 필터링
며칠 후 이씨는 새로운 요구사항을 받았습니다. "문서를 대분류, 중분류, 소분류로 나눠서 검색하고 싶어요." 단순한 필터링으로는 이런 계층 구조를 표현하기 어려웠습니다.
카테고리마다 레벨이 다르고 상하 관계가 있었기 때문입니다.
계층적 필터링은 문서를 트리 구조로 분류하여 검색하는 방법입니다. 마치 폴더 안에 하위 폴더가 있는 것처럼, 대분류 안에 중분류, 중분류 안에 소분류를 두어 체계적으로 관리합니다.
상위 카테고리를 선택하면 하위 카테고리까지 함께 검색됩니다.
다음 코드를 살펴봅시다.
from typing import List, Dict, Any, Optional
class HierarchicalFilter:
def __init__(self):
# 계층 구조 정의
self.hierarchy = {
"경영": ["전략", "인사", "재무"],
"기술": ["개발", "인프라", "보안"],
"영업": ["국내", "해외", "파트너"]
}
def get_subcategories(self, category: str) -> List[str]:
# 하위 카테고리 가져오기
return self.hierarchy.get(category, [])
def build_category_filter(self, category: str, include_subcategories: bool = True) -> List[str]:
# 카테고리 필터 생성 (하위 포함 여부 선택)
categories = [category]
if include_subcategories:
categories.extend(self.get_subcategories(category))
return categories
def filter_documents(self, documents: List[Dict[str, Any]], category: str, include_subcategories: bool = True):
# 계층적 필터링 수행
allowed_categories = self.build_category_filter(category, include_subcategories)
filtered = []
for doc in documents:
doc_category = doc.get("metadata", {}).get("category")
if doc_category in allowed_categories:
filtered.append(doc)
return filtered
# 사용 예시
filter_system = HierarchicalFilter()
# "기술" 카테고리 선택 시 "개발", "인프라", "보안"도 함께 검색됨
categories = filter_system.build_category_filter("기술", include_subcategories=True)
print(categories) # ['기술', '개발', '인프라', '보안']
이씨는 기획팀의 김매니저 씨에게 새로운 요구사항을 받았습니다. "문서를 분류별로 체계적으로 관리하고 싶어요.
예를 들어 '경영' 카테고리를 선택하면 '전략', '인사', '재무' 문서가 다 나와야 해요." 이씨는 고개를 갸우뚱했습니다. "그럼 메타데이터에 카테고리를 여러 개 넣어야 하나?
아니면 문서마다 모든 레벨을 다 저장해야 하나?" 박시니어 씨가 지나가다 이씨의 혼잣말을 듣고 멈춰 섰습니다. "계층적 필터링을 구현해야겠네.
폴더 구조처럼 만들면 돼." 그렇다면 계층적 필터링이란 정확히 무엇일까요? 쉽게 비유하자면, 계층적 필터링은 마치 회사 조직도와 같습니다.
CEO 아래에 본부장이 있고, 본부장 아래에 팀장이 있습니다. "경영본부" 문서를 검색하면 그 아래 "전략팀", "인사팀", "재무팀" 문서도 함께 나옵니다.
이처럼 계층적 필터링은 상하 관계를 유지하며 체계적으로 문서를 분류하는 역할을 담당합니다. 단순 필터링만 사용하면 어땠을까요?
각 문서에 모든 레벨의 카테고리를 중복해서 저장해야 했습니다. "재무팀 보고서"라면 "경영", "재무" 두 개의 카테고리를 모두 기록해야 했습니다.
문제는 카테고리가 바뀌면 모든 문서를 수정해야 한다는 점이었습니다. 더 큰 문제는 카테고리 레벨이 많아질수록 관리가 기하급수적으로 복잡해진다는 것이었습니다.
바로 이런 문제를 해결하기 위해 계층적 필터링이 등장했습니다. 계층적 필터링을 사용하면 카테고리 구조를 한 곳에서 관리할 수 있습니다.
또한 상위 카테고리만 선택해도 하위 항목까지 자동으로 검색됩니다. 무엇보다 새로운 하위 카테고리를 추가해도 기존 문서를 수정할 필요가 없다는 큰 이점이 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 hierarchy 딕셔너리에서 카테고리 계층 구조를 정의합니다.
키가 상위 카테고리이고 값이 하위 카테고리 리스트입니다. build_category_filter 메서드는 선택한 카테고리와 그 하위 카테고리들을 모두 리스트로 만듭니다.
마지막으로 filter_documents가 이 리스트에 포함된 카테고리를 가진 문서만 걸러냅니다. 실제 현업에서는 어떻게 활용할까요?
전자상거래 사이트의 상품 분류가 대표적인 예입니다. "패션" 대분류 안에 "남성", "여성", "아동" 중분류가 있고, "남성" 안에는 "상의", "하의", "아우터" 소분류가 있습니다.
고객이 "패션"을 선택하면 모든 하위 카테고리 상품이 검색됩니다. 법률 문서 시스템에서도 활용됩니다.
"민법" 아래에 "총칙", "물권", "채권"이 있고, "물권" 아래에는 "소유권", "점유권", "지상권"이 있습니다. 변호사가 "물권" 관련 판례를 검색하면 모든 하위 항목의 문서가 함께 조회됩니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 계층을 너무 깊게 만드는 것입니다.
대분류-중분류-소분류-세분류-상세분류까지 5단계, 6단계로 만들면 사용자가 원하는 카테고리를 찾기 어려워집니다. 일반적으로 3단계 이내로 유지하는 것이 좋습니다.
또 하나 주의할 점은 순환 참조입니다. "A 카테고리" 아래에 "B"가 있고, "B" 아래에 다시 "A"가 있으면 무한 루프에 빠집니다.
계층 구조를 설계할 때 반드시 트리 형태를 유지해야 합니다. 다시 이씨의 이야기로 돌아가 봅시다.
이씨는 계층 구조를 설정 파일로 분리하여 관리하기로 했습니다. 기획팀에서 카테고리를 추가하거나 수정할 때 코드 변경 없이 설정만 바꾸면 되도록 만들었습니다.
계층적 필터링을 제대로 구현하면 대규모 문서 시스템도 체계적으로 관리할 수 있습니다. 여러분도 프로젝트에 적용해 보세요.
실전 팁
💡 - 계층 구조는 설정 파일이나 데이터베이스로 관리하여 유연성을 높이세요
- 계층 깊이는 3단계 이내로 유지하는 것이 사용자 경험에 좋습니다
- include_subcategories 옵션을 제공하여 사용자가 선택하게 하세요
4. 실습 문서 속성 기반 필터링
이씨는 이론은 이해했지만 실제 코드로 구현하려니 막막했습니다. "부서별로 필터링하는 건 알겠는데, 보안 등급이나 프로젝트명 같은 복잡한 조건은 어떻게 처리하지?" 여러 속성을 조합한 필터링이 필요했습니다.
문서 속성 기반 필터링은 부서, 보안 등급, 프로젝트명 같은 다양한 속성을 조합하여 검색하는 방법입니다. AND, OR 같은 논리 연산을 사용하여 복잡한 조건도 표현할 수 있습니다.
실무에서 가장 많이 사용되는 패턴입니다.
다음 코드를 살펴봅시다.
from typing import List, Dict, Any, Optional
from enum import Enum
class FilterOperator(Enum):
EQUALS = "equals"
IN = "in"
GREATER_THAN = "gt"
LESS_THAN = "lt"
class AttributeFilter:
def __init__(self):
self.filters = []
def add_filter(self, field: str, operator: FilterOperator, value: Any):
# 필터 조건 추가
self.filters.append({
"field": field,
"operator": operator,
"value": value
})
return self # 체이닝을 위해 self 반환
def apply(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
# 모든 필터 조건을 AND로 결합하여 적용
filtered = documents
for filter_condition in self.filters:
filtered = self._apply_single_filter(filtered, filter_condition)
return filtered
def _apply_single_filter(self, documents: List[Dict[str, Any]], condition: Dict[str, Any]):
field = condition["field"]
operator = condition["operator"]
value = condition["value"]
result = []
for doc in documents:
doc_value = doc.get("metadata", {}).get(field)
if operator == FilterOperator.EQUALS and doc_value == value:
result.append(doc)
elif operator == FilterOperator.IN and doc_value in value:
result.append(doc)
elif operator == FilterOperator.GREATER_THAN and doc_value > value:
result.append(doc)
elif operator == FilterOperator.LESS_THAN and doc_value < value:
result.append(doc)
return result
# 사용 예시
filter = AttributeFilter()
filter.add_filter("department", FilterOperator.IN, ["sales", "marketing"]) \
.add_filter("security_level", FilterOperator.LESS_THAN, 3) \
.add_filter("project", FilterOperator.EQUALS, "ProjectX")
이씨는 실제 프로젝트에 필터링을 적용하려다가 벽에 부딪혔습니다. 사용자들이 원하는 조건이 단순하지 않았습니다.
"영업팀이나 마케팅팀 문서 중에서 보안 등급 2 이하이고 ProjectX에 속한 문서만 찾아줘." 최주니어 씨가 옆에서 코드를 보다가 물었습니다. "이씨, 이거 조건이 세 개나 되는데 어떻게 처리해요?" 이씨는 머리를 긁적이며 대답했습니다.
"if문을 여러 개 중첩해야 하나? 근데 조건이 늘어나면 코드가 엄청 복잡해질 것 같은데..." 박시니어 씨가 두 사람의 대화를 듣고 화이트보드를 꺼냈습니다.
"속성 기반 필터를 만들어야 해. 조건을 객체로 관리하면 확장하기 쉬워." 그렇다면 속성 기반 필터링이란 무엇일까요?
쉽게 비유하자면, 속성 기반 필터링은 마치 온라인 쇼핑몰의 상세 검색과 같습니다. 브랜드, 가격대, 색상, 사이즈를 각각 선택하면 모든 조건을 만족하는 상품만 보여줍니다.
각 조건은 독립적으로 관리되지만 최종 결과는 모든 조건을 만족해야 합니다. 이처럼 속성 기반 필터링은 여러 조건을 체계적으로 결합하는 역할을 담당합니다.
조건을 하드코딩하면 어땠을까요? 각 조건 조합마다 별도의 함수를 만들어야 했습니다.
"부서와 보안 등급" 조합, "부서와 프로젝트" 조합, "세 가지 모두" 조합 등 경우의 수가 기하급수적으로 늘어났습니다. 코드 중복이 심해지고 유지보수가 악몽이 되었습니다.
더 큰 문제는 새로운 속성이 추가될 때마다 모든 함수를 수정해야 한다는 것이었습니다. 바로 이런 문제를 해결하기 위해 속성 기반 필터링이 등장했습니다.
속성 기반 필터링을 사용하면 조건을 동적으로 추가할 수 있습니다. 또한 각 조건의 로직이 독립적이라 테스트와 디버깅이 쉽습니다.
무엇보다 새로운 연산자나 속성을 추가할 때 기존 코드를 거의 수정하지 않아도 된다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 FilterOperator 열거형에서 지원하는 연산자를 정의합니다. EQUALS는 정확히 일치, IN은 리스트에 포함, GREATER_THAN과 LESS_THAN은 범위 비교입니다.
add_filter 메서드는 필터 조건을 리스트에 추가하고 self를 반환하여 메서드 체이닝이 가능하게 합니다. apply 메서드는 모든 조건을 순차적으로 적용하여 AND 연산을 구현합니다.
실제 현업에서는 어떻게 활용할까요? 인사 시스템을 예로 들어봅시다.
"개발팀이고 경력 3년 이상이며 Python 스킬을 가진 직원"을 검색할 때 이 패턴을 사용합니다. 각 조건을 독립적으로 추가하고, 시스템이 알아서 AND 연산으로 결합합니다.
부동산 검색 서비스에서도 활용됩니다. "강남구이고 매매가 10억 이하이며 방 3개 이상인 아파트"처럼 복잡한 조건도 쉽게 표현할 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 모든 조건을 AND로만 처리하는 것입니다.
실제로는 OR 연산도 필요한 경우가 많습니다. "영업팀 또는 마케팅팀" 같은 조건을 위해서는 IN 연산자를 활용하거나 별도의 OR 로직을 추가해야 합니다.
또 하나 주의할 점은 성능입니다. 조건이 많아질수록 필터링 시간이 길어집니다.
가능하면 선택도가 낮은 조건을 먼저 적용하여 초반에 문서 수를 크게 줄이는 것이 좋습니다. 다시 이씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 이씨는 AttributeFilter 클래스를 구현했습니다. 사용자가 여러 조건을 선택하면 동적으로 필터를 생성하여 적용하도록 만들었습니다.
속성 기반 필터링을 제대로 구현하면 복잡한 검색 요구사항도 깔끔하게 처리할 수 있습니다. 여러분도 프로젝트에 적용해 보세요.
실전 팁
💡 - 메서드 체이닝을 활용하면 코드가 직관적이고 읽기 쉬워집니다
- 선택도가 낮은 필터를 먼저 적용하여 성능을 최적화하세요
- OR 연산이 필요하면 IN 연산자를 활용하거나 별도 메서드를 추가하세요
5. 실습 시간 범위 검색
마지막 요구사항이 남았습니다. "최근 3개월 문서만 보고 싶어요", "2023년 1월부터 6월까지 작성된 보고서를 찾아주세요" 같은 시간 기반 검색이었습니다.
이씨는 날짜를 어떻게 비교해야 할지 고민에 빠졌습니다.
시간 범위 검색은 문서 작성일이나 수정일을 기준으로 특정 기간의 문서를 찾는 방법입니다. 최근 N일, 특정 날짜 범위, 분기별 검색 같은 다양한 패턴을 지원합니다.
날짜를 올바른 타입으로 저장하고 비교하는 것이 핵심입니다.
다음 코드를 살펴봅시다.
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
class DateRangeFilter:
@staticmethod
def last_n_days(documents: List[Dict[str, Any]], days: int, date_field: str = "created_at") -> List[Dict[str, Any]]:
# 최근 N일 문서 검색
cutoff_date = datetime.now() - timedelta(days=days)
filtered = []
for doc in documents:
doc_date = doc.get("metadata", {}).get(date_field)
if doc_date and doc_date >= cutoff_date:
filtered.append(doc)
return filtered
@staticmethod
def date_range(documents: List[Dict[str, Any]],
start_date: datetime,
end_date: datetime,
date_field: str = "created_at") -> List[Dict[str, Any]]:
# 특정 날짜 범위 문서 검색
filtered = []
for doc in documents:
doc_date = doc.get("metadata", {}).get(date_field)
if doc_date and start_date <= doc_date <= end_date:
filtered.append(doc)
return filtered
@staticmethod
def quarter(documents: List[Dict[str, Any]], year: int, quarter: int, date_field: str = "created_at") -> List[Dict[str, Any]]:
# 분기별 문서 검색 (1분기: 1-3월, 2분기: 4-6월 등)
quarter_starts = {
1: (1, 1), 2: (4, 1), 3: (7, 1), 4: (10, 1)
}
quarter_ends = {
1: (3, 31), 2: (6, 30), 3: (9, 30), 4: (12, 31)
}
start_month, start_day = quarter_starts[quarter]
end_month, end_day = quarter_ends[quarter]
start_date = datetime(year, start_month, start_day)
end_date = datetime(year, end_month, end_day, 23, 59, 59)
return DateRangeFilter.date_range(documents, start_date, end_date, date_field)
# 사용 예시
# 최근 30일 문서
recent_docs = DateRangeFilter.last_n_days(documents, 30)
# 2023년 1분기 문서
q1_docs = DateRangeFilter.quarter(documents, 2023, 1)
# 특정 날짜 범위
start = datetime(2023, 1, 1)
end = datetime(2023, 6, 30)
range_docs = DateRangeFilter.date_range(documents, start, end)
프로젝트가 거의 끝나가던 어느 날, 김매니저 씨가 이씨를 찾아왔습니다. "검색 기능 잘 쓰고 있어요.
그런데 최근 문서만 보고 싶을 때가 많거든요. 시간으로도 필터링할 수 있나요?" 이씨는 자신 있게 대답했습니다.
"물론이죠! 메타데이터에 날짜를 넣으면...
어? 그런데 날짜를 어떻게 저장하고 비교하지?" 최주니어 씨도 끼어들었습니다.
"저도 그게 궁금했어요. 문자열로 저장하면 '2023-12-01'이 '2023-03-15'보다 크다고 나오는데 이게 맞나요?" 박시니어 씨가 웃으며 말했습니다.
"날짜는 반드시 datetime 객체로 저장해야 해. 문자열 비교는 정확하지 않아." 그렇다면 시간 범위 검색이란 무엇일까요?
쉽게 비유하자면, 시간 범위 검색은 마치 일기장에서 특정 기간의 일기를 찾는 것과 같습니다. "지난달에 쓴 일기만 보기", "여름방학 기간 일기만 모아보기" 같은 요청을 처리합니다.
날짜를 숫자로 변환해서 크기를 비교하는 것이 핵심입니다. 이처럼 시간 범위 검색은 시간의 순서와 간격을 정확하게 계산하는 역할을 담당합니다.
날짜를 문자열로 저장하면 어땠을까요? "2023-12-01"과 "2023-03-15"를 문자열로 비교하면 사전순으로 비교됩니다.
"12"가 "03"보다 앞서므로 12월이 3월보다 이전이라고 잘못 판단합니다. 또한 "최근 30일" 같은 상대적 기간 계산이 불가능했습니다.
더 큰 문제는 시간대나 일광절약시간 같은 복잡한 상황을 처리할 수 없다는 것이었습니다. 바로 이런 문제를 해결하기 위해 datetime 기반 시간 범위 검색이 등장했습니다.
datetime을 사용하면 날짜 계산이 정확해집니다. 또한 "30일 전" 같은 상대적 기간을 쉽게 계산할 수 있습니다.
무엇보다 분기, 연도 같은 다양한 단위로 검색할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 last_n_days 메서드는 현재 시각에서 N일을 뺀 cutoff_date를 계산합니다. 그 다음 각 문서의 날짜가 cutoff_date보다 크거나 같은지 비교합니다.
date_range 메서드는 두 날짜 사이에 있는지 확인하고, quarter 메서드는 분기의 시작일과 종료일을 계산하여 date_range를 호출합니다. 실제 현업에서는 어떻게 활용할까요?
뉴스 서비스를 예로 들어봅시다. "최근 7일 뉴스"를 보여줄 때 last_n_days를 사용합니다.
사용자가 "지난주", "이번 달", "올해" 같은 버튼을 클릭하면 해당 기간의 기사만 필터링됩니다. 재무 보고 시스템에서도 필수적입니다.
"2023년 2분기 매출 보고서"를 검색할 때 quarter 메서드를 활용합니다. 경영진이 분기별 실적을 비교 분석할 때 매우 유용합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 시간대를 고려하지 않는 것입니다.
datetime.now()는 서버의 로컬 시간을 반환하므로, 글로벌 서비스라면 UTC를 사용하고 사용자 시간대로 변환해야 합니다. 또 하나 주의할 점은 성능입니다.
날짜 필드에 인덱스를 걸어두지 않으면 대량의 문서를 검색할 때 매우 느려집니다. 벡터 데이터베이스를 사용한다면 메타데이터 인덱싱 기능을 꼭 활성화하세요.
다시 이씨의 이야기로 돌아가 봅시다. 이씨는 모든 문서의 created_at 필드를 datetime 객체로 변경했습니다.
그리고 사용자 인터페이스에 "최근 7일", "이번 달", "지난 분기" 버튼을 추가했습니다. 김매니저 씨가 새로운 검색 기능을 써보고는 감탄했습니다.
"우와, 이제 정말 편하네요! 최근 문서만 딱 볼 수 있어서 좋아요." 시간 범위 검색을 제대로 구현하면 사용자 경험이 크게 향상됩니다.
여러분도 프로젝트에 적용해 보세요.
실전 팁
💡 - 날짜는 반드시 datetime 객체로 저장하고, 문자열은 파싱하여 사용하세요
- 글로벌 서비스라면 UTC를 기준으로 저장하고 표시할 때만 로컬 시간대로 변환하세요
- 날짜 필드에 인덱스를 걸어 검색 성능을 최적화하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
ReAct 패턴 마스터 완벽 가이드
LLM이 생각하고 행동하는 ReAct 패턴을 처음부터 끝까지 배웁니다. Thought-Action-Observation 루프로 똑똑한 에이전트를 만들고, 실전 예제로 웹 검색과 계산을 결합한 강력한 AI 시스템을 구축합니다.
AI 에이전트의 모든 것 - 개념부터 실습까지
AI 에이전트란 무엇일까요? 단순한 LLM 호출과 어떻게 다를까요? 초급 개발자를 위해 에이전트의 핵심 개념부터 실제 구현까지 이북처럼 술술 읽히는 스타일로 설명합니다.
프로덕션 RAG 시스템 완벽 가이드
검색 증강 생성(RAG) 시스템을 실제 서비스로 배포하기 위한 확장성, 비용 최적화, 모니터링 전략을 다룹니다. AWS/GCP 배포 실습과 대시보드 구축까지 프로덕션 환경의 모든 것을 담았습니다.
RAG 캐싱 전략 완벽 가이드
RAG 시스템의 성능을 획기적으로 개선하는 캐싱 전략을 배웁니다. 쿼리 캐싱부터 임베딩 캐싱, Redis 통합까지 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
실시간으로 답변하는 RAG 시스템 만들기
사용자가 질문하면 즉시 답변이 스트리밍되는 RAG 시스템을 구축하는 방법을 배웁니다. 실시간 응답 생성부터 청크별 스트리밍, 사용자 경험 최적화까지 실무에서 바로 적용할 수 있는 완전한 가이드입니다.