Azure AI Search는 보유한 벡터가 몇 개인지로 크기가 정해지지 않는다. 티어로 정해진다. vector index는 해당 SKU에서 vector search에 예약된 메모리에 묶이고, Serverless 모델에서는 인덱스당 300 MB라는 하드 리밋을 넘는 순간 indexing job이 그대로 실패한다. 기능 비교가 아니라 이 속성 하나가, 이 회사의 retrieval 레이어가 Azure AI Search에 속하는지 전용 벡터 데이터베이스에 속하는지를 결정한다. 6부는 반복되는 질문을 싸게 만들었다. 이번 편은 훨씬 더 흔한 경우, 모델이 본 적 없는 사실을 필요로 하는 경우를 다루고, 1부의 리테일 지식 검색이 마침내 만들어진다.

retrieval이 게이트웨이 기준으로 앉는 위치

어떤 비교보다 먼저 정리할 가치가 있는 지점이 하나 있는데, 시리즈의 전제를 온전하게 지키는 것이 바로 이것이기 때문이다. retrieval은 게이트웨이 트래픽이 아니다. 애플리케이션은 search index를 직접 조회하고, 돌아온 것으로 프롬프트를 조립해, 그 프롬프트를 다른 completion과 똑같이 게이트웨이를 통해 보낸다. 게이트웨이는 검색된 컨텍스트가 더하는 token을 계량하는데, 이것이 정확히 원하는 가시성이다. retrieval은 대개 프롬프트 크기가 조용히 세 배가 되는 지점이기 때문이다.

게이트웨이에 닿는 유일한 부분은 embedding이다. 쿼리와 문서의 embeddings를 생성하는 것은 모델 호출이므로, 다른 모든 것과 같은 문을 지나며, 같은 quota와 같은 귀속을 받는다. 이는 또한 6부가 semantic caching을 위해 만든 embeddings 배포가 retrieval이 원하는 배포와 같을 가능성이 크다는 뜻이고, 그 용량은 이제 두 주인을 섬긴다.

실제로 결정을 가르는 한도들

기능 체크리스트만 보면 Azure AI Search와 전용 벡터 스토어는 서로 바꿔 쓸 수 있어 보인다. 서비스 한도는 그렇지 않다.

  • vector index 크기는 데이터가 아니라 SKU의 함수다. Dedicated 가격 모델에서는 해당 티어에서 vector search에 예약된 메모리에 묶이고, Serverless에서는 인덱스당 300 MB, 전체 index storage의 대략 30퍼센트이며, 성능 저하가 아니라 indexing 실패를 만드는 하드 리밋이다.
  • 벡터 한도는 서비스가 언제 생성되었는지에 달려 있다. 더 높은 벡터 quota는 추가 용량이 있는 리전에서 2024년 4월 이후 생성된 서비스에 적용된다. 지원 리전의 오래된 서비스는 이를 얻으려면 명시적 업그레이드가 필요할 수 있는데, 이는 설정 토글이 아니라 마이그레이션 문제다.
  • 용량은 replica 곱하기 partition이다. Basic은 2024년 4월 3일 이후 생성된 서비스에서 partition 3개와 replica 3개, 즉 search unit 9개를 지원한다. 그보다 오래된 Basic 서비스는 partition 1개로 제한된다. S1부터 S3까지는 각각 12개까지 간다.
  • SLA는 replica 수다. 쿼리 워크로드에는 2개 이상, 쿼리와 indexing에는 3개 이상이다. partition 수는 SLA 고려 사항이 전혀 아니고, 스토리지 때문에 확장하면서 가용성도 따라온다고 가정했던 사람들을 놀라게 한다.
  • index 수는 티어별로 상한이 있다. Basic은 5개 또는 15개, S1은 50개, S2와 S3는 200개다. 테넌트마다 index를 하나씩 주는 플랫폼은 스토리지보다 이 천장에 훨씬 먼저 부딪히는데, index-per-tenant가 기본 본능이다.
  • vector field당 최대 4096 dimensions는 모든 티어에 적용되고, 넉넉하지만 누군가 더 큰 embedding 모델로 표준화하기 전에 알아 둘 가치가 있다.

이것들을 함께 읽으면 구체적인 이야기가 된다. Azure AI Search는 벡터도 하는 검색 서비스로서 가격이 매겨지고 확장된다. 벡터 코퍼스가 지배적인 비용 동인이고 쿼리 볼륨과 무관하게 자란다면, 메모리를 붙들기 위해 search unit 비용을 내는 셈이다. 그것이 전용 벡터 데이터베이스가 이기는 조건이고, 아키텍처 취향이 아니라 확인할 수 있는 정량적 조건이다.

Azure AI Search는 주고 벡터 스토어는 주지 않는 것

반대쪽 추도 실재하고, 기업 지식 베이스에서는 종종 결정적이다.

요청 하나로 끝나는 hybrid search. 단일 쿼리가 search 파라미터와 vectorQueries를 함께 싣고, full-text와 벡터 retrieval을 병렬로 실행한 뒤, Reciprocal Rank Fusion으로 병합해 하나의 랭킹된 결과 세트를 만든다. 텍스트 랭킹은 BM25를, 벡터 랭킹은 HNSW 또는 exhaustive KNN을 쓴다. 이것이 중요한 이유는 제품 문서가 모델 번호, 오류 코드, 부품 번호 같은 정확한 토큰으로 가득하고, 그런 토큰은 lexical search가 안정적으로 찾아내는 반면 embeddings는 흐리게 만들기 때문이다.

{
  "search": "device not connecting after the firmware update",
  "vectorQueries": [
    { "kind": "vector", "vector": [ ... ], "k": 50,
      "fields": "contentVector", "exhaustive": false, "oversampling": 10 }
  ],
  "queryType": "semantic",
  "semanticConfiguration": "kb-semantic",
  "top": 10
}

필터링, 패싯, 정렬, scoring profile이 같은 요청에 적용되므로, 문서 수준 인가(이 테넌트, 이 제품 라인, 이 언어)는 별도의 index가 아니라 필터다. indexer를 통한 integrated vectorization은 Blob Storage, Cosmos DB, Azure SQL, OneLake에서 끌어와 수집 시점에 embed하고, 이는 플랫폼 팀이 아니었으면 떠안았을 파이프라인 하나를 통째로 없앤다. 그리고 index는 Microsoft Foundry의 agent 시나리오에서 knowledge source로 붙일 수 있다.

랭킹 품질 이야기 안에 용량 디테일 하나가 숨어 있다. semantic ranker는 큐로 스로틀링된다. search unit당 최대 동시 요청 수가 Basic에서 2, S1에서 3, S2 이상에서 4이고, 그 뒤에 크기가 제한된 큐가 있다. 큐를 넘으면 요청은 거부되고 재시도해야 한다. 이를 올리는 방법은 search unit을 늘리거나 support 요청을 넣는 것이다. 따라서 모든 쿼리에 semantic ranking을 켜는 retrieval 레이어는 방금 자신의 동시성 천장을 검색 티어의 함수로 만든 셈이고, Basic에서 그 천장은 작다.

테스트로 서술한 결정

취향이 아니다. 순서대로 세 가지 확인이다.

  • 코퍼스에 lexical과 벡터 retrieval이 함께 필요한가? 정확한 식별자가 중요하다면, 그리고 제품 문서에서는 언제나 중요하다면, 한 서비스 안의 hybrid와 semantic ranking은 큰 가치가 있고, 순수 벡터 스토어 위에 이를 다시 구현한다는 것은 inverted index까지 함께 운영한다는 뜻이다.
  • 벡터 풋프린트가 쿼리 볼륨과 무관하게 자라는가? 그렇다면 이를 담는 데 필요한 search unit을 모델링해서, 스토리지 기준으로 산정한 벡터 데이터베이스와 비교하라. 이것은 산수이고, 대개 여기서 답이 뒤집힌다.
  • 논리적 코퍼스가 몇 개인가? index-per-tenant는 티어별 index 천장에 금방 부딪힌다. 대안은 tenant filter를 건 index 하나이고, 이는 클라이언트가 아니라 쿼리 레이어에서 강제해야 하는 보안 통제다.

이 플랫폼의 답은 Azure AI Search다. 리테일 지식 베이스가 정확한 모델 번호가 중요한 수십 기가바이트의 제품 문서이고, 관리형 서비스 하나를 더 두는 운영 비용이 같은 AKS 클러스터에서 벡터 스토어를 직접 돌리는 비용보다 낮기 때문이다. 수억 개의 벡터를 담고 lexical 요구가 없는 agent memory 스토어라면 답은 반대일 것이고, 시리즈가 그렇지 않은 척한다면 틀린 것이다.

테넌시, 그리고 보안 통제인 필터

tenant filter를 건 index 하나가 index 수 천장 아래에서 올바른 기본값이고, 이는 인가 결정을 쿼리 안에 넣는다. 규칙은 6부의 vary-by와 같다. 필터 값은 요청 파라미터가 아니라 검증된 token에서 나온다. 자신의 tenant ID를 스스로 전달하는 애플리케이션은 버그 하나면 다른 누군가의 것을 전달한다.

retrieval은 API Management를 지나지 않으므로, 강제 지점은 게이트웨이 정책이 아니라 애플리케이션 또는 control plane이다. 이는 one-door 모델의 진짜 틈이고, 얼버무리기보다 이름을 붙여 두는 편이 낫다. 게이트웨이는 모델 접근을 통제하고, 검색 서비스는 문서 접근을 통제하며, 둘은 테넌트가 누구인지에 합의해야 하는 서로 다른 두 control plane이다. 둘 다 같은 Entra ID claim에서 파생시키는 것이 이들이 어긋나지 않게 지켜 준다.

지켜봐야 할 실패 모드

  • 저하 대신 크기 한도에서 실패하는 indexing. Serverless에서 인덱스당 벡터 상한을 넘으면 작업이 실패한다. vector index 용량은 bulk load 도중 발견할 것이 아니라 계획 단계의 입력값이다.
  • 오래된 벡터 quota를 가진 오래된 검색 서비스. 2024년 4월 이전에, 지금은 더 많이 제공하는 리전에서 생성되었고, 누군가 업그레이드할 때까지 조용히 제한된다.
  • partition을 확장하며 가용성을 기대하는 것. partition은 스토리지다. SLA는 replica를 센다. 쿼리에는 2개, indexing이 함께 돌면 3개다.
  • 부하 아래에서 스로틀링되는 semantic ranker. 동시성은 search unit당이고 낮은 티어에서는 작다. 증상은 느려진 쿼리가 아니라 피크에 거부되는 쿼리다.
  • 티어 천장을 향해 가는 index-per-tenant. 처음 열두 테넌트까지는 훌륭하게 동작하다가 멈추고, 그 시점에 filtered index로의 마이그레이션은 데이터 프로젝트다.
  • 프롬프트 크기를 폭발시키는 검색된 컨텍스트. 게이트웨이가 이를 계량하므로, 5부의 숫자에서 코드 변경으로는 설명되지 않는 token 증가로 나타난다. 총지출만이 아니라 테넌트별 요청당 token에 알림을 걸라.

8부가 물려받는 것

애플리케이션이 직접 호출하는 retrieval 레이어, cache와 공유하는 embeddings 배포, 그리고 이전보다 상당히 커진 프롬프트다. 여기서 다음 편의 질문이 나온다. 애플리케이션에 retrieval, tool calling, 여러 번의 모델 hop이 필요해지면, orchestration 프레임워크는 이 게이트웨이 위에 속하는가, 아니면 프레임워크가 게이트웨이와 싸우기 시작하는가.

다음으로 읽을 글

이것을 대규모로 운영할 때의 인프라와 플랫폼 쪽 이야기는 ercan.cloud에, 허브는 ercanermis.com에 있다.

참고 자료