Published on

Pinecone 메타데이터 필터 느림, 인덱스 설계로 5배 개선

Authors

서빙 중인 RAG 시스템에서 Pinecone 검색이 갑자기 느려졌다면, 대부분은 임베딩 차원이나 topK 때문이 아니라 메타데이터 필터의 선택도(selectivity)와 인덱스/샤딩 구조에서 병목이 생깁니다. 특히 테넌트(고객) 단위 격리, 문서 상태(활성/폐기), 접근 제어(ACL) 같은 필터를 붙이기 시작하면, 필터가 “검색 범위를 좁혀주는” 게 아니라 오히려 “검색 전에 확인해야 할 조건”으로 작동하면서 지연이 커질 수 있습니다.

이 글에서는 Pinecone의 메타데이터 필터가 느려지는 전형적인 패턴을 설명하고, **인덱스 설계(네임스페이스/분리 인덱스/메타데이터 모델링)**로 체감 5배까지 개선하는 접근을 실무 관점에서 정리합니다.

증상: 필터를 붙이면 p95가 급등한다

대표적인 증상은 다음과 같습니다.

  • 필터 없이 벡터 검색은 빠른데, filter를 추가하면 p95/p99가 급격히 증가
  • 특정 테넌트나 특정 상태 값에서만 느림(데이터 분포 편향)
  • 트래픽이 늘 때만 느려짐(필터 평가 비용이 누적)
  • 타임아웃 또는 게이트웨이 계층에서 503/504가 증가

서버 전체 관점에서 보면 “검색 API가 느려짐”은 곧 다운스트림 타임아웃으로 이어집니다. 애플리케이션 레벨에서는 타임아웃/재시도 정책이 성능을 더 악화시키기도 하니, 이런 상황이라면 먼저 타임아웃 설계도 함께 점검하세요. 예를 들어 gRPC를 사용한다면 데드라인 초과 원인을 함께 보는 것이 도움이 됩니다: Go gRPC 데드라인 초과 원인 7가지와 해결

왜 느려지나: “필터가 인덱스”가 아닐 때

Pinecone의 메타데이터 필터는 SQL의 B-Tree 인덱스처럼 “필터 조건만으로 미리 후보를 빠르게 좁히는” 방식과는 다르게 동작할 수 있습니다(서비스/인덱스 타입/설정에 따라 내부 구현은 달라질 수 있지만, 실무에서 체감되는 병목 패턴은 유사합니다).

핵심은 다음입니다.

  • 선택도가 낮은 필터(예: status = "active"가 95%)는 사실상 거의 안 좁혀짐
  • 조건이 많거나 배열/중첩 구조를 쓰면 필터 평가 비용이 커짐
  • 테넌트 격리 필터를 모든 요청에 붙이면, 사실상 “항상 필터 평가”가 발생
  • 데이터가 한 인덱스/한 네임스페이스에 과밀하게 섞이면, 검색 후보가 커져 필터 비용이 커짐

즉, 필터가 느린 이유는 “필터가 나빠서”가 아니라, 필터로 해결하려는 문제(격리/파티셔닝/라이프사이클)가 원래 인덱스 설계로 풀어야 하는 성격이기 때문입니다.

목표: 필터를 ‘최소화’하고, 구조로 ‘범위를 먼저 줄이기’

5배 개선을 만들려면 한 가지 트릭보다, 아래 3가지를 함께 적용하는 쪽이 성공 확률이 높습니다.

  1. 네임스페이스로 강한 파티셔닝(테넌트/도메인 단위)
  2. 분리 인덱스(또는 복제 인덱스)로 워크로드 분리
  3. 메타데이터 모델 단순화 + 선택도 높은 필터만 남기기

아래에서 각각을 실무적으로 풀어보겠습니다.

1) 테넌트 필터는 네임스페이스로 바꾼다

가장 흔한 안티패턴은 다음과 같습니다.

  • 모든 벡터를 한 인덱스에 넣고
  • 모든 쿼리에 tenantId 필터를 붙여 격리

이 방식은 구현이 쉽지만, 트래픽이 늘면 모든 요청이 필터 평가 비용을 지불합니다. 테넌트 격리는 가능하면 namespace로 옮겨 “검색 대상 자체를 분리”하는 게 유리합니다.

개선 전(필터 기반)

import { Pinecone } from "@pinecone-database/pinecone";

const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const index = pc.index("docs");

async function queryWithTenantFilter(tenantId: string, vector: number[]) {
  const res = await index.query({
    vector,
    topK: 20,
    includeMetadata: true,
    filter: {
      tenantId: { "$eq": tenantId },
      status: { "$eq": "active" }
    }
  });
  return res;
}

개선 후(네임스페이스 기반)

import { Pinecone } from "@pinecone-database/pinecone";

const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const index = pc.index("docs");

async function queryWithNamespace(tenantId: string, vector: number[]) {
  const res = await index.namespace(tenantId).query({
    vector,
    topK: 20,
    includeMetadata: true,
    // 필터는 정말 필요한 것만 남긴다
    filter: {
      status: { "$eq": "active" }
    }
  });
  return res;
}

왜 빨라지나

  • “테넌트 후보군”을 필터로 걸러내는 게 아니라, 애초에 검색 대상이 테넌트 단위로 분리됨
  • 선택도가 낮은 tenantId 필터 평가가 사라짐
  • 테넌트별 데이터 분포 편향이 완화되어 tail latency가 줄어드는 경우가 많음

주의점

  • 테넌트 수가 매우 많을 때 네임스페이스 운영 전략(생성/삭제/마이그레이션)을 정해야 함
  • 테넌트 간 공유 문서가 있다면 “공유 네임스페이스 + 테넌트 네임스페이스” 형태로 설계가 필요

2) 필터가 무거운 워크로드는 인덱스를 분리한다

필터가 느려지는 또 다른 패턴은 “한 인덱스에서 서로 다른 성격의 쿼리”를 동시에 처리하는 경우입니다.

예:

  • 실시간 챗봇 검색(짧은 문서, 높은 QPS, 낮은 지연 목표)
  • 백오피스 분석/재색인 검증(긴 문서, 낮은 QPS, 필터 복잡)

이 둘을 한 인덱스에서 처리하면, 무거운 필터/큰 topK/대량 includeMetadata 요청이 지연을 끌어올릴 수 있습니다.

실무 설계: “서빙 인덱스”와 “운영 인덱스” 분리

  • docs-serving: 챗봇/검색 API 전용
    • 필터 최소화
    • includeMetadata는 꼭 필요한 필드만(가능하다면 결과 ID만 받고 별도 저장소에서 조회)
  • docs-ops: 운영/관리/검증 전용
    • 복잡한 필터 허용
    • 느려도 되는 대신, 서빙 지연에 영향 없음

데이터를 이중으로 쓰는 비용이 들지만, “필터 느림”이 곧바로 매출/사용자 경험에 영향을 주는 서비스라면 분리 비용이 더 저렴한 경우가 많습니다.

업서트 파이프라인 예시(이중 인덱스)

import { Pinecone } from "@pinecone-database/pinecone";

const pc = new Pinecone({ apiKey: process.env.PINECONE_API_KEY! });
const serving = pc.index("docs-serving");
const ops = pc.index("docs-ops");

type Vec = {
  id: string;
  values: number[];
  metadata: Record<string, any>;
};

async function upsertBoth(namespace: string, vectors: Vec[]) {
  // 서빙 인덱스에는 최소 메타데이터만
  const servingVectors = vectors.map(v => ({
    id: v.id,
    values: v.values,
    metadata: {
      status: v.metadata.status,
      docType: v.metadata.docType,
      // 서빙에 필요한 것만 남긴다
    }
  }));

  await Promise.all([
    serving.namespace(namespace).upsert(servingVectors),
    ops.namespace(namespace).upsert(vectors)
  ]);
}

3) 메타데이터 모델링: “필터 친화적인 형태”로 단순화

필터 성능은 메타데이터의 “표현 방식”에도 영향을 받습니다. 다음은 자주 느려지는 모델입니다.

  • 중첩 객체가 깊음
  • 배열이 크고, in/contains류 조건을 자주 씀
  • 문자열 정규화가 안 되어 값 종류가 지나치게 많음(카디널리티 폭발)

권장 패턴

  • 자주 필터링하는 값은 flat key로 끌어올리기
  • 값은 가능한 한 짧고 정규화된 문자열 또는 숫자 코드
  • 선택도가 낮은 필터는 제거하거나, 더 강한 파티셔닝(네임스페이스/인덱스)로 대체

예를 들어 ACL을 이렇게 저장하면 느려지기 쉽습니다.

{
  "acl": {
    "users": ["u1", "u2", "u3"],
    "groups": ["g1", "g2"]
  }
}

대안은 “검색 단계에서 ACL을 완벽히 해결하려고 하지 말고”,

  • 1차 검색: Pinecone에서 후보를 좁힘(테넌트/문서 상태 정도)
  • 2차 필터: 애플리케이션에서 ACL 체크 후 부족하면 topK를 늘려 재시도

처럼 후처리로 옮기는 전략이 현실적입니다.

후처리 예시(ACL 애플리케이션 필터)

type Match = { id: string; score: number; metadata?: any };

function canAccess(userId: string, m: Match) {
  const allowedUsers: string[] = m.metadata?.allowedUsers ?? [];
  return allowedUsers.includes(userId);
}

async function queryWithPostFilter(index: any, ns: string, vector: number[], userId: string) {
  const res = await index.namespace(ns).query({
    vector,
    topK: 50,
    includeMetadata: true,
    filter: { status: { "$eq": "active" } }
  });

  const filtered = (res.matches ?? []).filter((m: Match) => canAccess(userId, m));
  return filtered.slice(0, 10);
}

이 방식은 “Pinecone에서 ACL까지 완벽히”보다 구현이 번거로울 수 있지만, 필터로 tail latency가 폭발하는 상황에서는 전체 시스템 성능이 훨씬 안정적입니다.

4) 선택도 높은 필터만 남기는 체크리스트

필터를 남길지 말지 판단할 때는 다음 질문이 유용합니다.

  • 이 조건이 없으면 후보가 얼마나 늘어나는가?
  • 이 조건이 전체 데이터의 몇 %를 제거하는가?
  • 이 조건은 매 요청마다 동일하게 붙는가?

경험적으로 “매 요청마다 항상 붙는 조건”은 필터로 남기기보다 구조로 해결하는 편이 낫습니다.

예:

  • tenantId는 네임스페이스로
  • env = "prod" 같은 건 인덱스 분리 또는 별도 프로젝트/인덱스
  • status = "active"는 데이터 분포에 따라 남길지 결정(대부분 active면 의미 없음)

5) 성능 측정: p50보다 p95/p99를 보라

“5배 개선”은 보통 평균이 아니라 **꼬리 지연(tail latency)**에서 발생합니다. 필터 병목은 특정 분포/특정 테넌트에서만 튀는 경우가 많기 때문입니다.

권장 측정 방법:

  • 동일한 쿼리 벡터 세트를 준비해 A/B 비교
  • p50/p95/p99를 분리해서 기록
  • 테넌트별, 네임스페이스별로 분해해 관찰

애플리케이션이 Next.js 기반이라면, 검색 API 지연이 곧 TTFB로 전파될 수 있습니다. RSC/서버 컴포넌트 환경에서 체감 성능이 무너질 때의 진단 관점은 다음 글도 참고할 만합니다: Next.js 14 App Router TTFB 폭증 잡는 RSC 튜닝

6) 운영 팁: 타임아웃·재시도는 “증폭기”가 될 수 있다

필터가 느려지면 클라이언트는 재시도를 걸고, 재시도는 다시 Pinecone 부하를 올려 더 느려지는 악순환이 생깁니다. 특히 gRPC나 서버 간 호출에서 데드라인이 짧게 잡혀 있으면 “거의 성공할 요청”도 실패로 돌아가며 재시도를 유발합니다.

  • 데드라인을 무작정 늘리기보다, 필터/구조를 먼저 개선
  • 재시도는 지수 백오프 + 최대 시도 제한
  • 서빙 경로와 운영 경로 분리(앞서 말한 인덱스 분리와 궁합이 좋음)

관련해서 타임아웃이 표면 증상으로 나타나는 상황은 다음 글의 진단 흐름도 도움이 됩니다: Go gRPC deadline exceeded(코드 4) 원인·해결

정리: 5배 개선을 만드는 “인덱스 설계” 우선순위

Pinecone 메타데이터 필터가 느릴 때, 쿼리 문법을 조금 바꾸는 수준으로는 한계가 있습니다. 아래 우선순위로 접근하면 재현성과 효과가 좋습니다.

  1. 테넌트 격리 필터를 네임스페이스로 이동
  2. 서빙/운영 워크로드를 인덱스로 분리(또는 복제)
  3. 메타데이터를 flat하게 정규화하고, 선택도 낮은 필터를 제거
  4. ACL 같은 복잡 조건은 후처리로 옮기고, topK로 보정
  5. p95/p99 중심으로 측정하고, 재시도/타임아웃이 병목을 증폭시키지 않게 조정

이 흐름대로 설계를 바꾸면, “필터가 붙으면 느리다”는 문제를 쿼리 트릭이 아니라 구조적 개선으로 해결할 수 있고, 실제로 p95 기준 5배 수준의 개선도 충분히 현실적인 목표가 됩니다.