
RAG(검색 증강 생성) 시스템에서 사용자마다 볼 수 있는 문서가 다르다면, 권한 필터링은 벡터 유사도 검색과 동시에 걸어야 합니다. 검색 결과를 다 받아온 뒤 나중에 걸러내는 방식은 결과 개수 부족과 정보 유출이라는 두 가지 문제를 동시에 안고 갑니다. 이 글에서는 벡터 DB 종류별로 메타데이터 필터를 어느 시점에 적용해야 하는지, 실제 코드와 함께 정리해 보겠습니다.
사내 문서, 고객사별 매뉴얼, 부서별 보고서를 하나의 벡터 인덱스에 담아 놓고 챗봇을 붙이는 팀이라면 이 문제를 이미 겪었을 겁니다. 권한 없는 문서 조각이 답변에 섞여 나오는 순간, RAG 시스템은 보안 사고 원인이 됩니다.
사후 필터링만으로는 검색 결과가 비어버립니다
가장 쉬운 방식은 벡터 검색으로 top-5나 top-10을 먼저 뽑고, 애플리케이션 코드에서 사용자 권한과 대조해 걸러내는 것입니다. 구현이 단순해서 초기 프로토타입에서 흔히 씁니다.
문제는 필터링 이후 남는 문서 개수를 예측할 수 없다는 점입니다. 예를 들어 상위 5개 중 3개가 다른 부서 문서라면, LLM에 전달할 컨텍스트는 2개로 줄어듭니다. 조직 규모가 커서 문서 대비 열람 가능 비율이 낮은 사용자(신입, 외부 협력사 계정)일수록 이 현상이 심해지고, 심한 경우 답변에 쓸 문서가 0개가 됩니다.
이 문제는 벡터 검색 자체가 권한을 모른 채 유사도 순서만 매기기 때문에 생깁니다. 순서상 상위권이 전부 비허용 문서인 경우, 사후 필터는 구조적으로 빈 결과를 낼 수밖에 없습니다.
벡터 DB별 메타데이터 필터 지원 방식 비교

주요 벡터 DB는 대부분 검색 요청 안에 메타데이터 조건을 함께 넣는 방식(사전 필터링)을 지원합니다. 다만 내부 구현 방식이 달라서 필터 선택도가 높을 때(허용 문서 비율이 아주 낮을 때) 동작이 갈립니다.
| 벡터 DB | 필터링 시점 | 특징 |
|---|---|---|
| Pinecone | 쿼리와 동시에 메타데이터 필터 전달 | 서버리스 인덱스 기준 필터 조건을 함께 지정, 공식 문서에서 필터 문법($eq, $in 등)을 제공 |
| Qdrant | HNSW 그래프 탐색 중 페이로드 필터 적용 | 필터 통과 포인트가 적으면 근사 탐색 대신 정확 탐색으로 자동 전환하는 옵션 존재 |
| Weaviate | where 필터를 벡터 검색과 결합 |
필터 대상 프로퍼티에 인덱스를 걸어두는 것을 권장 |
| pgvector | SQL WHERE 절 + Postgres RLS 결합 |
데이터베이스 자체 권한 체계(Row-Level Security)를 그대로 재사용 가능 |
이 중 pgvector는 별도 필터 로직을 애플리케이션에 짤 필요 없이, Postgres의 Row-Level Security 정책으로 “이 role은 이 row만 본다”를 선언하면 벡터 검색 쿼리에도 그대로 적용된다는 점이 다릅니다. 이미 RLS를 쓰는 조직이라면 권한 필터를 새로 설계할 필요가 없습니다.
접근 제어 모델, 메타데이터 스키마에 어떻게 매핑하나요
메타데이터 필터를 걸려면 먼저 접근 제어 방식을 정해야 합니다. 역할 기반으로 갈지, 속성 기반으로 갈지에 따라 청크마다 붙이는 필드가 달라집니다.
역할 기반 접근 제어(RBAC)를 쓴다면 청크마다 allowed_roles: ["finance", "hr"] 같은 배열 필드를 붙이고, 검색 시 사용자가 속한 role과 교집합이 있는지 확인합니다. RBAC 자체는 오래된 모델로 위키백과의 역할 기반 접근 제어 문서에 개념과 변형이 정리돼 있습니다.
속성 기반(ABAC)으로 갈 경우 부서, 문서 민감도, 프로젝트 코드 같은 여러 속성을 조합해 조건을 만듭니다. 규칙이 복잡해질수록 필터 조건도 길어지므로, OWASP의 Access Control Cheat Sheet에서 권장하는 “기본은 거부, 명시적으로 허용된 것만 통과” 원칙을 필터 조건 설계에 그대로 가져오는 것이 안전합니다. 즉 allowed_roles가 비어 있는 청크는 검색 자체에서 제외하도록 기본값을 설계해야 합니다.
Qdrant로 짜보는 권한 필터 검색 코드
Qdrant는 페이로드(메타데이터) 필드에 조건을 걸어 검색 시점에 바로 걸러낼 수 있습니다. 아래는 allowed_groups 페이로드 필드에 사용자가 속한 그룹이 포함된 문서만 검색하는 예시입니다.

from qdrant_client import QdrantClient
from qdrant_client.models import Filter, FieldCondition, MatchValue
client = QdrantClient(url="http://localhost:6333")
user_group = "team-finance"
results = client.search(
collection_name="documents",
query_vector=query_embedding,
query_filter=Filter(
must=[
FieldCondition(
key="allowed_groups",
match=MatchValue(value=user_group)
)
]
),
limit=5,
)
for r in results:
print(r.payload["title"], r.score)
이 코드는 벡터 유사도 계산과 페이로드 필터를 같은 요청 안에서 처리하기 때문에, “top-5를 먼저 뽑고 나중에 거른다”는 사후 필터링의 결과 부족 문제가 생기지 않습니다. 정확한 필터 문법과 인덱싱 옵션(allowed_groups 필드에 페이로드 인덱스를 미리 만들어야 검색 속도가 유지됩니다)은 Qdrant 공식 필터링 문서에서 최신 문법을 확인하는 것이 안전합니다. 클라이언트 버전에 따라 search 메서드 대신 query_points를 권장하는 경우도 있으니, 실제 적용 전에는 사용 중인 클라이언트 버전의 문서를 함께 봐야 합니다.
인덱스를 아예 나누는 전략은 언제 써야 할까요
권한 그룹 수가 적고(예: 부서 5~10개) 그룹 간 문서 중복이 거의 없다면, 메타데이터 필터 대신 그룹별로 인덱스나 네임스페이스 자체를 나누는 방법도 있습니다. Pinecone의 네임스페이스, Qdrant의 컬렉션 분리가 여기 해당합니다.
이 방식은 필터 조건을 매 쿼리마다 계산할 필요가 없어 지연 시간이 안정적이라는 장점이 있습니다. 반대로 한 사용자가 여러 그룹에 걸쳐 있어 문서를 여러 인덱스에서 동시에 가져와야 하는 조직(매트릭스 조직, 다부서 겸직이 흔한 회사)이라면, 인덱스마다 검색을 따로 날리고 결과를 병합·재정렬하는 로직이 따로 필요해 오히려 복잡도가 늘어납니다. 이런 경우엔 메타데이터 필터 방식이 관리하기 더 쉽습니다.
권한 필터, 어느 단계에 넣을지 정하는 기준
정리하면 사용자마다 문서 접근 범위가 다른 RAG 시스템에서는 벡터 검색 요청 안에 메타데이터 필터를 함께 넣는 사전 필터링이 기본값이 되어야 합니다. 사후 필터링은 프로토타입 단계에서만 쓰고, 운영 단계로 넘어가기 전에 반드시 사전 필터로 전환하는 것이 안전합니다.
권한 그룹 구조가 단순하고 겹치지 않는다면 인덱스 분리도 검토할 만하지만, 조직 대부분은 겸직과 프로젝트별 권한이 섞여 있어 메타데이터 필터 쪽이 관리 부담이 적습니다. 지금 쓰는 벡터 DB의 필터 문법부터 공식 문서에서 확인하고, 청크를 인덱싱할 때 allowed_roles나 allowed_groups 필드를 처음부터 채워 넣는 것이 다음 단계로 적당합니다.
