Поиск векторов в Azure AI Search: HNSW, профили и ретривер RAG на Python
Создание векторного индекса в Azure AI Search с использованием Python: HNSW против полного KNN, профили, эмбеддинги, векторные запросы и промпты RAG, а также советы по гибридному поиску.
Источники: Building a Vector Index in Azure AI Search: HNSW, Profiles, and RAG Retrieval
Поиск информации — это та часть системы RAG, которая определяет, является ли окончательный ответ обоснованным или расплывчатым. В этом руководстве рассматривается, как работает векторный поиск в Azure AI Search, а затем создается небольшой Python-сервис, который принимает вопрос, находит наиболее близкий по смыслу текст и формирует запрос для языковой модели. Примечание к названию: ранее этот сервис назывался Azure Cognitive Search, и многие старые публикации и примеры до сих пор используют это название. Концепции остаются прежними.
Две составляющие RAG
Метод генерации с усилением поиском делится на этапы поиска информации и генерации ответа. При поступлении вопроса система ищет в базе знаний и извлекает наиболее релевантные фрагменты текста. Затем она передает эти фрагменты вместе с вопросом языковой модели, так что ответ основывается на реальном исходном тексте.
Качество генерации зависит исключительно от того, что ей предоставляется. Если система поиска возвращает неверные фрагменты текста, модель будет отвечать неправильно или уклончиво, независимо от своих способностей. Именно поэтому выбор метода поиска имеет такое большое значение.
Почему совпадение по ключевым словам недостаточно
Классический поиск сопоставляет буквальные слова. Запрос на слово «car» может пропустить документ, в котором используется термин «automobile». Векторный поиск решает эту проблему, работая с смыслом текста.
Каждый фрагмент текста преобразуется в список чисел, называемый эмбеддингом. Тексты с похожим значением находятся рядом друг от друга в векторном пространстве. При поступлении запроса он также преобразуется в эмбеддинг, и сервис находит хранящиеся векторы, наиболее близкие к нему — это задача, известная как поиск ближайшего соседа. В Azure AI Search вы объявляете поле векторов в индексе, храните там эмбеддинги, и сервис создает структуру, позволяющую быстро сканировать огромное количество векторов.
Алгоритмы и профили в Azure AI Search
Сервис предлагает два алгоритма поиска для векторов:
- HNSW (Hierarchical Navigable Small World) — это приближенный метод поиска ближайшего соседа. Он быстрый и обычно используется в производственных условиях.
Алгоритмы не привязаны напрямую к полю. Индекс содержит настройки поиска векторов с списком алгоритмов и списком профилей, причём каждый профиль соответствует определённому выбору алгоритма. Поле векторов затем обращается к профилю по имени, что позволяет использовать одинаковые настройки алгоритма в нескольких полях.
Тип поля — Collection(Edm.Single), то есть коллекция чисел с плавающей точкой одной значимости. Необходимо указать количество измерений, и это число должно совпадать с размером выходных данных модели эмбеддинга, которая используется.
Структура системы
Существуют два пути обработки. Путь ингестии выполняется один раз или каждый раз при изменении данных и заполняет индекс. Путь запросов работает при каждом вопросе. Их связывает одно правило: оба пути должны использовать одну и ту же модель эмбеддингов. Если документы эмбеддировать с помощью одной модели, а вопросы — другой, векторы не смогут быть сравнены, и результаты потеряют смысл.
Документы обычно хранятся в виде фрагментов, а не целых файлов. Меньшие фрагменты обеспечивают более точное поисковое восстановление и легче встраиваются в запрос. Каждый фрагмент становится отдельным документом для поиска, содержащим его текст, эмбеддинг и метаданные, указывающие на источник. В приведённом примере используются короткие документы, поэтому каждый из них считается отдельным фрагментом. В производственной системе был бы добавлен этап разбиения на фрагменты, но структура индекса остаётся прежней.
Практическое создание
Вам потребуется подписка Azure, Python 3.9 или новее, а также базовые навыки работы с скриптами. Эмбеддинги генерируются локально с использованием небольшой открытой модели, поэтому для выполнения задачи не требуется развертывание в Azure OpenAI. Вопрос перехода на Azure OpenAI рассматривается в конце.
Шаг 1: Создание сервиса поиска
В портале Azure создайте ресурс Azure AI Search. Для этих целей достаточен бесплатный тариф. После его создания скопируйте два значения: конечную точку сервиса в формате https://<your-service>.search.windows.net и административный ключ из раздела Keys. Рассматривайте этот ключ как секретную информацию. В реальном проекте он должен храниться в переменной окружения или Azure Key Vault, а не в исходном коде.
Шаг 2: Установка библиотек
Понадобятся два пакета: Azure Search SDK и sentence-transformers.
pip install azure-search-documents sentence-transformers
Шаг 3: Определение индекса векторов
Следующий скрипт создаёт индекс с векторным полем, затем определяет настройки поиска векторов с указанием алгоритма и профиля перед формированием индекса.
from azure.core.credentials import AzureKeyCredential
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
SearchIndex,
SimpleField,
SearchableField,
SearchField,
SearchFieldDataType,
VectorSearch,
HnswAlgorithmConfiguration,
HnswParameters,
VectorSearchProfile,
)
endpoint = "https://<your-service>.search.windows.net"
admin_key = "<your-admin-key>"
index_name = "rag-docs"
index_client = SearchIndexClient(
endpoint=endpoint,
credential=AzureKeyCredential(admin_key),
)
# 1. Define the fields. The embedding field is the vector field.
fields = [
SimpleField(name="doc_id", type=SearchFieldDataType.String, key=True, filterable=True),
SearchableField(name="text", type=SearchFieldDataType.String),
SimpleField(name="source", type=SearchFieldDataType.String, filterable=True),
SearchField(
name="embedding",
type=SearchFieldDataType.Collection(SearchFieldDataType.Single),
searchable=True,
vector_search_dimensions=384, # must match the embedding model
vector_search_profile_name="my-vector-profile",
),
]
# 2. Define the vector search configuration: one algorithm and one profile.
vector_search = VectorSearch(
algorithms=[
HnswAlgorithmConfiguration(
name="my-hnsw",
parameters=HnswParameters(
m=4,
ef_construction=400,
ef_search=500,
metric="cosine",
),
)
],
profiles=[
VectorSearchProfile(
name="my-vector-profile",
algorithm_configuration_name="my-hnsw",
)
],
)
# 3. Create (or update) the index.
index = SearchIndex(name=index_name, fields=fields, vector_search=vector_search)
index_client.create_or_update_index(index)
print("Index created:", index_name)
Некоторые аспекты этого скрипта требуют пояснения. Количество измерений составляет 384, поскольку именно столько генерирует выбранный модель вкладки. Поле векторов указывает на профиль с именем my-vector-profile, который в свою очередь указывает на алгоритм HNSW с именем my-hnsw. Метрика расстояния — косинус, что является стандартным значением для вкладок текста.
HNSW предоставляет параметры настройки, и Azure задаёт разумные значения по умолчанию. Задокументированные значения: m равно 4 — количество двунаправленных связей на узел, efConstruction равно 400 — размер списка кандидатов при построении графа, а efSearch равно 500 — размер списка кандидатов при поиске. Повышение этих значений улучшает точность восстановления данных, но увеличивает задержку и время построения, поэтому начните с значений по умолчанию и настраивайте их в соответствии с вашими целями.
Шаг 4: Вкладка и загрузка
Этот скрипт загружает модель, создаёт небольшую базу знаний из пяти коротких документов, вычисляет их эмбеддинги и загружает их в индекс.
from azure.search.documents import SearchClient
from sentence_transformers import SentenceTransformer
search_client = SearchClient(
endpoint=endpoint,
index_name=index_name,
credential=AzureKeyCredential(admin_key),
)
# Load the embedding model (produces 384-dimensional vectors)
model = SentenceTransformer("all-MiniLM-L6-v2")
# Our small knowledge base
documents = [
{"doc_id": "1", "source": "billing-faq",
"text": "You can update your payment method from the account settings page under Billing."},
{"doc_id": "2", "source": "billing-faq",
"text": "Refunds are processed within five to seven business days to the original payment method."},
{"doc_id": "3", "source": "shipping-faq",
"text": "Standard delivery takes three to five working days within the country."},
{"doc_id": "4", "source": "account-faq",
"text": "To reset your password, click the forgot password link on the login screen."},
{"doc_id": "5", "source": "shipping-faq",
"text": "International orders may take up to fourteen working days depending on customs."},
]
# Create embeddings for the text of each document
texts = [d["text"] for d in documents]
vectors = model.encode(texts)
# Attach the embedding to each document and upload
to_upload = []
for doc, vector in zip(documents, vectors):
to_upload.append({
"doc_id": doc["doc_id"],
"source": doc["source"],
"text": doc["text"],
"embedding": vector.tolist(),
})
result = search_client.upload_documents(documents=to_upload)
print("Uploaded", len(result), "documents.")
В настоящем проекте данные читаются из файлов или базы данных, текст разбивается на фрагменты, и тысячи или миллионы документов отправляются с помощью одного и того же вызова upload_documents, обычно пакетами.
Шаг 5: Поиск по векторам
При извлечении информации вопрос пользователя обрабатывается с помощью той же модели, после чего выполняется запрос по векторам. Объект VectorizedQuery указывает сервису, какой вектор использовать, сколько соседних элементов возвращать и в каком поле производить поиск.
from azure.search.documents.models import VectorizedQuery
def search(question, k=3):
# Embed the question with the same model
query_vector = model.encode([question])[0]
vector_query = VectorizedQuery(
vector=query_vector.tolist(),
k_nearest_neighbors=k,
fields="embedding",
)
results = search_client.search(
search_text=None, # pure vector search
vector_queries=[vector_query],
select=["doc_id", "source", "text"],
)
output = []
for r in results:
output.append({
"score": r["@search.score"],
"source": r["source"],
"text": r["text"],
})
return output
for item in search("how do I get my money back", k=3):
print(round(item["score"], 4), "|", item["source"], "|", item["text"])
Попробуйте сформулировать вопрос как «верните мои деньги». В ни одном из примеров документов нет этих слов, тем не менее документ о возвратах средств должен занять первое место, поскольку сравнение основано на значении, а не на ключевых словах. Именно такое поведение и требуется от системы RAG.
Шаг 6: Составление промпта
Поиск выполнен. На последнем этапе берутся найденные результаты и формируется промпт. Здесь не указывается конкретная модель, поскольку можно использовать Azure OpenAI или любого другого поставщика. Скрипт лишь готовит входные данные.
def build_prompt(question, k=3):
hits = search(question, k=k)
context = "\n\n".join(f"- {hit['text']}" for hit in hits)
prompt = (
"You are a support assistant. Use only the context below to answer "
"the question. If the answer is not in the context, say that you do "
"not have enough information.\n\n"
f"Context:\n{context}\n\n"
f"Question: {question}\n"
"Answer:"
)
return prompt
print(build_prompt("how do I get my money back"))
Результатом формирования промпта является вопрос вместе с наиболее релевантными фрагментами текста. Отправка этого промпта в модель завершает цикл с использованием технологии поиска с усиленным извлечением данных, причем Azure AI Search выполняет функции векторного хранилища и механизма извлечения информации.
Использование встроенных моделей Azure OpenAI
Многие команды в Azure используют хостинговые модели встроенных представлений, такие как text-embedding-3-small, вместо локальных версий. Изменения минимальны: необходимо вызывать клиент Azure OpenAI для создания каждого встроенного представления и устанавливать количество измерений индекса в соответствии с моделью, например, 1536 для text-embedding-ada-002. Все остальное в коде индекса и запросов остается без изменений.
from openai import AzureOpenAI
client = AzureOpenAI(
api_key="<your-azure-openai-key>",
api_version="2024-10-21",
azure_endpoint="https://<your-resource>.openai.azure.com/",
)
def get_embedding(text):
response = client.embeddings.create(
input=text,
model="text-embedding-3-small", # your deployment name
)
return response.data[0].embedding
Azure AI Search также поддерживает интегрированную векторизацию: сервис самостоятельно вызывает модель Azure OpenAI, преобразуя текст в векторы во время индексации и снова для каждого запроса. Это избавляет от необходимости писать код для генерации векторов и подходит для более крупных пайплайнов, но описанный выше базовый процесс уже достаточен для понимания принципа работы векторного поиска.
Что может пойти не так
Три ошибки являются причиной большинства разочаровывающих первых результатов. Первая — несоответствие размерностей: если индекс указывает 384, но модель генерирует 1536 чисел, загрузка терпит неудачу; решение заключается в получении информации о размере вывода модели и соответствующем установке этого значения. Вторая — использование разных моделей эмбеддингов на этапах загрузки данных и выполнения запросов, что приводит к результатам, кажущимся правдоподобными, но на самом деле случайным. Третья — изменение моделей позже без их пересоздания, поскольку все сохраненные векторы тогда относятся к пространству старой модели и требуют перегенерации и повторной загрузки.
Привычка, которая экономит время, — хранить имя модели и количество размерностей в одном значении конфигурации, которое считывают как скрипт загрузки, так и функция запросов, чтобы они не отклонялись друг от друга.
Проверка качества поиска
Прежде чем доверять системе поиска, составьте около десяти реалистичных вопросов и определите, какой документ должен вернуться для каждого из них. Проанализируйте результаты с помощью функции поиска и обратите внимание на три лучших результата. Если нужный фрагмент отсутствует, обычными причинами являются слишком большие или слишком маленькие фрагменты, слабая модель вжимания для вашей области или настройки HNSW, предпочитающие скорость восстановлению информации. Сравните результаты с полным поиском методом KNN на тех же данных, чтобы понять, насколько снижается точность поиска при использовании приблизительного метода, ведь KNN позволяет получить точный ответ для сравнения.
Примечания по переходу от демо-версии
- Модель вжимания. Она определяет размерность векторов и сильно влияет на качество поиска, поэтому выбирайте её тщательно.
- Разбиение на фрагменты. Разделите длинные документы на более короткие фрагменты перед индексацией.
search_text вместе с векторным запросом. Сервис объединяет результаты поиска по ключевым словам с помощью алгоритма BM25 с векторными результатами.Здесь вы можете добавить собственные документы, использовать разбиение на части, гибридный поиск и модель по своему выбору, чтобы превратить демо-версию в полноценное приложение типа RAG.