Published on

Pinecone 메타데이터 필터가 느릴 때 인덱스 설계

Authors

서빙 중인 RAG에서 topK 는 작고 임베딩 차원도 무난한데, 유독 메타데이터 필터를 켜는 순간 쿼리가 느려지는 경우가 있습니다. Pinecone는 벡터 검색 엔진이지만, 실제 서비스에서는 tenantId, projectId, language, docType, accessLevel, createdAt 같은 조건으로 “검색 가능한 후보군”을 먼저 좁히는 일이 훨씬 더 중요해집니다.

이 글에서는 Pinecone 메타데이터 필터가 느려지는 전형적인 패턴을 짚고, 이를 피하기 위한 인덱스 설계(네임스페이스, 파티셔닝, 메타데이터 스키마, 쿼리 형태) 를 구체적으로 정리합니다.

왜 메타데이터 필터가 병목이 되나

Pinecone 쿼리는 크게 두 단계로 생각하면 이해가 쉽습니다.

  1. 벡터 유사도 검색으로 후보를 찾는다
  2. 메타데이터 조건에 맞는 결과만 남긴다

문제는 2번이 “싸게” 끝나지 않을 때 발생합니다. 특히 다음 상황에서 필터가 병목이 되기 쉽습니다.

  • 필터 선택도(selectivity)가 낮다: 예를 들어 {"lang": "ko"} 처럼 전체의 70%가 해당되면, 사실상 거의 안 줄어듭니다.
  • 고카디널리티(high-cardinality) 필드를 잘못 사용: userId 같은 수백만 값이 가능한 필드를 단독 필터로 쓰면, 내부적으로 효율적인 후보 축소가 어렵거나 분포가 나빠질 수 있습니다.
  • 배열 필드에 과도한 태그를 넣는다: tags 에 수십 개를 넣고 $in 류 조건을 자주 쓰면 필터 비용이 커집니다.
  • 시간 범위/다중 조건이 많다: createdAt 범위 + tenantId + docType 같은 복합 조건은 설계가 나쁘면 후보 축소가 잘 안 됩니다.
  • 네임스페이스를 안 나눈다: 멀티테넌트인데 모든 테넌트를 한 네임스페이스에 넣고 필터로만 자르면, 쿼리마다 거대한 집합에서 필터링이 발생합니다.

핵심은 “필터가 느리다”가 아니라 필터가 충분히 후보를 줄이지 못하는 구조 인 경우가 많다는 점입니다.

1) 네임스페이스로 1차 파티셔닝하기

가장 강력한 최적화는 네임스페이스(namespace) 로 데이터를 물리적으로 분리하는 것입니다. Pinecone에서 네임스페이스는 사실상 가장 쉬운 파티셔닝 레버입니다.

멀티테넌트라면 tenantId 는 네임스페이스로

  • 나쁜 예: 한 네임스페이스에 모두 넣고 {"tenantId": "t1"} 필터로만 구분
  • 좋은 예: namespace = tenantId 로 분리하고, 그 안에서만 추가 필터 적용

이렇게 하면 검색 후보군의 기본 크기가 줄어, 필터 비용이 크게 낮아집니다.

업서트 예시 (Node.js)

import { Pinecone } from '@pinecone-database/pinecone'

const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY })
const index = pc.index(process.env.PINECONE_INDEX)

await index.namespace('tenant_t1').upsert([
  {
    id: 'doc_001#chunk_0001',
    values: embedding,
    metadata: {
      projectId: 'p1',
      lang: 'ko',
      docType: 'policy',
      createdAt: 1709251200
    }
  }
])

쿼리 예시

const res = await index.namespace('tenant_t1').query({
  vector: queryEmbedding,
  topK: 20,
  includeMetadata: true,
  filter: {
    projectId: 'p1',
    lang: 'ko'
  }
})

네임스페이스를 더 쪼개면 더 빨라지나

무조건 더 쪼갠다고 좋은 건 아닙니다.

  • 너무 잘게 쪼개면 운영이 복잡해지고, 교차 검색이 필요할 때 비용이 커집니다.
  • 일반적으로는 tenantId 정도가 가장 효과 대비 단순합니다.
  • projectId 까지 네임스페이스로 내릴지는 “한 쿼리가 여러 프로젝트를 동시에 검색하는가”에 달려 있습니다.

2) 필터는 ‘선택도 높은 조건’부터 설계한다

필터가 빠르려면 결과 후보군이 빨리 줄어야 합니다. 그래서 메타데이터 스키마를 설계할 때는 다음 질문을 먼저 해야 합니다.

  • 이 필드는 값 종류가 많나(카디널리티)
  • 한 값이 전체에서 차지하는 비율이 낮나(선택도)
  • 쿼리에서 얼마나 자주 쓰나

추천 패턴: accessScope 같은 “버킷화된” 필드

예를 들어 권한 모델이 복잡해서 allowedUserIds 배열을 메타데이터에 넣고 $in 으로 필터링하려고 하면 대개 느려집니다.

대신 다음처럼 “버킷화”를 고려합니다.

  • visibility: public / internal / private
  • accessLevel: 0..3 같은 작은 정수
  • projectId + visibility 조합으로 후보군을 줄인 뒤, 애플리케이션 레벨에서 최종 권한 체크

즉, Pinecone 메타데이터 필터는 검색 후보를 줄이는 1차 게이트 로 쓰고, 복잡한 ACL은 후단에서 처리하는 것이 안정적입니다.

3) 태그 배열(tags) 남용을 피하고, 검색용 필드를 분리한다

tags: ["a", "b", "c", ...] 같은 배열은 편하지만, 다음 요구가 생기면 급격히 비싸질 수 있습니다.

  • $in 조건이 길어짐
  • 태그가 많아져서 한 벡터의 메타데이터가 비대해짐
  • 태그 조합이 다양해져 선택도가 떨어짐

대안 1: “대표 태그”만 필터에 쓰기

예: topic = "billing" 처럼 1개 축으로 먼저 줄이고, 상세 태그는 결과에서 후처리

대안 2: 태그를 미리 정규화한 필드로 분해

예: docType, domain, lang, region 처럼 쿼리에서 자주 쓰는 축만 필드로 고정

이 접근은 RDB 인덱스 튜닝에서 “복합 인덱스를 어떤 컬럼 순서로 잡을까”와 유사합니다. 필터 축을 줄이고, 자주 쓰는 축을 고정하면 성능이 예측 가능해집니다.

관련해서 DB 인덱스 사고방식이 익숙하지 않다면, MongoDB의 explain 기반 튜닝 글도 같이 보면 도움이 됩니다: MongoDB 느린 쿼리 - explain으로 원인 찾고 인덱스 튜닝

4) 시간 범위 필터는 “버킷 + 범위”로 단순화

createdAt 같은 범위 조건은 흔하지만, 단독으로 쓰면 선택도가 낮아질 때가 많습니다(예: 최근 90일).

추천: timeBucket 를 추가

  • timeBucket: 2026-02 같은 월 단위
  • 쿼리: timeBucket in ["2026-01", "2026-02"] + createdAt 범위

이렇게 하면 “월 단위로 큰 덩어리를 먼저 줄이고”, 그 다음에 세부 범위를 적용할 수 있습니다.

예시 메타데이터

metadata: {
  projectId: 'p1',
  lang: 'ko',
  timeBucket: '2026-02',
  createdAt: 1709251200
}

예시 필터

filter: {
  projectId: 'p1',
  timeBucket: { $in: ['2026-01', '2026-02'] },
  createdAt: { $gte: 1706745600, $lte: 1709251200 }
}

주의할 점은, 연도-월 버킷은 애플리케이션에서 쉽게 생성 가능하고, 쿼리 패턴도 단순해져서 운영 안정성이 높습니다.

5) includeMetadata 와 payload 크기를 점검한다

“필터가 느린 것처럼 보이지만 사실은 응답이 무거운” 경우도 많습니다.

  • includeMetadata: true 로 큰 텍스트(원문 chunk)까지 메타데이터에 넣어두면 응답이 커집니다.
  • 네트워크 비용과 JSON 파싱 비용이 늘어나서, 필터가 느린 것처럼 체감될 수 있습니다.

권장: 메타데이터에는 키만, 본문은 별도 저장

  • Pinecone 메타데이터: docId, chunkId, offset, title, lang 정도
  • 본문 텍스트: 오브젝트 스토리지나 DB에 저장 후, 검색 결과의 docId 로 조회

이 방식은 RAG 파이프라인에서도 일반적이며, 특히 트래픽이 늘면 차이가 크게 납니다.

6) 인덱스 설계 체크리스트 (실전용)

서비스에서 “필터가 느리다” 이슈가 나오면 아래 순서로 점검하면 재발이 줄어듭니다.

A. 데이터 파티셔닝

  • 멀티테넌트면 namespace = tenantId 인가
  • 한 쿼리가 검색하는 네임스페이스 범위가 과도하게 넓지 않은가

B. 필터 선택도

  • 가장 자주 쓰는 필터 조합에서, 후보군이 실제로 얼마나 줄어드는가
  • lang, docType 같이 분포가 쏠린 필드만으로 필터링하고 있지 않은가

C. 메타데이터 스키마

  • 배열 필드(tags, allowedUserIds)를 검색 필터로 남용하고 있지 않은가
  • 시간 범위는 timeBucket 같은 버킷 필드로 1차 축소가 가능한가

D. 응답 페이로드

  • includeMetadata 가 꼭 필요한가
  • 메타데이터에 큰 문자열(원문)을 넣고 있지 않은가

7) “필터 최적화”를 위한 추천 설계 예시

아래는 많은 RAG 서비스에서 무난하게 성능이 나오는 템플릿입니다.

네임스페이스

  • namespace: tenantId

메타데이터 필드

  • projectId: 프로젝트 단위 분리(선택도 높음)
  • docType: faq, policy, manual 등(중간)
  • lang: ko, en 등(낮을 수 있음)
  • timeBucket: YYYY-MM (선택도 개선)
  • createdAt: epoch seconds (정밀 범위)
  • docId, chunkId: 원문 조회 키

쿼리 패턴

  • 항상 projectId 를 포함
  • 시간 조건이 있으면 timeBucket 를 함께 사용
  • lang 은 가능하면 후순위(또는 프로젝트가 이미 언어별로 분리되어 있으면 생략)

8) 디버깅 팁: “쿼리 패턴”부터 로그로 고정하라

Pinecone 성능 이슈는 재현 가능한 쿼리 패턴을 먼저 고정해야 합니다.

  • 어떤 네임스페이스에서
  • 어떤 필터 조합으로
  • topK 는 얼마이고
  • 응답 크기(includeMetadata)는 어떤지

이걸 고정하지 않으면, 팀 내에서 “느리다”의 기준이 계속 바뀌고 최적화가 산으로 갑니다. RAG 체인에서 도구 호출이나 파서 실패처럼, 재현 조건을 고정하는 것이 해결의 절반입니다. 관련해서 LLM 파이프라인 안정화 관점은 다음 글도 참고할 만합니다: LangChain 도구호출 무한루프 차단 7가지

마무리: 필터를 빠르게 만드는 가장 현실적인 방법

Pinecone 메타데이터 필터가 느릴 때 “필터 문법을 바꿔서” 해결되는 경우는 제한적입니다. 대부분은 설계 문제입니다.

  • 1순위: tenantId 를 네임스페이스로 분리해 기본 후보군을 줄인다
  • 2순위: 선택도 높은 필터 축(projectId, timeBucket)을 확보한다
  • 3순위: 배열/ACL 같은 복잡한 조건은 필터에서 빼고 후단에서 처리한다
  • 4순위: includeMetadata 와 메타데이터 크기를 줄여 체감 지연을 없앤다

이 원칙대로 인덱스와 스키마를 재정렬하면, 같은 임베딩/같은 topK 여도 응답 시간이 눈에 띄게 안정화되는 경우가 많습니다.