Agora
Retrieval (RAG)

pgvector store

The production RAG store — cosine/L2/inner similarity over a vector(N) column on Postgres + pgvector, through @adonisjs/lucid, with a published migration and safe identifier handling.

retrievers.pgvector is the production retriever: it stores chunk embeddings in a vector(N) column and ranks them with pgvector's distance operators over @adonisjs/lucid. Postgres + the pgvector extension only. The Lucid peer is imported lazily inside the factory thunk, so it stays optional.

Run the migration

node ace configure @adonis-agora/agent publishes a create_agent_rag_chunks migration alongside the core tables. Apply it (Postgres):

node ace migration:run

It provisions the vector extension, the chunk table (default agent_rag_chunks), and the metric's index opclass.

Wire the retriever

config/agent.ts
import { defineConfig, retrievers } from '@adonis-agora/agent'

export default defineConfig({
  model: () => aiSdkModel(openai('gpt-4o-mini')),
  retriever: retrievers.pgvector({
    embedder: myEmbedder,
    dimension: 1536,
  }),
  retrievalTopK: 5,
})

Configuration

FieldDefaultMeaning
embedder—The EmbeddingProvider (query + ingestion), or a lazy factory. Required.
dbresolved lazilyA structural Lucid DB handle (bring-your-own). Omit to resolve @adonisjs/lucid's default db service inside the thunk.
connectiondefault connectionLucid connection name (used only when db is omitted).
tableagent_rag_chunksChunk table name. Validated against a strict identifier regex.
dimension1536Embedding width — must match the model (e.g. 1536 for text-embedding-3-small). A list ([768, 1536]) makes a mixed-dimension table; see below.
metriccosineSimilarity metric / distance operator (cosine, l2, inner).
columnsmirror defaultsOverride the physical column names (each validated).
nullableEmbeddingsfalseAllow chunks without a vector: embedding: [] is stored as NULL and skipped by vector search (full-text still finds it).
iterativeScanoff'strict_order' / 'relaxed_order': pgvector ≥ 0.8 keeps scanning the HNSW index until topK rows pass the filter. SET LOCAL in a transaction; skipped on older pgvector.
efSearchpgvector's (40)hnsw.ef_search for searches.
upsertBatchSize100Rows per multi-row INSERT … ON CONFLICT. Duplicate ids in one call keep the last.
fullTextoffAdd a Postgres full-text leg and return a HybridRetriever (see below).
ensureSchemafalseProvision the extension/table/index at boot (idempotent DDL). Handy for tests; production should run the migration.
documents—Documents to chunk → embed → upsert at boot.
chunkSize / overlap800 / 100Chunking parameters.

Identifiers are validated, embeddings are bound

The store composes raw SQL for pgvector's operators (Lucid has no native vector type), but every table/column name is validated against a strict identifier regex before it is ever spliced in, and the query embedding is passed as a ?::vector binding — never string-concatenated. Scores are normalized so a higher score always means more relevant, across all three metrics.

Hybrid search with Postgres full-text

fullText turns the store into a PgLexicalVectorStore and the retriever into a HybridRetriever that fuses vector search with full-text search (reciprocal rank fusion) over the same rows:

retriever: retrievers.pgvector({
  embedder: () => aiSdkEmbedding(openai.embedding('text-embedding-3-small')),
  fullText: { config: 'simple' }, // or { column: 'tsv' } for a tsvector column you maintain
}),

A question is searched by its meaningful terms: its stop words are dropped (English, Portuguese and Spanish lists by default, only the language(s) the question is written in; stopWords to change, or false to keep every word) and rows holding any remaining term are ranked by the sum of the matched terms' IDF (BM25 without term frequencies), ts_rank_cd breaking ties, so three rare terms beat a row full of "the/of/which". Quoted phrases and -word go through websearch_to_tsquery as written first; anyTermFallback: false keeps only that (every word must appear). Without column, the schema adds a GIN index over to_tsvector(config, text) — on a populated table, create it CONCURRENTLY in a migration (store.schemaStatements() prints it). Programmatically: new PgLexicalVectorStore(db, {...}) with new HybridRetriever([new EmbeddingRetriever(embedder, store), new LexicalRetriever(store)]). minScore does not apply to a fused result — a rank is not a similarity.

Several embedding models in one table

dimension: [768, 1536] makes the column an untyped vector with one partial HNSW index per width (USING hnsw ((embedding::vector(768)) …) WHERE vector_dims(embedding) = 768), and a search only compares vectors as wide as the query. Store the model in chunk metadata and filter on it when two models share a width.

Scores and ranking

Each metric maps to a pgvector operator (<=>, <->, <#>) and an index opclass. The store rewrites the raw distance into a monotonically-increasing relevance score on each returned Passage, so downstream code (inject-mode context, a reranker, your UI) can treat score uniformly regardless of the configured metric.

For the in-memory alternative and the retrieval SPIs, see RAG & retrieval.

On this page

Partner

Your product as the official integration for Agent, right here.

Become a partner →