← Cloudflare Workers AI / workers-ai / guides / tutorials
Использование BigQuery с Workers AI
Самый простой способ начать работу с Workers AI попробовать его в Мультимодальный Playground ↗ и LLM playground ↗. Если вы решите интегрировать свой код с Workers AI, вы можете использовать его Конечные точки REST API или привязка Worker.
А как быть с данными? Что, если вы хотите, чтобы эти модели использовали данные, которые хранятся за пределами Cloudflare?
В этом руководстве вы узнаете, как передать данные из Google BigQuery в Cloudflare Worker, чтобы использовать их как входные данные для моделей Workers AI.
Предварительные требования
Вам потребуется:
- A Cloudflare Worker проект, в котором выполняется Скрипт Hello World.
- Google Cloud Platform сервисный аккаунт ↗ с связанный ключ ↗ загруженный файл с доступом на чтение к BigQuery.
- Доступ к таблице BigQuery с тестовыми данными, который позволит вам создать BigQuery Job Query ↗. Для этого руководства рекомендуется создать собственную таблицу в виде образцы таблиц ↗, если их не клонировать в собственное пространство имён GCP, они не позволяют выполнять по ним запросы заданий. В этом примере, Hacker News Corpus ↗ использовался по лицензии MIT.
1. Настройте свой Cloudflare Worker
Чтобы загрузить данные в Cloudflare и передать их в Workers AI, вы будете использовать Cloudflare Worker. Если вы ещё не создали его, ознакомьтесь с нашим руководство по началу работы.
После выполнения шагов по созданию Worker в новом проекте Worker должен появиться следующий код:
export default {
async fetch(request, env, ctx) {
return new Response("Hello World!");
},
};Если проект Worker был успешно создан, вы также сможете выполнить npx wrangler dev в консоли, чтобы запустить Worker локально:
[wrangler:inf] Ready on http://localhost:8787Откройте вкладку браузера по адресу http://localhost:8787/ чтобы увидеть развернутый Worker. Обратите внимание, что порт 8787 в вашем случае может отличаться.
Вы должны увидеть Hello World! в браузере:
Hello World!Если на этом шаге у вас возникнут проблемы, ознакомьтесь с Руководство по началу работы с Worker.
2. Импортируйте сервисный ключ GCP в Worker в виде Secrets
Теперь, когда вы убедились, что Worker успешно создан, вам нужно будет сослаться на сервисный ключ Google Cloud Platform, созданный в Предварительные требования раздел этого руководства.
Скачанный JSON-файл ключа из Google Cloud Platform должен иметь следующий формат:
{
"type": "service_account",
"project_id": "<your_project_id>",
"private_key_id": "<your_private_key_id>",
"private_key": "<your_private_key>",
"client_email": "<your_service_account_id>@<your_project_id>.iam.gserviceaccount.com",
"client_id": "<your_oauth2_client_id>",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/<your_service_account_id>%40<your_project_id>.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}Для этого руководства вам понадобятся только значения следующих полей: client_email, private_key, private_key_id, а также project_id.
Вместо того чтобы хранить эту информацию в виде обычного текста в Worker, вы будете использовать Секреты чтобы его незашифрованное содержимое было доступно только самому Worker.
Импортируйте эти три значения из JSON-файла в Secrets, начиная с поля из файла ключа JSON под названием client_email, который мы теперь назовём BQ_CLIENT_EMAIL (можно использовать другое имя переменной):
npx wrangler secret put BQ_CLIENT_EMAILВам будет предложено ввести секретное значение, которое станет значением поля client_email в файле JSON-ключа.
Если секрет был успешно загружен, отобразится следующее сообщение:
✨ Success! Uploaded secret BQ_CLIENT_EMAILТеперь импортируйте секреты для трёх оставшихся полей; private_key, private_key_id, а также project_id как BQ_PRIVATE_KEY, BQ_PRIVATE_KEY_ID, а также BQ_PROJECT_ID соответственно:
npx wrangler secret put BQ_PRIVATE_KEYnpx wrangler secret put BQ_PRIVATE_KEY_IDnpx wrangler secret put BQ_PROJECT_IDНа этом этапе вы успешно импортировали три поля из JSON-файла ключа, скачанного из Google Cloud Platform, в Cloudflare Secrets для использования в Worker.
Секреты становятся доступны Workers только после развёртывания. Чтобы сделать их доступными во время разработки, создать .dev.vars файл, чтобы локально хранить эти учётные данные и ссылаться на них как на переменные окружения.
Ваш dev.vars файл должен выглядеть следующим образом:
BQ_CLIENT_EMAIL="<your_service_account_id>@<your_project_id>.iam.gserviceaccount.com"
BQ_CLIENT_KEY="-----BEGIN PRIVATE KEY-----<content_of_your_private_key>-----END PRIVATE KEY-----\n"
BQ_PRIVATE_KEY_ID="<your_private_key_id>"
BQ_PROJECT_ID="<your_project_id>"Обязательно укажите .dev.vars в своём проекте .gitignore файл, чтобы ваши учётные данные не попали в репозиторий при использовании систем контроля версий.
Убедитесь, что секреты корректно загружены в src/index.js записав их значения в вывод консоли, как показано ниже:
export default {
async fetch(request, env, ctx) {
console.log("BQ_CLIENT_EMAIL: ", env.BQ_CLIENT_EMAIL);
console.log("BQ_PRIVATE_KEY: ", env.BQ_PRIVATE_KEY);
console.log("BQ_PRIVATE_KEY_ID: ", env.BQ_PRIVATE_KEY_ID);
console.log("BQ_PROJECT_ID: ", env.BQ_PROJECT_ID);
return new Response("Hello World!");
},
};Перезапустите Worker и выполните npx wrangler dev. Вы должны увидеть, что сервер теперь упоминает недавно добавленные переменные:
Using vars defined in .dev.vars
Your worker has access to the following bindings:
- Vars:
- BQ_CLIENT_EMAIL: "(hidden)"
- BQ_PRIVATE_KEY: "(hidden)"
- BQ_PRIVATE_KEY_ID: "(hidden)"
- BQ_PROJECT_ID: "(hidden)"
[wrangler:inf] Ready on http://localhost:8787Если вы откроете http://localhost:8787 в браузере, вы должны увидеть значения переменных в консоли там, где npx wrangler dev команда выполняется, при этом видно только Hello World! текст в окне браузера.
Теперь у вас есть доступ к учётным данным GCP из Worker. Далее вы установите библиотеку, которая поможет создать JSON Web Token, необходимый для работы с API GCP.
3. Установка библиотеки для работы с JWT
Чтобы взаимодействовать с REST API BigQuery, потребуется создать JSON Web Token ↗ для аутентификации запросов с использованием учетных данных, которые вы сохранили в Worker secrets на предыдущем шаге.
В этом руководстве вы будете использовать jose ↗ библиотеку для операций с JWT. Установите её, выполнив следующую команду в консоли:
npm i joseЧтобы убедиться, что установка прошла успешно, можно выполнить npm list, в котором перечислены все установленные пакеты, чтобы проверить, jose зависимость добавлена:
<project_name>@0.0.0
/<path_to_your_project>/<project_name>
├── @cloudflare/[email protected]
├── [email protected]
├── [email protected]
└── [email protected]4. Генерация JSON Web Token
Теперь, когда вы установили jose библиотеку, пора импортировать её и добавить в код функцию, которая создаёт подписанный JSON Web Token (JWT):
import * as jose from 'jose';
...
const generateBQJWT = async (aCryptoKey, env) => {
const algorithm = "RS256";
const audience = "https://bigquery.googleapis.com/";
const expiryAt = (new Date().valueOf() / 1000);
const privateKey = await jose.importPKCS8(env.BQ_PRIVATE_KEY, algorithm);
// Generate signed JSON Web Token (JWT)
return new jose.SignJWT()
.setProtectedHeader({
typ: 'JWT',
alg: algorithm,
kid: env.BQ_PRIVATE_KEY_ID
})
.setIssuer(env.BQ_CLIENT_EMAIL)
.setSubject(env.BQ_CLIENT_EMAIL)
.setAudience(audience)
.setExpirationTime(expiryAt)
.setIssuedAt()
.sign(privateKey)
}
export default {
async fetch(request, env, ctx) {
...
// Create JWT to authenticate the BigQuery API call
let bqJWT;
try {
bqJWT = await generateBQJWT(env);
} catch (e) {
return new Response('An error has occurred while generating the JWT', { status: 500 })
}
},
...
};Теперь, когда вы создали JWT, пора выполнить вызов API к BigQuery, чтобы получить данные.
5. Отправка аутентифицированных запросов в Google BigQuery
Используя JWT-токен, созданный на предыдущем шаге, отправьте запрос к API BigQuery, чтобы получить данные из таблицы.
Теперь вы обратитесь с запросом к таблице, которую создали в BigQuery ранее в этом руководстве. В этом примере используется выборочная версия Hacker News Corpus ↗ который использовался по лицензии MIT и был загружен в BigQuery.
const queryBQ = async (bqJWT, path) => {
const bqEndpoint = `https://bigquery.googleapis.com${path}`
// In this example, text is a field in the BigQuery table that is being queried (hn.news_sampled)
const query = 'SELECT text FROM hn.news_sampled LIMIT 3';
const response = await fetch(bqEndpoint, {
method: "POST",
body: JSON.stringify({
"query": query
}),
headers: {
Authorization: `Bearer ${bqJWT}`
}
})
return response.json()
}
...
export default {
async fetch(request, env, ctx) {
...
let ticketInfo;
try {
ticketInfo = await queryBQ(bqJWT);
} catch (e) {
return new Response('An error has occurred while querying BQ', { status: 500 });
}
...
},
};Имея необработанные данные строк из BigQuery, вы можете отформатировать их в стиле, похожем на JSON.
6. Форматирование результатов запроса
Теперь, когда вы получили данные из BigQuery, ответ BigQuery API должен выглядеть примерно так:
{
...
"schema": {
"fields": [
{
"name": "title",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "text",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
...
"rows": [
{
"f": [
{
"v": "<some_value>"
},
{
"v": "<some_value>"
}
]
},
{
"f": [
{
"v": "<some_value>"
},
{
"v": "<some_value>"
}
]
},
{
"f": [
{
"v": "<some_value>"
},
{
"v": "<some_value>"
}
]
}
],
...
}Такой формат может быть неудобен для чтения и работы при переборе результатов. Поэтому сейчас вы реализуете функцию, которая сопоставляет схему с каждым отдельным значением, и итоговый результат будет читаться проще, как показано ниже. Каждая строка соответствует объекту внутри массива.
[
{
title: "<some_value>",
text: "<some_value>",
},
{
title: "<some_value>",
text: "<some_value>",
},
{
title: "<some_value>",
text: "<some_value>",
},
];Создайте formatRows функция, которая принимает набор строк и полей, возвращённых в теле ответа BigQuery, и возвращает массив результатов в виде объектов с именованными полями.
const formatRows = (rowsWithoutFieldNames, fields) => {
// Index to fieldName
const fieldsByIndex = new Map();
// Load all fields by name and have their index in the array result as their key
fields.forEach((field, index) => {
fieldsByIndex.set(index, field.name)
})
// Iterate through rows
const rowsWithFieldNames = rowsWithoutFieldNames.map(row => {
// Per each row represented by an array f, iterate through the unnamed values and find their field names by searching them in the fieldsByIndex.
let newRow = {}
row.f.forEach((field, index) => {
const fieldName = fieldsByIndex.get(index);
if (fieldName) {
// For every field in a row, add them to newRow
newRow = ({ ...newRow, [fieldName]: field.v });
}
})
return newRow
})
return rowsWithFieldNames
}
export default {
async fetch(request, env, ctx) {
...
// Transform output format into array of objects with named fields
let formattedResults;
if ('rows' in ticketInfo) {
formattedResults = formatRows(ticketInfo.rows, ticketInfo.schema.fields);
console.log(formattedResults)
} else if ('error' in ticketInfo) {
return new Response(ticketInfo.error.message, { status: 500 })
}
...
},
};7. Передача данных в Workers AI
Теперь, когда вы преобразовали ответ BigQuery API в массив результатов, сгенерируйте теги и добавьте оценку тональности с помощью LLM через Workers AI:
const generateTags = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `Create three one-word tags for the following text. return only these three tags separated by a comma. don't return text that is not a category.Lowercase only. ${JSON.stringify(data)}`,
});
}
const generateSentimentScore = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `return a float number between 0 and 1 measuring the sentiment of the following text. 0 being negative and 1 positive. return only the number, no text. ${JSON.stringify(data)}`,
});
}
// Iterates through values, sends them to an AI handler and encapsulates all responses into a single Promise
const getAIGeneratedContent = (data, env, aiHandler) => {
let results = data?.map(dataPoint => {
return aiHandler(dataPoint, env)
})
return Promise.all(results)
}
...
export default {
async fetch(request, env, ctx) {
...
let summaries, sentimentScores;
try {
summaries = await getAIGeneratedContent(formattedResults, env, generateTags);
sentimentScores = await getAIGeneratedContent(formattedResults, env, generateSentimentScore)
} catch {
return new Response('There was an error while generating the text summaries or sentiment scores')
}
},
formattedResults = formattedResults?.map((formattedResult, i) => {
if (sentimentScores[i].response && summaries[i].response) {
return {
...formattedResult,
'sentiment': parseFloat(sentimentScores[i].response).toFixed(2),
'tags': summaries[i].response.split(',').map((result) => result.trim())
}
}
}
};Раскомментируйте следующие строки в файле Wrangler вашего проекта:
{
"ai": {
"binding": "AI"
}
}[ai]
binding = "AI"Перезапустите Worker, который выполняется локально, а затем перейдите к конечной точке вашего приложения:
curl http://localhost:8787Скорее всего, вам будет предложено войти в свою учётную запись Cloudflare и предоставить Wrangler (Cloudflare CLI) временный доступ к вашей учётной записи при использовании Worker AI.
После того как вы перейдёте в http://localhost:8787 вы должны увидеть результат, похожий на следующий:
{
"data": [
{
"text": "You can see a clear spike in submissions right around US Thanksgiving.",
"sentiment": "0.61",
"tags": [
"trends",
"submissions",
"thanksgiving"
]
},
{
"text": "I didn't test the changes before I published them. I basically did development on the running server. In fact for about 30 seconds the comments page was broken due to a bug.",
"sentiment": "0.35",
"tags": [
"software",
"deployment",
"error"
]
},
{
"text": "I second that. As I recall, it's a very enjoyable 700-page brain dump by someone who's really into his subject. The writing has a personal voice; there are lots of asides, dry wit, and typos that suggest restrained editing. The discussion is intelligent and often theoretical (and Bartle is not scared to use mathematical metaphors), but the tone is not academic.",
"sentiment": "0.86",
"tags": [
"review",
"game",
"design"
]
}
]
}Фактические значения и поля в основном зависят от запроса, сделанного на шаге 5, который затем передаётся в LLM.
Итоговый результат
Весь код, показанный на разных этапах, объединён в следующий код в src/index.js:
import * as jose from "jose";
const generateBQJWT = async (env) => {
const algorithm = "RS256";
const audience = "https://bigquery.googleapis.com/";
const expiryAt = new Date().valueOf() / 1000;
const privateKey = await jose.importPKCS8(env.BQ_PRIVATE_KEY, algorithm);
// Generate signed JSON Web Token (JWT)
return new jose.SignJWT()
.setProtectedHeader({
typ: "JWT",
alg: algorithm,
kid: env.BQ_PRIVATE_KEY_ID,
})
.setIssuer(env.BQ_CLIENT_EMAIL)
.setSubject(env.BQ_CLIENT_EMAIL)
.setAudience(audience)
.setExpirationTime(expiryAt)
.setIssuedAt()
.sign(privateKey);
};
const queryBQ = async (bgJWT, path) => {
const bqEndpoint = `https://bigquery.googleapis.com${path}`;
const query = "SELECT text FROM hn.news_sampled LIMIT 3";
const response = await fetch(bqEndpoint, {
method: "POST",
body: JSON.stringify({
query: query,
}),
headers: {
Authorization: `Bearer ${bgJWT}`,
},
});
return response.json();
};
const formatRows = (rowsWithoutFieldNames, fields) => {
// Index to fieldName
const fieldsByIndex = new Map();
fields.forEach((field, index) => {
fieldsByIndex.set(index, field.name);
});
const rowsWithFieldNames = rowsWithoutFieldNames.map((row) => {
// Map rows into an array of objects with field names
let newRow = {};
row.f.forEach((field, index) => {
const fieldName = fieldsByIndex.get(index);
if (fieldName) {
newRow = { ...newRow, [fieldName]: field.v };
}
});
return newRow;
});
return rowsWithFieldNames;
};
const generateTags = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `Create three one-word tags for the following text. return only these three tags separated by a comma. don't return text that is not a category.Lowercase only. ${JSON.stringify(data)}`,
});
};
const generateSentimentScore = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `return a float number between 0 and 1 measuring the sentiment of the following text. 0 being negative and 1 positive. return only the number, no text. ${JSON.stringify(data)}`,
});
};
const getAIGeneratedContent = (data, env, aiHandler) => {
let results = data?.map((dataPoint) => {
return aiHandler(dataPoint, env);
});
return Promise.all(results);
};
export default {
async fetch(request, env, ctx) {
// Create JWT to authenticate the BigQuery API call
let bqJWT;
try {
bqJWT = await generateBQJWT(env);
} catch (error) {
console.log(error);
return new Response("An error has occurred while generating the JWT", {
status: 500,
});
}
// Fetch results from BigQuery
let ticketInfo;
try {
ticketInfo = await queryBQ(
bqJWT,
`/bigquery/v2/projects/${env.BQ_PROJECT_ID}/queries`,
);
} catch (error) {
console.log(error);
return new Response("An error has occurred while querying BQ", {
status: 500,
});
}
// Transform output format into array of objects with named fields
let formattedResults;
if ("rows" in ticketInfo) {
formattedResults = formatRows(ticketInfo.rows, ticketInfo.schema.fields);
} else if ("error" in ticketInfo) {
return new Response(ticketInfo.error.message, { status: 500 });
}
// Generate AI summaries and sentiment scores
let summaries, sentimentScores;
try {
summaries = await getAIGeneratedContent(
formattedResults,
env,
generateTags,
);
sentimentScores = await getAIGeneratedContent(
formattedResults,
env,
generateSentimentScore,
);
} catch {
return new Response(
"There was an error while generating the text summaries or sentiment scores",
);
}
// Add AI summaries and sentiment scores to previous results
formattedResults = formattedResults?.map((formattedResult, i) => {
if (sentimentScores[i].response && summaries[i].response) {
return {
...formattedResult,
sentiment: parseFloat(sentimentScores[i].response).toFixed(2),
tags: summaries[i].response.split(",").map((result) => result.trim()),
};
}
});
const response = { data: formattedResults };
return new Response(JSON.stringify(response), {
headers: { "Content-Type": "application/json" },
});
},
};Если вы хотите развернуть этот Worker, сделать это можно, выполнив npx wrangler deploy:
Total Upload: <size_of_your_worker> KiB / gzip: <compressed_size_of_your_worker> KiB
Uploaded <name_of_your_worker> (x sec)
Deployed <name_of_your_worker> triggers (x sec)
https://<your_public_worker_endpoint>
Current Version ID: <worker_script_version_id>Это создаст публичную конечную точку, через которую можно обращаться к Worker откуда угодно. Учитывайте это при работе с продакшен-данными и обязательно предусмотрите дополнительные меры контроля доступа.
Заключение
В этом руководстве вы узнали, как интегрировать Google BigQuery и Cloudflare Workers, создав ключ сервисного аккаунта GCP и сохранив часть его в виде Worker secrets. Далее он был импортирован в код, и с помощью jose npm-библиотеку, вы создали JSON Web Token для аутентификации запроса к API BigQuery.
Получив результаты, вы отформатировали их для передачи генеративным AI-моделям через Workers AI, чтобы сгенерировать теги и выполнить анализ тональности извлечённых данных.
Дальнейшие шаги
Если вместо отображения в браузере результатов передачи данных модели AI ваш процесс подразумевает получение и сохранение данных (например, в R2 или D1) через регулярные интервалы, возможно, стоит добавить обработчик scheduled для этого Worker. Это позволяет запускать Worker по заданному расписанию через Cron Trigger. Рекомендуем ознакомиться со справочными архитектурными схемами на Загрузка данных BigQuery в Workers AI.
Один из вариантов использования импорта данных из других источников, как вы делали в этом руководстве, создание системы RAG. Если вам это актуально, посмотрите Руководство по созданию ИИ на основе Retrieval Augmented Generation (RAG).
Чтобы узнать больше о других моделях ИИ, доступных в Cloudflare, посетите Workers AI раздел нашей документации.