AACSearch / руководство
Инкрементальная индексация
Синхронизация коллекции с каталогом — upsert, частичные обновления, пакетный импорт с построчными результатами, удаление, асинхронные задания и переиндексация без простоя.
Индексация — задача сервера: каждой операции на этой странице нужен полный
ключ (см. ключи и области доступа). В примерах используется
клиент TypeScript SDK из быстрого старта и curl с
-H "X-NOVA-API-KEY: $NOVA_API_KEY".
Найти коллекцию
getCollections (GET /collections) выводит все коллекции организации с их
схемой; getCollection (GET /collections/{collectionName}) возвращает одну:
const all = await client.collections.getCollections();
const products = await client.collections.getCollection({ collectionName: "products" });
// products.fields, products.num_documents, products.default_sorting_fieldДействия записи
Запись одного документа и пакетов принимает параметр action: он решает, что
делать, если документ с таким id уже есть — или его нет:
action | Документ есть | Документа нет | Когда использовать |
|---|---|---|---|
create | ошибка | создаётся | первая загрузка, где повторный id — это ошибка |
upsert | заменяется целиком | создаётся | полная синхронизация — каждый раз отправляется весь документ |
update | меняются переданные поля, остальные сохраняются | ошибка | изменение нескольких полей известного документа |
emplace | меняются переданные поля, остальные сохраняются | создаётся | частичные фиды (только цена и остаток), в которых могут быть новые товары |
id документа задаёте вы: используйте первичный ключ своего каталога, чтобы
повторные синхронизации попадали в тот же документ.
dirty_values определяет, что делать, если значение поля не соответствует
схеме (строка в поле float): coerce_or_reject, coerce_or_drop, drop или
reject.
Один документ
indexDocument — POST /collections/{collectionName}/documents:
curl -X POST "https://api.aacsearch.com/v1/collections/products/documents?action=upsert" \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" -H "Content-Type: application/json" \
-d '{"id":"42","name":"Cordless drill, 18V","price":84.0,"in_stock":true}'getDocument (GET /collections/{collectionName}/documents/{documentId})
читает один документ по id.
Изменить часть полей одного документа — updateDocument,
PATCH /collections/{collectionName}/documents/{documentId}:
await client.documents.updateDocument({
collectionName: "products",
documentId: "42",
body: { price: 79.0 },
});Изменить одни и те же поля у всех документов, подходящих под фильтр, —
updateDocuments, PATCH /collections/{collectionName}/documents?filter_by=…,
отвечает {"num_updated": n}:
await client.documents.updateDocuments({
collectionName: "products",
updateDocumentsParameters: { filter_by: "brand:=Acme" },
body: { in_stock: false },
});Пакеты: импорт с построчными результатами
importDocuments — POST /collections/{collectionName}/documents/import —
принимает JSONL (по документу на строку) и параметры запроса action,
dirty_values, batch_size, return_id и return_doc.
curl -X POST "https://api.aacsearch.com/v1/collections/products/documents/import?action=emplace&return_id=true" \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" -H "Content-Type: application/octet-stream" \
--data-binary @changes.jsonlОтвет — JSONL, по одной строке на каждую входную строку, в том же порядке:
{"success": true, "id": "42"}
{"success": false, "error": "Bad JSON.", "document": "[bad doc"}Запрос целиком отвечает 200, даже если часть строк не записалась, — пакет не
атомарен: прочитайте каждую строку и повторяйте только неудачные —
остальные уже проиндексированы.
async function importBatch(collectionName: string, docs: Record<string, unknown>[]) {
const jsonl = docs.map((doc) => JSON.stringify(doc)).join("\n");
const res = await client.documents.importDocuments(
{ data: new TextEncoder().encode(jsonl), filename: "batch.jsonl", contentType: "application/x-ndjson" },
collectionName,
{ importDocumentsParameters: { action: "emplace", dirty_values: "coerce_or_drop" } },
);
const results = new TextDecoder().decode(await res.arrayBuffer()).trim().split("\n")
.map((line) => JSON.parse(line) as { success: boolean; error?: string });
// results[i] относится к docs[i]
return docs.filter((_, i) => !results[i]?.success);
}
let pending = changedProducts;
for (let attempt = 0; attempt < 3 && pending.length > 0; attempt++) {
pending = await importBatch("products", pending);
}
if (pending.length > 0) console.error("still failing", pending.map((doc) => doc.id));Строка, дважды не записавшаяся по одной и той же причине (несоответствие
схеме, нет id), не запишется и с третьей попытки — залогируйте её и исправьте
данные, а не повторяйте бесконечно. Передавайте action явно при каждой
записи, не полагаясь на значение по умолчанию.
exportDocuments (GET /collections/{collectionName}/documents/export)
возвращает коллекцию в том же формате JSONL, поэтому экспорт можно сразу
передать в импорт.
Удаление документов
Один документ — deleteDocument,
DELETE /collections/{collectionName}/documents/{documentId}:
await client.documents.deleteDocument({ collectionName: "products", documentId: "43" });Все документы, подходящие под фильтр, — deleteDocuments,
DELETE /collections/{collectionName}/documents?filter_by=…, отвечает
{"num_deleted": n}:
curl -X DELETE -G "https://api.aacsearch.com/v1/collections/products/documents" \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" \
--data-urlencode "filter_by=id:[43,44,45]"truncate: true удаляет все документы, сохраняя коллекцию и её схему. Чтобы
синхронизация удаляла то, чего больше нет в каталоге, либо удаляйте известные
удалённые id, либо помечайте каждый документ меткой синхронизации (synced_at)
и после прогона удаляйте по filter_by: "synced_at:<…".
Асинхронные задания загрузки
Если не хочется держать соединение открытым — или данные представляют собой
URL, sitemap, YML-фид, CSV, Markdown или HTML, а не JSON-документы, — поставьте
задание загрузки в очередь. Оно сразу отвечает status: queued.
createIngestJob — POST /ingest-jobs:
const job = await client.ingest.createIngestJob({
collection: "products",
source: "documents", // или url, sitemap, yml, youtube, csv, markdown, html
action: "upsert",
documents: [{ id: "42", price: 79.0 }], // до 100 документов в запросе
});Опрашивайте getIngestJob (GET /ingest-jobs/{jobId}), пока status не станет
ready или failed:
let current = job;
while (current.status === "queued" || current.status === "processing") {
await new Promise((resolve) => setTimeout(resolve, 2000));
current = await client.ingest.getIngestJob({ jobId: job.id });
}
console.log(current.stats); // { fetched, imported, failed, errors: [{ id, error }] }stats.errors перечисляет каждый незаписанный документ. retryIngestJob
(POST /ingest-jobs/{jobId}/retry) перезапускает неудавшееся задание,
retryFailedIngestJobs (POST /ingest-jobs/retry-failed) — все неудавшиеся,
а cancelIngestJob (POST /ingest-jobs/{jobId}/cancel) останавливает задание.
listIngestJobs (GET /ingest-jobs) выводит недавние задания, сначала
новые; deleteIngestJob (DELETE /ingest-jobs/{jobId}) удаляет запись о
задании, не трогая уже импортированные документы. Большие файлы YML-фидов
загружаются через uploadIngestFeed — см. YML-фиды.
Сначала посмотреть и подтвердить набор данных
Перед тем как фиксировать схему коллекции, previewIngest
(POST /ingest/preview) анализирует ограниченный встроенный образец — до
2 МиБ, 200 полей на объект, восемь уровней вложенности, 20 000
просмотренных значений — и предлагает роли полей и сопоставление, не
создавая коллекцию, не записывая документы, не обращаясь по внешним URL и не
генерируя эмбеддинги:
const preview = await client.ingest.previewIngest({
source: "documents",
documents: [{ id: "42", title: "Дрель аккумуляторная 18 В", price: 89 }],
});
// preview.profile, preview.proposal, preview.capabilitiesИсточник documents требует documents и запрещает content; текстовые
источники (markdown, csv, html) требуют непустой content и запрещают
documents. roles явно переопределяет предполагаемую роль поля по его
пути. Предложения описывают образец, а не гарантию — ничего не проверяется
по реальному движку, пока вы не примените их.
Для существующей коллекции confirmDatasetProfile
(PUT /collections/{collectionName}/profile) заново анализирует образец с
вашими явными переопределениями и сохраняет результат, отклоняя
сопоставление, которое конфликтует с реальной схемой коллекции или называет
поле, которого в ней нет; документы при этом всё ещё не импортируются, а
схема движка не меняется. getDatasetProfile
(GET /collections/{collectionName}/profile) читает подтверждённый профиль
обратно — сохранённый профиль не доказывает готовность коллекции к работе,
а только то, что его роли были проверены.
Переиндексация
На месте
reindexCollection — POST /collections/{collectionName}/reindex — ставит в
очередь задание reindex: оно экспортирует все документы коллекции и
импортирует их обратно (action, по умолчанию upsert). Используйте его, чтобы
применить изменения сопоставления полей или эмбеддингов. Возвращается задание
загрузки — опрашивайте его как любое другое.
const reindex = await client.collections.reindexCollection({ collectionName: "products" });Без простоя через алиас
Если изменение нельзя применить к существующей коллекции (новый тип поля, другой токенизатор), соберите новую коллекцию рядом со старой и переключите трафик алиасом — именем, которое указывает на коллекцию, как символическая ссылка. Поиски, импорт и виджеты, использующие имя алиаса, не меняются.
С самого начала направляйте приложение на алиас:
await client.collections.upsertAlias({ aliasName: "products", collection_name: "products_v1" });
// поиск, виджет и синхронизация используют "products"Чтобы переиндексировать:
Создайте новую коллекцию
products_v2 с новой схемой (createCollection).
Импортируйте в неё все данные
importDocuments, повторяя неудачные строки. Пока это идёт, записывайте
текущие изменения в обе коллекции.
Проверьте её
searchCollection напрямую по products_v2.
Переключите
upsertAlias (PUT /aliases/{aliasName}) перенаправляет алиас одним вызовом:
curl -X PUT https://api.aacsearch.com/v1/aliases/products \
-H "X-NOVA-API-KEY: $NOVA_API_KEY" -H "Content-Type: application/json" \
-d '{"collection_name":"products_v2"}'Удалите старую коллекцию, когда убедитесь, что всё в порядке
deleteCollection (DELETE /collections/products_v1). До этого откат — это
ещё один upsertAlias на products_v1.
getAlias показывает, куда указывает алиас; getAliases выводит список всех.
deleteAlias (DELETE /aliases/{aliasName}) удаляет алиас — коллекция, на
которую он указывал, не затрагивается.
Проверка перед запуском
Проверьте данные, доступ, выдачу, восстановление и эксплуатацию конкретными проверками перед подключением посетителей к поиску.
Разбор датасета и схема
Отправьте образец данных, получите профиль, предложение схемы коллекции и ролей полей, проверьте доступные возможности и подтвердите профиль.