TypeScript SDK
Сгенерированный клиент @repo/sdk — параметры конструктора, все методы, поведение повторов и тип AACSearchError.
@repo/sdk — типизированный клиент, сгенерированный из packages/api/routes/v1/openapi.json (пересборка — pnpm sdk:generate). Это ESM-пакет, использующий глобальный fetch, поэтому он работает в Node 18+, Bun, Deno и на edge-рантаймах.
import { AACSearchClient, AACSearchError } from "@repo/sdk";
const client = new AACSearchClient({
apiKey: process.env.AAC_KEY!,
baseUrl: "https://search.example.com/api/v1",
});
const result = await client.search("documentation", { q: "тарифы", per_page: 5 });
console.log(result.found, result.hits[0]?.document.title);Конструктор
interface AACSearchClientOptions {
/** Ключ aac_live_… Обязателен. */
apiKey: string;
/** По умолчанию http://localhost:3000/api/v1 */
baseUrl?: string;
/** Повторы для 429 и 5xx. По умолчанию 2. */
maxRetries?: number;
/** Своя реализация fetch — для трассировки или прокси. */
fetch?: typeof fetch;
}Пустой apiKey приводит к исключению сразу, до первого запроса:
new AACSearchClient({ apiKey: "" });
// AACSearchError: apiKey is required (status 401, code MISSING_API_KEY)Завершающий слэш в baseUrl отбрасывается, поэтому https://host/api/v1/ и https://host/api/v1 эквивалентны.
Ошибки и повторы
Любая ошибка — это AACSearchError с разобранным конвертом и HTTP-статусом:
class AACSearchError extends Error {
name: "AACSearchError";
status: number; // 401, 403, 422, 429, 502…
code: string; // UNAUTHORIZED, QUOTA_EXCEEDED, VALIDATION_ERROR…
details?: unknown; // details из тела ошибки
requestId?: string; // указывайте в обращении в поддержку
}try {
await client.upsertDocuments("documentation", {
documents: [{ external_id: "pricing", content: "Pro стоит 29 USD в месяц." }],
action: "create",
});
} catch (error) {
if (error instanceof AACSearchError) {
if (error.code === "QUOTA_EXCEEDED") {
console.error("Квота исчерпана, ждать до", (error.details as { resetsAt: string }).resetsAt);
} else if (error.status === 429) {
// сработал лимит запросов — SDK уже исчерпал maxRetries
} else {
console.error(error.code, error.message, error.requestId);
}
}
throw error;
}Повторы: 429 и любые 5xx повторяются до maxRetries раз (по умолчанию 2) с экспоненциальной задержкой 250 мс, 500 мс, 1 с. Остальные 4xx не повторяются никогда. Когда попытки исчерпаны, разбирается последний ответ и бросается исключение. Если в теле нет объекта error, code становится REQUEST_FAILED, а сообщение — Request failed with <status>.
Таймаут клиент не навязывает: передайте свой fetch, учитывающий AbortSignal, если он нужен.
Методы
Параметры и тела — обычные объекты; клиент сериализует только определённые и непустые значения, поэтому undefined и "" не превращаются в пустые параметры.
Поиск и обнаружение
| Метод | HTTP |
|---|---|
search(index, query) | GET /indexes/{index}/search |
searchPost(index, body) | POST /indexes/{index}/search |
suggest(index, query) | GET /indexes/{index}/suggest |
multiSearch(body) | POST /indexes/multi_search |
nlSearch(index, body) | POST /indexes/{index}/nl-search |
indexStats(index, query?) | GET /indexes/{index}/stats |
health() | GET /health |
capabilities() | GET /capabilities |
openapi() | GET /openapi.json |
metrics() | GET /metrics |
const suggestions = await client.suggest("documentation", { q: "тар", limit: 5 });
const stats = await client.indexStats("documentation");
const caps = await client.capabilities();
if (!caps.features.autoEmbedding) throw new Error("семантический поиск недоступен на этом узле");Документы
| Метод | HTTP |
|---|---|
upsertDocuments(index, body) | POST /indexes/{index}/documents |
getDocument(index, externalId) | GET /indexes/{index}/documents/{externalId} |
deleteDocument(index, externalId) | DELETE /indexes/{index}/documents/{externalId} |
deleteDocuments(index, query) | DELETE /indexes/{index}/documents?filter_by=… |
bulkUpdateDocuments(index, body) | POST /indexes/{index}/documents/bulk |
exportDocuments(index) | GET /indexes/{index}/documents/export |
await client.upsertDocuments("documentation", {
documents: [
{
external_id: "pricing",
title: "Тарифы",
content: "Pro стоит 29 USD в месяц и включает 500 000 поисков.",
tags: ["billing"],
},
],
action: "upsert",
});
const doc = await client.getDocument("documentation", "pricing");
await client.deleteDocument("documentation", "pricing");
await client.deleteDocuments("documentation", { filter_by: "source_type:=connector" });AI
| Метод | HTTP |
|---|---|
answer(index, body) | POST /indexes/{index}/answer — ответ RAG со ссылками |
assistantAsk(index, body) | POST /indexes/{index}/assistant — шаг диалога |
assistantStream(index, body, onEvent) | POST /indexes/{index}/assistant/stream — SSE |
assistantNative(index, body) | POST /indexes/{index}/assistant/native — модель диалога движка |
assistantThreads(index) | GET /indexes/{index}/assistant |
assistantThread(index, conversationId) | GET /indexes/{index}/assistant/{conversationId} |
conversationModels(index) | GET /indexes/{index}/conversation-models |
voiceSearch(index, body) | POST /indexes/{index}/voice |
imageSearch(index, body) | POST /indexes/{index}/image |
const answer = await client.answer("documentation", {
question: "Сколько стоит тариф pro?",
per_page: 4,
});
console.log(answer.answer, answer.citations);
await client.assistantStream("documentation", { question: "А какие лимиты?" }, (event) => {
if (event.type === "delta")
process.stdout.write(String((event.payload as { text?: string }).text ?? ""));
});assistantStream сам читает тело ответа, разбивает кадры по пустой строке, разбирает каждую строку data: и передаёт вам { type, payload }. Типы событий: citations, delta, done, error. Промис завершается при закрытии потока; пустое тело даёт AACSearchError с кодом SSE_EMPTY.
Релевантность и операции
| Метод | HTTP |
|---|---|
listSynonyms(index) / putSynonym(index, body) / deleteSynonym(index, id) | /indexes/{index}/synonyms |
listCurations(index) / putCuration(index, body) / deleteCuration(index, id) | /indexes/{index}/curations |
trackEvent(index, body) | POST /indexes/{index}/events |
reindex(index) | POST /indexes/{index}/reindex |
scopedKey(index, body) | POST /indexes/{index}/scoped-key |
triggerSnapshot() | POST /admin/snapshot |
runJobs(query) | POST /admin/jobs/run |
exportOrganization() | GET /organization/export |
purgeOrganization(query) | DELETE /organization/data |
Аналитика и биллинг
| Метод | HTTP |
|---|---|
analyticsOverview(query) | GET /analytics/overview |
analyticsOverviewCsv(query) | GET /analytics/overview.csv |
slowQueries(query) | GET /analytics/slow-queries |
trendingQueries(query) | GET /analytics/trending |
usage() | GET /billing/usage |
wallet() | GET /billing/wallet |
const { overview } = await client.analyticsOverview({ days: 7 });
console.log(overview.totals.searches, overview.totals.zeroResultRate);
const { usage } = await client.usage();
console.log(`${usage.searches.used}/${usage.searches.limit} поисков за период`);Известные расхождения генератора
Клиент генерируется механически из документа OpenAPI, и три метода не совпадают с сервером. Они перечислены здесь, чтобы вы не потратили на них полдня:
| Метод | Проблема | Обход |
|---|---|---|
multiSuggest() | Не отправляет тело, хотя эндпоинту нужны { indexes, q } | fetch на POST /indexes/suggest/multi |
exportDocuments(index) | Разбирает NDJSON-поток через response.json() | fetch на экспорт и чтение текста |
webhookIngest(index) | Не отправляет ни тело, ни заголовки подписи | Подпишите и отправьте сами, см. Вебхуки |
Остальные методы соответствуют документированным эндпоинтам один к одному.