Post

AInfo 챗봇 구조 #4 - Vector 검색 구조 분석

AInfo 프로젝트에서 벡터 검색을 담당하는 VectorRetriever 클래스의 전체 구조와 검색 흐름, 멀티 컬렉션 구성 방식에 대해 정리합니다.

AInfo 챗봇 구조 #4 - Vector 검색 구조 분석

벡터 검색이 중요한 이유

AInfo는 사용자 질문에 대해 RAG 기반의 정보 검색을 수행합니다. 이때 핵심이 되는 건 얼마나 적절한 문서를 빠르게 유사도 기반으로 찾을 수 있는가입니다. 이를 위해 다음 구조의 검색기를 구성했습니다:

  • LangChain + ChromaDB를 활용한 고속 벡터 검색
  • 여러 컬렉션을 동시에 검색 (멀티 DB)
  • 메타데이터 필터링 기반 사용자 조건 매칭 지원
  • Markdown 형식으로 결과 포맷팅

VectorRetriever 구조

1
2
3
class VectorRetriever:
    def search(self, query, k=5, filters=None, collection_names=None): ...
    def format_docs(self, docs): ...

VectorRetriever는 AInfo의 RAG 검색을 담당하는 싱글톤 클래스입니다. 주요 특징은 다음과 같습니다:

구성 요소설명
__new__싱글톤으로 클래스 생성 제한
_initialize임베딩 모델 및 DB 디렉토리 초기화
_register_collectionsChroma 컬렉션 등록 (5개)
search()유사도 검색 실행 + 메타데이터 필터링
format_docs()Markdown 형식으로 결과 포맷

1. 컬렉션 등록 방식

1
2
3
4
collection_names = [
  "gov24_service_list", "gov24_service_detail",
  "youth_policy_list", "employment_programs", "pdf_sections",
]
  • Chroma의 각 컬렉션은 공공서비스 출처별로 분리되어 있음
  • 초기 실행 시 한 번만 등록되어 메모리에서 재사용됨
1
2
3
4
5
6
7
8
return {
    name: Chroma(
        collection_name=name,
        embedding_function=self.embedding_model,
        persist_directory=self.DB_DIR,
    )
    for name in collection_names
}

한 번 로딩한 컬렉션은 서버 실행 중 계속 재사용 가능함으로 빠름

2. 멀티 컬렉션 검색 흐름

1
2
def search(self, query, k=5, filters=None, collection_names=None):
    ...
  • 기본적으로 모든 등록된 컬렉션에 대해 검색
  • collection_names를 넘기면 원하는 DB만 검색 가능
  • 각 컬렉션에서 최대 k개의 문서를 가져옴
  • _metadata_match() 함수로 필터 조건이 있는 경우 체크

예시:

1
2
3
4
5
results = retriever.search(
    query="서울 청년 지원금",
    k=5,
    filters={"지역": "서울", "나이": "29"},
)

컬렉션마다 비슷한 정책이 있을 수 있으므로 다수 컬렉션 병렬 검색 구조가 효율적입니다.

3. 메타데이터 기반 필터링

1
2
3
4
def _metadata_match(self, metadata, filters):
    for key, value in filters.items():
        if key not in metadata or value not in str(metadata[key]):
            return False
  • 검색 결과 문서가 조건에 부합하지 않으면 제거
  • 예: {"지역": "서울"}metadata["지역"]에 “서울”이 포함되지 않으면 제외

결과 품질이 올라가는 핵심 로직이며, RAG에 들어가는 문서 수를 줄여 비용도 절감됩니다.

4. 결과 포맷팅: Markdown 변환

검색된 결과는 그대로 LLM에 넘기기보다, 사용자에게 보여줄 수 있는 형태로 변환합니다.

1
2
def format_docs(self, docs):
    # [(컬렉션 이름, Document)] → Markdown string

예시 출력:

1
2
3
4
5
6
7
8
**[서울 청년수당]**
- 내용: 만 19~34세 청년에게 월 50만원을 6개월 지원합니다.
- 링크: [바로가기](https://youth.seoul.go.kr)


**[취업날개 서비스]**
- 내용: 서울시 청년 대상 면접 정장 무료 대여
- 링크: 해당 서비스는 URL이 제공되지 않습니다.
  • 제목은 다양한 필드에서 자동 추출 (서비스명, title, plcyNm 등)
  • URL이 없는 경우도 자연스럽게 안내

5. 활용 예시: CrewAI Tool에서도 사용

VectorRetriever는 LangChain 체인뿐 아니라, CrewAI Tool에서도 직접 사용됩니다.

1
2
3
4
5
class RagSearchTool(BaseTool):
    def _run(...):
        retriever = VectorRetriever()
        docs = retriever.search(...)
        return retriever.format_docs(docs)
  • Tool 내부에서 search → format_docs 호출
  • 크루 에이전트가 그대로 사용자에게 결과를 설명하는 데 사용됨

마무리

AInfo 프로젝트에서는 다양한 출처의 문서를 검색해야 하기에, 싱글톤 + 멀티 컬렉션 + 메타 필터링 + 포맷 변환까지 책임지는 VectorRetriever 구조는 핵심 모듈입니다.

This post is licensed under CC BY 4.0 by the author.