Platform

Collections

Index text, web pages and PDF, HTML or text files from storage into vector collections, then find passages with hybrid vector and keyword search, for retrieval and cited answers.

A collection is a searchable set of documents. You add text, a URL or a file from project storage; NestCore extracts the text, splits it into chunks, embeds them with your project's AI provider key, and keeps them in your project database. Search combines vector similarity with keyword matching and returns the best passages with their source, ready to show or to pass to a model. Threads can use collections as cited sources.

Before you start

  • Add a Gemini, OpenAI or OpenAI-compatible key in Project Settings → AI. Embedding uses your own key, like every AI gateway call.
  • Creating collections and adding shared documents needs the service-role key (or Studio). Signed-in users can search, and can add private documents to collections that allow it.
  • The data lives in your project database, in the ai_collections, ai_documents and ai_chunks tables, created on first use with row-level security.

Quick start

On your server, create a collection and add documents:

server.ts
import { createClient } from '@nestcore/node';

const admin = createClient({ apiKey: process.env.NESTCORE_SERVICE_KEY! });

await admin.collections.create({ name: 'handbook' });

const docs = admin.collections.from('handbook');
await docs.ingest({ text: '# Refunds\n\nRefunds are paid within 14 days to the original card.' }, { title: 'Policies', metadata: { team: 'support' } });
await docs.ingest({ url: 'https://example.com/guide.pdf' });
await docs.ingest({ storage: { bucket: 'docs', path: 'manuals/setup.pdf' } }, { wait: true });

In your app, search as the signed-in user:

TypeScript
const { data, error } = await client.collections.from('handbook').search('How do refunds work?', { k: 5 });

for (const r of data?.results ?? []) {
  console.log(r.document.title, r.highlight, r.score);
}

client.collections is in the Node.js SDK from version 0.14.0 and the web SDK (@nestcore/js) from version 0.8.0.

Flutter:

Dart
final results = await nestcore.collections.from('handbook').search('How do refunds work?', k: 5);

Create a collection

OptionDefaultDescription
namerequiredLowercase letters, digits, _ and -, starting with a letter or digit, up to 63 characters. Unique in the project.
modelthe provider's defaultEmbedding model. If you leave it out, the first document fixes it.
providerproject defaultgemini, openai or openai_compatible
chunkTokens400Chunk size in tokens (about 4 characters each), 50 to 2,000
chunkOverlap60Tokens repeated from the end of the previous chunk, 0 to less than half of chunkTokens
userIngestfalseLet signed-in users add documents that only they can read
metadata{}Any JSON for your own use

A second collection with the same name returns 409. collections.update(name, patch) changes userIngest, metadata, model and provider. collections.delete(name) deletes the collection with all its documents and chunks.

Add documents

Give exactly one source:

SourceExampleLimits
text{ text: 'Refunds are paid…' }Up to 5 MB of inline text
url{ url: 'https://example.com/guide.pdf' }A public http or https address, fetched by NestCore: HTML, PDF or text, up to 50 MB, 30 seconds and 5 redirects. Private and internal addresses are refused.
storage{ storage: { bucket: 'docs', path: 'guide.pdf' } }A file in project storage: PDF, HTML or a text format (Markdown, CSV, JSON, code and so on), up to 50 MB

Also optional: title (up to 300 characters), metadata (JSON you can filter on later) and, with the service key, ownerUserId to make the document private to one user.

ingest returns the document at once with status: "queued". A background worker extracts and embeds it, and the status moves to processing, then ready or failed (with error). Pass wait: true, or { timeoutMs, intervalMs }, to poll until it finishes (default 120 seconds, then a TIMEOUT error). waitFor(documentId) polls an existing document.

  • Text is split on headings and blank lines first, then sentences, then words, so a chunk is a readable passage.
  • Without a title, a web page's <title> or a storage file's name is used.
  • For URL and storage sources only the chunks are kept, not the downloaded file.
  • Images and Office files can't be read; they fail with "Cannot read text from a … file". A document with no text, or with more than 10,000 chunks, also fails.
  • A temporary failure is retried, up to 3 attempts in total.

Manage documents with listDocuments({ status, limit, offset }), getDocument(id), remove(id) (deletes its chunks too) and stats() (chunks, chunks with the current model, documents and failed documents).

TypeScript
const { data } = await client.collections.from('handbook').search('refund to original card', {
  k: 8,
  hybrid: true,
  filter: { team: 'support' },
  minSimilarity: 0.3,
});
OptionDefaultDescription
k8Results, 1 to 50
hybridtrueMerge vector and keyword ranks with reciprocal rank fusion (k = 60). false is vector only.
filternoneOnly documents whose metadata contains this object (JSON containment)
minSimilarity0Drop results below this cosine similarity. Keyword-only results are kept.

Each result has:

  • score: the fused rank score; results are sorted by it (by similarity when hybrid is false).
  • similarity: cosine similarity, or null for a passage found only by keywords.
  • keywordMatch: whether the keyword search found it.
  • highlight: the best-matching words, wrapped in **.
  • chunk: { id, ordinal, content, metadata }.
  • document: { id, title, sourceType, sourceRef, metadata }.

Only ready documents are searched. The query is embedded on every search: it is an ordinary AI gateway call that counts toward your per-minute limit and spend caps. A collection that has no model yet (no document processed) returns no results.

Private documents (userIngest)

With userIngest: true, a signed-in user can add documents to the collection. Such a document belongs to that user:

  • Only its owner finds it in search, and only its owner can delete it. Other users see the shared documents (those without an owner) and their own.
  • A storage source must be one of the user's own files.
  • The service key can add a private document for a user with ownerUserId.
TypeScript
await client.collections.from('notes').ingest({ text: 'My private note about the Q3 plan.' }, { wait: true });

Change the embedding model

collections.update('handbook', { model: 'text-embedding-3-small' }) switches the collection to a new model and re-embeds every chunk in the background. Until that finishes, search only finds chunks already re-embedded. stats() shows the progress as current_model_chunks of chunks.

Access rules

ActionSigned-in userService-role key
List, read and search collectionsYes (shared documents and their own)Yes (all documents)
Create, update or delete a collectionNo (403)Yes
Add a documentOnly with userIngest; the document is private to themYes, shared or for ownerUserId
Delete a documentTheir ownAny

The anon key alone is refused with 401: every search spends tokens on your key.

Limits and quotas

Embedding documents counts toward the workspace's monthly embedding allowance, shared with automatic embeddings: 50,000 tokens on Free, 1,000,000 on Starter and 10,000,000 on Pro. It is checked before each batch of 64 chunks; when it is used up, the document fails and its error says the monthly embedding allowance is reached. Search queries don't count toward it. See Plans & billing.

In Studio

Project Settings → AI → Collections lists collections with their model and document counts. You can create a collection, add a document (text, URL or storage file), watch document status and errors, run a Test search, change whether users can add documents, and delete documents or the collection. Changes need the workspace admin role.

From a terminal, nestcore ai collections list lists collections; the MCP tool is nestcore_list_collections.

HTTP API

App endpoints, under /api/project/{slug}/ai/v1. Send the anon key in apikey and the user's access token in Authorization: Bearer, or the service-role key.

MethodPathNotes
GET/collectionsList, with documents and pending counts
POST/collectionsCreate (service key)
GET/collections/{name}One collection
PATCH/collections/{name}{ userIngest?, metadata?, model?, provider? } (service key)
DELETE/collections/{name}Delete with all documents (service key)
GET/collections/{name}/statsChunk and document counts
POST/collections/{name}/documents{ text | url | storage, title?, metadata?, ownerUserId? }
GET/collections/{name}/documents?status=queued|processing|ready|failed&limit&offset
GET/collections/{name}/documents/{id}One document
DELETE/collections/{name}/documents/{id}Delete a document and its chunks
POST/collections/{name}/search{ query, k?, hybrid?, filter?, minSimilarity? }

Rows come back as stored, in snake_case (embed_model, chunk_count, owner_user_id). The search response is { results, model, usage: { tokensIn, costUsd } }.

Studio uses the same operations under /api/projects/:projectId/ai/collections with a Studio access token.

Errors

StatusWhen
400Not exactly one source, inline text over 5 MB, an invalid URL or name, bad chunk sizes, an empty query
401The anon key was sent without a user token
403A user tried to create or change a collection, or to add a document where userIngest is off
404Collection or document not found
409A collection with that name exists

Document problems found while processing (an unreadable file, a refused URL, the quota) don't fail the request; they set the document's status to failed and its error. Search can also fail with the AI gateway errors.