
Azure AI Search에서 벡터 인덱스 구축하기: HNSW, 프로필 및 RAG 검색
요약
Azure AI Search를 활용하여 RAG 시스템의 핵심인 벡터 인덱스를 구축하는 방법을 설명합니다. HNSW와 KNN 알고리즘의 차이를 이해하고, 의미 기반 검색을 통해 언어 모델에 전달할 컨텍스트를 추출하는 실습 과정을 다룹니다.
핵심 포인트
- RAG 시스템의 품질은 검색 레이어의 정확도에 크게 의존함
- 벡터 검색은 키워드 매칭을 넘어 의미적 유사성을 기반으로 검색 수행
- HNSW는 대규모 데이터에 적합한 빠르고 권장되는 ANN 알고리즘임
- KNN은 정확도가 높지만 속도가 느려 소규모 데이터셋에 적합함
이 글에서는 Azure AI Search에서 벡터 검색 (Vector Search)이 어떻게 작동하는지, 그리고 이를 검색 증강 생성 (Retrieval-Augmented Generation, RAG) 시스템의 검색 레이어 (Retrieval Layer)로 어떻게 사용하는지 이해해 보겠습니다. 이 글은 소프트웨어 엔지니어를 대상으로 합니다. 우리는 이론에만 머물지 않을 것입니다. 여러분의 컴퓨터에서 직접 실행하며 단계별로 따라 할 수 있는 작고 작동 가능한 예제를 구축할 것입니다.
마지막에는 사용자의 질문을 받아 벡터 유사도 (Vector Similarity)를 사용하여 가장 관련 있는 텍스트를 찾고, 언어 모델 (Language Model)에 전달할 수 있는 컨텍스트 (Context)를 준비하는 작은 문서 검색 서비스를 갖게 될 것입니다.
Azure AI Search는 이전에 Azure Cognitive Search라고 불렸음을 참고해 주세요. 서비스 이름이 변경되었지만, 많은 오래된 문서와 코드 샘플은 여전히 이전 이름을 사용하고 있습니다. 개념은 동일합니다.
시작해 봅시다.
RAG 시스템이란 무엇인가, 요약하자면
RAG 시스템은 두 가지 주요 부분으로 구성됩니다. 첫 번째 부분은 검색 (Retrieval)입니다. 사용자가 질문을 하면, 우리는 지식 베이스 (Knowledge Base)를 검색하여 가장 관련 있는 텍스트 조각들을 추출합니다. 두 번째 부분은 생성 (Generation)입니다. 우리는 이 텍스트 조각들을 질문과 함께 언어 모델에 전달하여, 모델이 실제적이고 근거 있는 정보를 사용하여 답변할 수 있도록 합니다.
RAG 시스템의 품질은 검색 부분에 크게 의존합니다. 만약 검색이 잘못된 텍스트를 반환하면, 언어 모델은 잘못되거나 모호한 답변을 생성할 것입니다. 이것이 벡터 검색이 중요한 이유입니다. 벡터 검색을 통해 키워드 매칭 (Keyword Matching)뿐만 아니라 의미 (Meaning)를 기반으로 텍스트를 검색할 수 있습니다.
왜 벡터 검색이 필요한가
전통적인 키워드 검색 (Keyword Search)은 정확한 단어를 일치시킵니다. 만약 사용자가 "car"를 검색했는데 문서에 "automobile"이라고 적혀 있다면, 키워드 검색은 이를 놓칠 수 있습니다. 벡터 검색은 이 문제를 해결합니다.
벡터 검색 (Vector search)에서는 먼저 각 텍스트 조각을 임베딩 (Embedding)이라고 불리는 숫자 리스트로 변환합니다. 의미가 유사한 텍스트는 벡터 공간 (Vector space)에서 서로 가까운 임베딩을 생성합니다. 사용자가 질문을 하면, 질문 또한 임베딩으로 변환한 다음, 저장된 임베딩 중 질문과 가장 가까운 것을 찾습니다. 이를 최근접 이웃 검색 (Nearest neighbour search)이라고 합니다.
Azure AI Search는 인덱스 (Index) 내에 벡터 필드 (Vector field)를 정의할 수 있게 함으로써 이를 지원합니다. 이 필드에 임베딩을 저장하면, 서비스가 수많은 벡터를 빠르게 검색할 수 있는 구조를 구축합니다.
Azure AI Search가 벡터 검색을 수행하는 방법
Azure AI Search는 벡터 검색을 위해 두 가지 알고리즘을 지원합니다. 첫 번째는 근사 최근접 이웃 (Approximate nearest neighbour, ANN) 알고리즘인 HNSW (Hierarchical Navigable Small World)입니다. 이는 빠르며 대부분의 프로덕션 워크로드 (Production workloads)에 권장되는 선택지입니다. 두 번째는 쿼리 (Query)를 저장된 모든 벡터와 비교하는 전수 KNN (Exhaustive KNN)입니다. 이는 정확하지만 더 느리며, 주로 작은 데이터 세트나 근사 방식의 정확도를 측정하는 데 유용합니다.
다음 표는 두 알고리즘을 비교한 것입니다.
| 알고리즘 | 유형 | 대규모 데이터에서의 속도 | 재현율 (Recall) | 가장 적합한 용도 |
|---|---|---|---|---|
| HNSW | 근사 (ANN) | 빠름 | 매우 높으며 조정 가능 | 대부분의 프로덕션 워크로드 및 대규모 인덱스 |
| 전수 KNN (Exhaustive KNN) | 정확 (Exact) | 대규모 데이터에서 느림 | 정확함 (100 퍼센트) | 작은 데이터 세트 또는 실제 정답(Ground-truth) 정확도 측정 |
Azure AI Search에서 알고리즘은 필드에 직접 연결되지 않습니다. 대신, 알고리즘 리스트와 프로필 (Profiles) 리스트를 포함하는 벡터 검색 구성 (Vector search configuration)을 정의합니다. 프로필은 선택된 알고리즘에 이름을 부여합니다. 그런 다음 각 벡터 필드는 이름을 통해 프로필을 참조합니다. 이러한 추가 계층 덕분에 여러 필드에 걸쳐 동일한 알고리즘 설정을 쉽게 재사용할 수 있습니다.
벡터 필드 자체는 단정밀도 부동 소수점 수(single-precision floating point numbers)의 컬렉션인 Collection(Edm.Single) 타입을 사용합니다. 이 필드에는 차원(dimensions) 수를 설정해야 하며, 이 숫자는 사용 중인 임베딩 모델(embedding model)의 출력 크기와 일치해야 합니다.
RAG 시스템의 아키텍처 (The architecture of our RAG system)
코드를 작성하기 전에 전체적인 그림을 살펴보겠습니다. 여기에는 두 가지 경로가 있습니다. 인제스션 경로(ingestion path)는 한 번(또는 데이터가 변경될 때마다) 실행되어 인덱스를 채웁니다. 쿼리 경로(query path)는 사용자가 질문을 할 때마다 실행됩니다.
한 가지 중요한 세부 사항을 유의하십시오. 두 경로 모두 동일한 임베딩 모델(embedding model)을 사용해야 합니다. 만약 문서에는 한 모델을 사용하여 임베딩하고, 쿼리에는 다른 모델을 사용한다면 벡터들이 서로 비교될 수 없으며 검색 결과는 무의미해질 것입니다.
데이터 모델 (The data model)
RAG를 위해 문서를 저장할 때, 보통 전체 문서를 하나의 레코드로 저장하지 않습니다. 문서를 더 작은 청크(chunks)로 분할하는데, 이는 더 작은 청크가 더 집중된 검색(retrieval)을 가능하게 하고 언어 모델(language model) 프롬프트에 더 잘 맞기 때문입니다. 각 청크는 Azure AI Search 인덱스 내의 하나의 문서가 되며, 각 문서는 청크 텍스트, 해당 임베딩(embedding), 그리고 결과를 소스로 추적하기 위한 일부 메타데이터(metadata)를 보유합니다.
다음 엔티티 관계 다이어그램(entity relationship diagram)은 소스 문서가 청크와 어떻게 연관되는지, 그리고 각 청크가 인덱스 내에서 어떻게 하나의 검색 문서로 저장되는지를 보여줍니다.
우리의 작은 예제에서는 문서가 이미 짧기 때문에 각 문서를 단일 청크로 취급하겠습니다. 실제 시스템에서는 청킹(chunking) 단계를 추가하겠지만, 인덱스의 구조는 동일하게 유지됩니다.
실습 튜토리얼 (Hands-on tutorial)
이제 시스템을 구축해 보겠습니다. 여러분이 Azure 구독, Python (버전 3.9 이상), 그리고 Python 스크립트를 실행하는 데 대한 기본적인 익숙함이 있다고 가정하겠습니다.
실습을 따라오기 위해 Azure OpenAI 배포를 설정할 필요가 없도록
| Parameter | 설정 위치 | 제어하는 내용 | 기본값 및 범위 |
|---|---|---|---|
m | 알고리즘 구성 | 그래프에서 각 노드가 유지하는 양방향 링크 수 | 기본값 4, 범위 4~10 |
| ... | |||
| 기본값은 합리적인 시작점입니다. 나중에 검색 재현율(recall) 및 지연 시간(latency) 요구 사항에 따라 조정할 수 있습니다. |
4단계: 문서 임베딩 및 업로드
이제 임베딩 모델을 로드하고, 작은 지식 기반을 생성하며, 임베딩을 생성하고, 문서를 인덱스에 업로드합니다.
from azure.search.documents import SearchClient
from sentence_transformers import SentenceTransformer
...
이 스크립트를 실행하면 다섯 개의 작은 문서가 인덱스에 업로드됩니다. 실제 프로젝트에서는 파일이나 데이터베이스에서 문서를 읽어와 청크(chunk)로 분할하고, 동일한 upload_documents 메서드를 사용하여 수천 또는 수백만 개의 문서를 보통 배치(batch) 단위로 업로드하게 됩니다.
5단계: 쿼리 벡터를 사용한 검색
이제 검색 함수를 작성합니다. 사용자 질문을 동일한 모델로 임베딩하고, 벡터 쿼리를 실행합니다. VectorizedQuery 객체는 Azure AI Search에 어떤 벡터로 검색할지, 몇 개의 이웃(neighbour)을 반환할지, 그리고 어떤 필드를 대상으로 검색할지를 알려줍니다.
from azure.search.documents.models import VectorizedQuery
def search(question, k=3):
...
질문에서
def build_prompt(question, k=3):
hits = search(question, k=k)
context = "\n\n".join(f"- {hit['text']}" for hit in hits)
...
출력값은 질문과 가장 관련성이 높은 텍스트 조각들을 포함하는 프롬프트 (prompt)입니다. 이제 이 프롬프트를 언어 모델 (language model)로 보내면, 모델은 근거에 기반한 (grounded) 답변을 생성할 것입니다. 이것이 Azure AI Search가 벡터 저장소 (vector store) 및 검색기 (retriever) 역할을 수행하는 전체 검색 증강 생성 (RAG, retrieval-augmented generation) 흐름입니다.
대신 Azure OpenAI 임베딩 (embeddings) 사용하기
이 튜토리얼에서는 로컬 머신에서 직접 임베딩 (embeddings)을 생성했습니다. 많은 Azure 프로젝트에서 팀들은 대신 text-embedding-3-small과 같은 Azure OpenAI 임베딩 모델을 사용합니다. 변경 사항은 미미합니다. Azure OpenAI 클라이언트 (client)를 호출하여 임베딩을 생성하고, 인덱스 차원 (index dimensions)을 모델에 맞춰 설정하기만 하면 됩니다 (예를 들어, text-embedding-ada-002의 경우 1536). 나머지 인덱스 및 쿼리 (query) 코드는 동일하게 유지됩니다.
from openai import AzureOpenAI
client = AzureOpenAI(
...
Azure AI Search는 통합 벡터화 (integrated vectorization)라고 불리는 기능도 지원합니다. 이 기능을 사용하면 서비스 자체에서 인덱싱 (indexing) 시점과 쿼리 (query) 시점에 Azure OpenAI 모델을 호출하여 텍스트를 벡터 (vectors)로 변환하므로, 사용자가 직접 코드에서 임베딩을 생성할 필요가 없습니다. 이는 더 큰 규모의 파이프라인 (pipelines)에 편리하지만, 위에서 보여준 기본적인 흐름만으로도 벡터 검색 (vector search)이 어떻게 작동하는지 이해하기에는 충분합니다.
몇 가지 실무적인 참고 사항
이 작은 예제를 실제 시스템으로 확장할 때는 다음 사항들을 유념하시기 바랍니다. 임베딩 모델 (embedding model)을 신중하게 선택하십시오. 모델이 벡터의 차원 (dimension)과 검색 (retrieval) 품질을 결정하기 때문입니다. 긴 문서를 집중된 구절 (passages)로 나눌 수 있도록 청킹 (chunking) 단계를 추가하십시오. 키워드 매칭 (keyword matching)과 의미 기반 매칭 (meaning-based matching)이 모두 필요한 경우, 벡터 쿼리와 함께 search_text에 값을 전달하여 하이브리드 검색 (hybrid search)을 사용하십시오. 그러면 Azure AI Search가 키워드 (BM25) 결과와 벡터 결과를 결합할 것입니다. 더욱 나은 순위 산정을 위해, 언어 모델 (language model)을 사용하여 상위 결과의 순위를 재조정하는 시맨틱 랭커 (semantic ranker)를 활성화할 수 있습니다. 마지막으로, 벡터 인덱스 (vector indexes)는 메모리를 소비하므로 인덱스 크기를 모니터링하십시오. 저장 공간이 문제가 된다면 벡터 압축 (vector compression) 및 이진 벡터 (binary vectors)를 검토해 보시기 바랍니다.
결론
우리는 벡터 검색 (vector search)이 무엇인지, 왜 RAG 시스템에 이것이 필요한지, 그리고 Azure AI Search가 벡터 필드 (vector fields), HNSW 및 전수 KNN (exhaustive KNN) 알고리즘, 그리고 프로필 기반 설정 (profile-based configuration)을 통해 이를 어떻게 제공하는지 살펴보았습니다. 그런 다음 작지만 완전한 예제를 구축했습니다. 검색 서비스 (search service)를 생성하고, 벡터 인덱스 (vector index)를 정의하며, 몇 개의 문서를 임베딩하여 업로드하고, 의미에 따라 검색한 뒤, RAG 프롬프트 (RAG prompt)를 구성했습니다. 또한 Azure OpenAI 임베딩 (embeddings)으로 전환하는 방법도 확인했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기