Этот репозиторий содержит минимальный шаблон решения, совместимый с нашей проверяющей системой.
В шаблоне есть два сервиса:
index— получает сообщения и строит чанки для индексации;search— получает вопрос и возвращаетmessage_idsнайденных сообщений.
Это не эталонное решение по качеству поиска. Его задача — показать корректный контракт и базовый pipeline.
index/main.py— сервис индексации;search/main.py— сервис поиска;data/Go Nova.json— анонимизированный пример реального чата;index/Makefile,search/Makefile— локальная сборка и публикация образов.
Вы можете менять:
- внутреннюю логику chunking;
- формирование текста для dense;
- формирование sparse векторов;
- retrieval и rerank pipeline;
- любые эвристики и фильтры.
Вы не должны менять:
- контракт
POST /index; - контракт
POST /sparse_embedding; - контракт
POST /search.
Если вы меняете request/response этих endpoint'ов, решение перестанет проходить проверку.
Для разбора входных данных используйте data/Go Nova.json.
Особенно важно посмотреть:
messages;- поля
text,parts,mentions; - metadata чанков в payload Qdrant (можете увидеть её формат в коде сервиса
search).
Во многих сообщениях значимая часть текста находится не в корневом text, а в parts[*].text.
Поле parts может содержать:
- обычный текст;
- цитату;
- пересланное сообщение.
Если вы только начинаете работу с шаблоном, сначала откройте data/Go Nova.json, затем сравните его со схемами в index/main.py и search/main.py.
При индексации в metadata чанка мы также сохраняем полезные поля:
participants— уникальныеsender_idсообщений, попавших в чанк;mentions— уникальные упоминания из сообщений чанка;contains_forward— есть ли в чанке пересланные сообщения;contains_quote— есть ли в чанке цитаты других сообщений.
Этими полями можно пользоваться в запросе к Qdrant.
index должен реализовать:
GET /healthPOST /indexPOST /sparse_embedding
ВАЖНО! У
index-контейнера нет доступа в интернет. Поэтому sparse-модель должна быть доступна локально внутри образа.
Endpoint для health check контейнера.
Мы используем его, чтобы проверить, что контейнер успешно запустился и сервис готов принимать запросы.
На выходе endpoint должен вернуть 200 OK.
На вход приходят:
chat;overlap_messages;new_messages.
Endpoint должен:
- принять новую порцию сообщений для индексации;
- построить чанки;
- вернуть текст чанков и связанные
message_ids.
На выходе сервис должен вернуть results[], где каждый элемент содержит:
page_content— текст чанка, который сохраняется в payload;dense_content— текст для dense embedding;sparse_content— текст для sparse embedding;message_ids— id сообщений, связанных с чанком.
Пример запроса:
curl -X POST "http://localhost:8001/index" \
-H "Content-Type: application/json" \
-d '{
"data": {
"chat": {
"id": "chat-1",
"name": "Go Nova",
"sn": "chat-1@chat.agent",
"type": "channel",
"is_public": true
},
"overlap_messages": [
{
"id": "1",
"time": 1710000000,
"text": "Обсуждаем релиз Go",
"sender_id": "u1",
"file_snippets": "",
"parts": [],
"mentions": [],
"is_system": false,
"is_hidden": false,
"is_forward": false,
"is_quote": false
}
],
"new_messages": [
{
"id": "2",
"time": 1710000060,
"text": "Релиз Go перенесли на следующую неделю",
"sender_id": "u2",
"file_snippets": "",
"parts": [],
"mentions": [],
"is_system": false,
"is_hidden": false,
"is_forward": false,
"is_quote": false
}
]
}
}'Пример ответа:
{
"results": [
{
"page_content": "Обсуждаем релиз Go\nРелиз Go перенесли на следующую неделю",
"dense_content": "Обсуждаем релиз Go\nРелиз Go перенесли на следующую неделю",
"sparse_content": "Обсуждаем релиз Go\nРелиз Go перенесли на следующую неделю",
"message_ids": ["1", "2"]
}
]
}На вход endpoint получает:
texts: string[]
Endpoint должен:
- принять batch текстов;
- посчитать sparse-вектора;
- вернуть их в формате, совместимом с Qdrant.
На выходе должен вернуть:
vectors[].indicesvectors[].values
Этот endpoint мы используем для вычисления sparse векторов.
Пример запроса:
curl -X POST "http://localhost:8001/sparse_embedding" \
-H "Content-Type: application/json" \
-d '{
"texts": [
"Релиз Go перенесли на следующую неделю",
"VK GPT обсуждали в отдельном чате"
]
}'Пример ответа:
{
"vectors": [
{
"indices": [1452, 9081, 12044],
"values": [0.6931, 1.0986, 0.6931]
},
{
"indices": [317, 2801, 9102],
"values": [0.6931, 0.6931, 1.3863]
}
]
}search должен реализовать:
GET /healthPOST /search
ВАЖНО! У
search-контейнера тоже нет доступа в интернет. Поэтому sparse-модель должна быть доступна локально внутри образа.
Endpoint для health check контейнера.
Мы используем его, чтобы проверить, что контейнер успешно запустился и сервис готов принимать запросы.
На выходе endpoint должен вернуть 200 OK.
На вход POST /search приходит вопрос пользователя с некоторой метадатой (можете изучить её в коде сервиса search).
Endpoint должен:
- получить вектора для поискового запроса;
- выполнить retrieval по коллекции в Qdrant;
- при необходимости выполнить rerank;
- вернуть
message_idsнайденных сообщений.
На выходе POST /search должен вернуть results[], где каждый элемент содержит:
message_ids
Пример запроса:
curl -X POST "http://localhost:8002/search" \
-H "Content-Type: application/json" \
-d '{
"question": {
"text": "Что писали про релиз Go?"
}
}'Пример ответа:
{
"results": [
{
"message_ids": ["2", "1"]
}
]
}Во время проверки мы передаем адреса наших сервисов через env. Внутри ваших контейнеров нужно использовать именно эти переменные, а не хардкодить адреса.
index должен использовать:
HOSTPORT
search должен использовать:
HOSTPORTAPI_KEYEMBEDDINGS_DENSE_URLRERANKER_URLQDRANT_URLQDRANT_COLLECTION_NAMEQDRANT_DENSE_VECTOR_NAMEQDRANT_SPARSE_VECTOR_NAME
Для dense embeddings и rerank мы предоставляем внешний HTTP API с Basic Auth. Будьте осторожны при использовании этих URL'ов, для каждой команды есть ограничение для использования (rate limit) в символах в минуту.
Базовый URL:
http://83.166.249.64:18001
Доступные endpoint'ы:
POST /embeddings— dense embeddings;GET /embeddings/models— список dense embedding моделей;POST /score— rerank;GET /score/models— список rerank моделей.
Примеры запросов:
Dense embeddings:
curl -u "$LOGIN:$PASSWORD" \
-X POST "http://83.166.249.64:18001/embeddings" \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-Embedding-0.6B",
"input": ["Пример поискового запроса"],
"encoding_format": "base64"
}'Список dense embedding моделей:
curl -u "$LOGIN:$PASSWORD" \
"http://83.166.249.64:18001/embeddings/models"Rerank:
curl -u "$LOGIN:$PASSWORD" \
-X POST "http://83.166.249.64:18001/score" \
-H "Content-Type: application/json" \
-d '{
"model": "nvidia/llama-nemotron-rerank-1b-v2",
"text_1": "Что обсуждали про релиз Go?",
"text_2": [
"Первый кандидат для реранка",
"Второй кандидат для реранка"
]
}'Список rerank моделей:
curl -u "$LOGIN:$PASSWORD" \
"http://83.166.249.64:18001/score/models"Примеры вопросов из test датасета, на котором оцениваются ваши решения.
Примеры:
В какой чат можно написать вопрос про VK GPT?Кто сейчас является руководителем команды коробочных продуктов Tarantool?Какие проблемы были с сервисом oauth-antibot в августе 2020?
При оценивании мы считаем две метрики:
Recall@KnDCG@K
Где K = 50. Если ваше решение возвращает больше 50-и message_id, то все после первых 50-и отбрасываются и не учитываются.
Для каждой посылки мы усредняем значения этих метрик по всем вопросам датасета и получаем:
recall_avgndcg_avg
Итоговый score рассчитывается по формуле:
score = recall_avg * 0.8 + ndcg_avg * 0.2
Время выполнения почти полностью состоит из вычисления векторов и индексации и занимает приблизительно 15 минут
Вы должны уложиться в 20 минут
Для локального запуска используйте docker compose.
Перед запуском укажите учетные данные для внешнего dense/rerank API:
export OPEN_API_LOGIN=...
export OPEN_API_PASSWORD=...Если эти переменные не заданы, docker compose up завершится с ошибкой.
Запуск:
docker compose up --buildБудут подняты:
qdrantнаlocalhost:6333indexнаlocalhost:8001searchнаlocalhost:8002
docker compose также создаст локальную коллекцию evaluation в Qdrant.
ВАЖНО! В текущем примере INDEX не вставляет чанки в Qdrant, поэтому
searchне вернет чанков
Сборка и запуск index:
cd index
make build
make runСборка и запуск search:
cd search
make build
make runВ личном кабинете получите логин и пароль для входа в Docker registry
Registry для хранения образов будет доступен по адресу 83.166.249.64:5000.
Поскольку он работает без TLS, необходимо добавить его в список insecure registries в настройках Docker.
- Откройте Docker Desktop -> Settings -> Docker Engine.
- Добавьте в JSON-конфиг поле
insecure-registries:
{
"insecure-registries": ["83.166.249.64:5000"]
}- Нажмите Apply & Restart.
- Откройте файл с конфигурацией докер демона в режиме редактирования
sudo nano /etc/docker/daemon.json- Добавьте в JSON-конфиг поле
insecure-registries:
{
"insecure-registries": ["83.166.249.64:5000"]
}- Перезапустите docker
sudo systemctl restart dockerНеобходимо пройти аутентификацию в docker registry, используя логин и пароль, полученные на шаге 1.
docker login 83.166.249.64:5000 -u <login> -p <password>Ожидаемый вывод: Login Succeeded.
Соберите образы своих Index Service и Search Service. Образы должны иметь тег, который имеет строгий формат:
- Для Index Service - 83.166.249.64:5000/{team_id}/index-service:latest
- Для Search Service - 83.166.249.64:5000/{team_id}/search-service:latest
Образы должны быть собраны под платформу linux/amd64
docker build --platform linux/amd64 -t 83.166.249.64:5000/{team_id}/index-service:latest {path_to_index_service_dir}
docker build --platform linux/amd64 -t 83.166.249.64:5000/{team_id}/search-service:latest {path_to_search_service_dir}docker push 83.166.249.64:5000/{team_id}/index-service:latest
docker push 83.166.249.64:5000/{team_id}/search-service:latestНа страничке оценивания нажать кнопку "Запустить/Оценить"
Для удобства сборки и публикации образов в шаблоне решения уже есть Makefile'ы с нужными командами
Примеры:
cd index
export LOGIN=... PASSWORD=... TEAM_ID=...
make pushcd search
export LOGIN=... PASSWORD=... TEAM_ID=...
make push