Генерация эмбеддингов для поиска

ShellBeginner
Практиковаться сейчас

Введение

Поиск по ключевым словам ищет совпадения слов. Семантический поиск пытается найти текст с тем же смыслом. Например, запрос «Я не могу войти» должен быть близок к статье о сбросе пароля, даже если предложения не содержат одни и те же слова.

Модель эмбеддингов преобразует текст в вектор — упорядоченный список чисел, описывающий признаки, которым модель научилась на основе языка. Тексты со схожим смыслом обычно указывают в похожих направлениях. В этой лабораторной работе мы сравним эти направления с помощью косинусного сходства — вычисления, которое возвращает большее значение для более близко ориентированных векторов. Значение имеет смысл только при сравнении векторов, созданных с одной и той же моделью, одинаковым количеством измерений и одинаковым способом pooling; это не универсальный процент истинности.

Вы создадите POST /search. Worker создаёт эмбеддинг одного запроса и трёх небольших справочных статей с помощью размещённой в Cloudflare модели @cf/baai/bge-small-en-v1.5. Модель создаёт 384 числа для каждого текста. Приложение проверяет каждый вектор перед сравнением, отклоняет несовместимые и нечисловые значения, а затем возвращает идентификаторы статей в порядке релевантности, не раскрывая сами векторы.

Это четвёртая лабораторная работа курса. Если вы открыли её напрямую, сначала выполните Подключение LabEx к вашему аккаунту Cloudflare, чтобы научиться пользоваться терминалом виртуальной машины, авторизовать Wrangler, подтвердить учебный аккаунт и настроить его идентификатор.

В бесплатных аккаунтах Workers сейчас доступна общая суточная квота в 10 000 Neurons. Эта модель стоит примерно 1 841 Neuron на миллион входных токенов, а в этой лабораторной работе используются всего несколько коротких синтетических предложений, поэтому Workers Paid не требуется, пока доступна бесплатная квота. Локальный запуск всё равно обращается к Cloudflare и расходует квоту аккаунта. Если модель или квота недоступны, остановитесь и не повторяйте запросы многократно.

Настройка устанавливает Node.js 22.22.0 и локальную версию Wrangler 4.132.0 в /home/labex/project/search-embeddings. Она также предоставляет детерминированные тесты и независимые проверки. Настройка не авторизует Wrangler, не вызывает модель, не разворачивает Worker и не создаёт облачные ресурсы.

Авторизация виртуальной машины и настройка Worker для эмбеддингов

На этом шаге вы авторизуете новую виртуальную машину и настроите один временный Worker. Вход в Dashboard относится к вашему браузеру, но Wrangler внутри этой виртуальной машины требует отдельной ограниченной авторизации.

Перейдите в подготовленный проект и проверьте зафиксированную версию CLI:

cd /home/labex/project/search-embeddings
npx wrangler --version

Ожидаемый результат — 4.132.0. Запросите ограниченные разрешения, которые использовались в предыдущих лабораторных работах по Workers AI. Разрешение KV нужно для проверки зависимостей очистки в Wrangler 4.132.0; эта лабораторная работа не создаёт данных KV.

npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write ai:write

Откройте показанную ссылку, введите текущий код, проверьте аккаунт и разрешения, затем авторизуйте свой учебный аккаунт. После этого просмотрите структурированные данные об учётной записи:

npx wrangler whoami --json

Убедитесь, что указано loggedIn: true, затем сгенерируйте уникальное имя Worker:

RUN="labex-c07-a04-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

Замените YOUR_ACCOUNT_ID фактическим идентификатором нужного аккаунта:

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "YOUR_ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-16",
  "workers_dev": true,
  "preview_urls": false,
  "observability": { "enabled": true, "head_sampling_rate": 1 },
  "ai": { "binding": "AI", "remote": true }
}
JSON

env.AI — это binding внутри процесса, а не API-ключ модели в исходном коде. Параметр remote: true означает, что локальная разработка всё равно обращается к модели, связанной с аккаунтом.

Изучение контракта вектора

На этом шаге вы свяжете конфигурацию модели с числовыми данными, которые должно проверять приложение.

Сгенерируйте типы окружения и проверьте binding платформы:

npx wrangler types
grep -A4 'interface __BaseEnv_Env' worker-configuration.d.ts

Найдите AI: Ai. Выбранная модель BGE Small возвращает один вектор размерностью 384 для каждого входного текста. Размерность означает количество позиций, поэтому пакет из четырёх текстов должен иметь форму [4, 384]. Каждая позиция должна содержать конечное число: не NaN, не положительную и не отрицательную бесконечность.

В этой лабораторной работе явно запрашивается pooling cls. Pooling — это способ, с помощью которого модель сворачивает информацию об отдельных токенах в один вектор. Векторы, созданные с pooling cls и mean, несовместимы, даже если оба содержат 384 позиции, поэтому приложение сохраняет выбранный способ вместе с моделью и размерностью.

Просмотрите предоставленные детерминированные фикстуры:

grep -nE 'incompatible|non-finite|cosine similarity' test/worker.test.mjs

Эти фикстуры делают тесты ошибок воспроизводимыми и не расходуют Neurons. Они также не требуют точного значения сходства в реальном времени, которое может измениться из-за поведения модели.

Создание проверяемой конечной точки сходства

На этом шаге вы реализуете запрос эмбеддингов, проверку векторов и локальное сравнение косинусного сходства. Worker возвращает идентификаторы документов и оценки, а не 1 536 исходных чисел из четырёх векторов.

Создайте точку входа:

cat > src/index.js <<'JS'
const MODEL = "@cf/baai/bge-small-en-v1.5";
const DIMENSIONS = 384;
const POOLING = "cls";
const MAX_QUERY = 300;
const DOCUMENTS = [
  { id: "password-reset", text: "Reset a forgotten password and regain account access." },
  { id: "upload-pdf", text: "Troubleshoot a PDF document that will not upload." },
  { id: "billing-receipt", text: "Download a receipt for a completed payment." }
];

export function validateEmbeddingBatch(result, expectedCount) {
  if (!Array.isArray(result?.shape) || result.shape[0] !== expectedCount || result.shape[1] !== DIMENSIONS) {
    throw new Error("incompatible embedding shape");
  }
  if (!Array.isArray(result.data) || result.data.length !== expectedCount) {
    throw new Error("incompatible embedding count");
  }
  for (const vector of result.data) {
    if (!Array.isArray(vector) || vector.length !== DIMENSIONS || !vector.every(Number.isFinite)) {
      throw new Error("invalid embedding vector");
    }
  }
  return result.data;
}

export function cosineSimilarity(left, right) {
  if (left.length !== right.length || left.length === 0) throw new Error("incompatible vectors");
  let dot = 0, leftNorm = 0, rightNorm = 0;
  for (let index = 0; index < left.length; index += 1) {
    dot += left[index] * right[index];
    leftNorm += left[index] ** 2;
    rightNorm += right[index] ** 2;
  }
  if (leftNorm === 0 || rightNorm === 0) throw new Error("zero-length direction");
  return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
}

function json(data, status = 200) { return Response.json(data, { status }); }

async function readQuery(request) {
  if (!(request.headers.get("content-type") || "").toLowerCase().includes("application/json")) {
    return { error: json({ error: "json_required" }, 415) };
  }
  let body;
  try { body = await request.json(); } catch { return { error: json({ error: "invalid_json" }, 400) }; }
  const query = typeof body?.query === "string" ? body.query.trim() : "";
  if (!query) return { error: json({ error: "invalid_query" }, 400) };
  if (query.length > MAX_QUERY) return { error: json({ error: "query_too_large" }, 413) };
  return { query };
}

async function search(request, env) {
  const parsed = await readQuery(request);
  if (parsed.error) return parsed.error;
  const requestId = crypto.randomUUID();
  let result;
  try {
    result = await env.AI.run(MODEL, { text: [parsed.query, ...DOCUMENTS.map((item) => item.text)], pooling: POOLING });
  } catch {
    console.error(JSON.stringify({ event: "embedding_failed", requestId, model: MODEL }));
    return json({ error: "model_unavailable", requestId }, 502);
  }
  let vectors;
  try { vectors = validateEmbeddingBatch(result, DOCUMENTS.length + 1); }
  catch {
    console.error(JSON.stringify({ event: "embedding_rejected", requestId, model: MODEL }));
    return json({ error: "invalid_embeddings", requestId }, 502);
  }
  const [queryVector, ...documentVectors] = vectors;
  const matches = DOCUMENTS.map((document, index) => ({ id: document.id, score: cosineSimilarity(queryVector, documentVectors[index]) }))
    .sort((left, right) => right.score - left.score);
  console.log(JSON.stringify({ event: "embedding_compared", requestId, model: MODEL, dimensions: DIMENSIONS, count: vectors.length, pooling: POOLING }));
  return json({ model: MODEL, dimensions: DIMENSIONS, pooling: POOLING, matches, requestId });
}

export default { async fetch(request, env) {
  const url = new URL(request.url);
  if (request.method === "GET" && url.pathname === "/health") return json({ status: "ok" });
  if (request.method === "POST" && url.pathname === "/search") return search(request, env);
  return json({ error: "not_found" }, 404);
} };
JS

Проверка выполняется до вычисления сходства. Она предотвращает незаметное усечение данных, бессмысленное сравнение векторов разной размерности и появление оценок NaN. В журналах сохраняются метаданные жизненного цикла, но не запрос, текст статей и векторы.

Запустите пять детерминированных тестов, затем соберите проект без развёртывания:

node --test test/worker.test.mjs
npx wrangler deploy --dry-run

Тесты подтверждают локальную математику и поведение при отклонении данных. Тестовый запуск подтверждает, что Worker и конфигурация binding успешно собираются вместе.

Выполнение одного реального пакета эмбеддингов

На этом шаге вы запустите обработчик локально, а его AI binding выполнит один реальный удалённый запрос эмбеддингов.

Запустите Wrangler в фоновом режиме и дождитесь ответа маршрута проверки, не использующего AI:

npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid
for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then break; fi
  sleep 1
done

Отправьте короткий синтетический запрос:

curl --silent --show-error http://127.0.0.1:8787/search \
  --header 'Content-Type: application/json' \
  --data '{"query":"I cannot sign in because I forgot my password"}'

Ожидайте поля model, dimensions: 384, pooling: "cls", три идентификатора в порядке релевантности и конечные значения оценок. Не требуйте точных оценок. Полученный порядок подтверждает результат именно для этого запроса, но не является постоянной гарантией модели.

Проверьте, что пустой запрос отклоняется до запуска инференса:

curl --silent --show-error --write-out '\nHTTP %{http_code}\n' http://127.0.0.1:8787/search \
  --header 'Content-Type: application/json' --data '{"query":""}'

Ожидайте {"error":"invalid_query"} и HTTP-код 400.

Развёртывание и проверка данных об эмбеддингах

На этом шаге вы развернёте ту же конечную точку и сопоставите данные выполнения с Cloudflare Dashboard.

Остановите только сохранённый процесс разработки и выполните развёртывание:

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true
npx wrangler deploy

Сохраните точный URL, напечатанный Wrangler, и отправьте один публичный запрос:

WORKER_URL="https://YOUR_WORKER_URL"
curl --silent --show-error "$WORKER_URL/search" \
  --header 'Content-Type: application/json' \
  --data '{"query":"I cannot sign in because I forgot my password"}'

Убедитесь, что ответ содержит модель, 384 измерения и pooling cls, а также ровно три предоставленных идентификатора с конечными значениями оценок.

Откройте Workers & Pages → Overview → ваш Worker labex-c07-a04-.... В разделе Bindings найдите binding AI. В разделе Observability → Logs найдите embedding_compared и разверните событие. Убедитесь, что указаны точная модель, dimensions: 384, count: 4, pooling: cls и идентификатор запроса. Запрос, документы и векторы должны отсутствовать.

Страница Bindings показывает это соединение: у Worker есть один binding Workers AI с именем AI. Binding — это безопасный дескриптор, который код использует как env.AI; API-ключ не нужно вставлять в исходный файл.

На странице Bindings показан подключённый binding Workers AI с именем AI

На странице Observability отображаются успешные запросы /search и отсутствие ошибок в этом временном запуске. Точные значения могут отличаться, поскольку каждый тестовый запрос становится отдельным событием.

На странице Observability Worker отображаются успешные запросы поиска и ноль ошибок

Разверните одно событие embedding_compared. В этом примере сохраняются только полезные эксплуатационные данные: были сравнены четыре текста, размерность каждого вектора равна 384, использовался pooling cls, а моделью была @cf/baai/bge-small-en-v1.5. Запрос учащегося, текст документов и сотни чисел векторов намеренно не записываются.

В развёрнутом журнале эмбеддингов содержатся поля count, dimensions, pooling и model

Затем откройте Workers AI. Найдите модель BGE Small в статистике использования за сегодня и убедитесь, что ограниченный запуск остаётся в пределах общей бесплатной квоты в 10 000 Neurons. Данные Dashboard могут отображаться с задержкой; немного подождите вместо повторного запуска инференса для обновления графика.

В протестированном бесплатном аккаунте модель эмбеддингов использовала только 0.29 Neurons, а общее использование составило 295.6 / 10k. В большее общее значение входят другие тесты курса, выполненные в тот же день, поэтому воспринимайте эти числа как пример, а не как обязательный результат. Важно, чтобы появилась строка BGE Small, а суточное общее использование оставалось ниже бесплатной квоты.

В статистике Workers AI использование эмбеддингов BGE Small находится в пределах суточной бесплатной квоты

Графики Dashboard полезны для визуальной проверки, однако JSON-ответ и независимый скрипт проверки остаются авторитетным подтверждением корректной работы развёрнутого Worker.

Удаление Worker и выход из аккаунта

На этом шаге вы удалите временную конечную точку, а затем отмените авторизацию этой виртуальной машины. Использование Workers AI хранится в истории аккаунта, поэтому удаление Worker не удаляет запись об использовании.

Удалите именно тот Worker, имя которого указано в wrangler.jsonc:

npx wrangler delete

Подтверждайте удаление только после того, как Wrangler покажет уникальное имя Worker этой лабораторной работы labex-c07-a04-.... Дождитесь сообщения Successfully deleted, затем, пока авторизация ещё действует, выполните независимую проверку отсутствия ресурса в облаке:

python3 .labex/verify.py deleted

Только после сообщения PASS: deleted выйдите из аккаунта и проверьте структурированное состояние:

npx wrangler logout
npx wrangler whoami --json

Убедитесь, что указано loggedIn: false. Закрытая вкладка браузера или отсутствие локального файла не подтверждают очистку облачных ресурсов.

Резюме

Вы создали эмбеддинги размерностью 384 с помощью размещённой в Cloudflare модели, зафиксировали параметры совместимости, проверили каждый вектор, сравнили семантические направления с помощью косинусного сходства и отклонили несовместимые данные до ранжирования. Вы также проверили рабочий binding и журналы с ограниченным раскрытием данных, а затем удалили временный Worker и авторизацию виртуальной машины.