Library Guide
The CLIs are thin shells over a public Pascal API. Use that API directly when you want to embed RAG inside your own program rather than shell out.
Setup
Add the project's src/ directory to your unit search path. The
units are organized by domain:
uses
mormot.db.pgvector, // codec
mormot.db.pgvector.binding, // bind / scan / DDL / schema
mormot.db.pgvector.orm, // TOrm helper (optional)
mormot.ai.embed, // IEmbeddingClient + transport
mormot.ai.embed.ollama, // Ollama embedder
mormot.ai.llm, // ILlmClient + stub
mormot.ai.llm.ollama, // Ollama LLM
mormot.ai.chunk, // Pascal source chunker
mormot.ai.rag, // orchestrator
mormot.ai.cli; // CLI harness
Building a vector
var v: TPgVector;
begin
v := TPgVector.Create([1.0, 2.0, 3.0]);
WriteLn('dim = ', v.Dim);
WriteLn(PgVectorToText(v)); // [1,2,3]
end;
The TPgVector value type is immutable, copies its input in the
constructor, and supports indexed read access via the default property:
WriteLn(v[0]); // 1.0
WriteLn(v[v.Dim - 1]); // 3.0
Convert to and from the wire formats:
text := PgVectorToText(v); // '[1,2,3]'
back1 := PgVectorFromText(text); // round-trip OK
bytes := PgVectorToBinary(v); // 16-byte big-endian payload
back2 := PgVectorFromBinary(bytes); // round-trip OK
Truncate to a Matryoshka dimension:
short := PgVectorTruncate(v, 256); // first 256 elements
Wiring up the database
Use mORMot 2's PostgreSQL connection properties; the binding helpers attach to whatever connection you already have:
var
props: TSqlDBPostgresConnectionProperties;
cfg: TPgVectorSchemaConfig;
begin
SynDBPostgresLibrary :=
'C:\Program Files\PostgreSQL\18\bin\libpq.dll';
props := TSqlDBPostgresConnectionProperties.Create(
'localhost:5432', 'mormot2_pgvector_test',
'postgres', 'password');
cfg := PgVectorSchemaConfigDefault; // table = mormot_chunks
// dim = 768
// hnsw (m=16, ef_construction=64)
PgVectorSetupSchema(props.MainConnection, cfg);
end;
Override any field of cfg before the call to provision a different
table layout.
Binding a vector to a SQL parameter
The bind helpers wrap the mORMot BindBlob / BindTextU paths:
stmt := props.NewThreadSafeStatementPrepared(
'INSERT INTO mormot_chunks (..., embedding) ' +
'VALUES (..., ?::vector)', false, true);
PgVectorBindBinary(stmt.Instance, 10, v); // paramFormat=1 binary
stmt.ExecutePrepared;
The SQL placeholder is ?::vector (mORMot translates ? to $N
automatically; the ::vector cast tells PostgreSQL to interpret the
binary bytes as a vector(N) value).
For text bind:
PgVectorBindText(stmt.Instance, 10, v);
For symmetry, the PgVectorBind dispatcher takes a TPgVectorBindMode
(vbmBinary or vbmText) and calls the right helper.
To scan a result row:
v := PgVectorScan(stmt.Instance, 0); // column 0
The scan path always reads the column as text and parses through
PgVectorFromText, which is the format pgvector returns by default
under libpq.
Embedding text
var
http: IHttpTransport;
emb: IEmbeddingClient;
v: TPgVector;
begin
http := TMormotHttpTransport.Create({receive_timeout_ms=}120000);
emb := TOllamaEmbeddingClient.Create(http,
'http://localhost:11434', 'embeddinggemma');
v := emb.Embed('How do I serialize a TOrm to JSON?');
end;
Batch:
inputs := ['first', 'second', 'third'];
vectors := emb.EmbedBatch(inputs); // length = 3
For production use, wrap the transport in retry + concurrency-limit decorators:
http := TConcurrencyLimitedHttpTransport.Create(
TRetryingHttpTransport.Create(
TMormotHttpTransport.Create(120000),
{max_attempts=}3, {initial_delay_ms=}250),
{limit=}4);
Chunking source
var
files: TRawUtf8DynArray;
chunks: TPascalChunkDynArray;
i: integer;
begin
files := ChunkerEnumerate('/path/to/src'); // walks .pas/.pp/.dpr/.inc
for i := 0 to High(files) do
begin
chunks := ChunkerDecompose(files[i]);
// chunks[k].UnitName, .Family, .Symbol, .Kind,
// .FilePath, .LineStart, .LineEnd,
// .Content, .Tokens
end;
end;
The default options split chunks above 800 tokens with a 4-line
overlap between adjacent slices. Override via TPascalChunkerOptions.
Running the full RAG pipeline
The orchestrator composes the codec + binding + embedder + chunker:
var
orch: TRagOrchestrator;
stats: TRagIngestStats;
results: TRagRetrievalResultDynArray;
begin
orch := TRagOrchestrator.Create(props, emb, cfg);
try
orch.SetupSchema;
stats := orch.Ingest('/path/to/mORMot2/src/orm');
WriteLn('files=', stats.FilesProcessed,
' chunks=', stats.ChunksEmitted,
' rows=', stats.RowsInserted,
' elapsed_ms=', stats.ElapsedMs);
results := orch.Retrieve(
'How do I serialize a TOrm to JSON?',
{top_k=}5,
{family=}'', // any
{alpha=}0.7);
for r in results do
WriteLn(r.UnitName, ':', r.LineStart, '-', r.LineEnd,
' (vsim=', r.VectorSimilarity:0:4, ')');
finally
orch.Free;
end;
end;
For parallel ingest with multiple worker threads:
function MakeEmbedder: IEmbeddingClient;
var http: IHttpTransport;
begin
// each worker gets its own transport because the persistent
// socket inside TMormotHttpTransport is not thread-safe
http := TMormotHttpTransport.Create(120000);
Result := TOllamaEmbeddingClient.Create(http, base_url, model, dim);
end;
stats := orch.IngestParallel('/path/to/src', MakeEmbedder,
{workers=}4);
Using the ORM helper
For TOrm-style code that wants the embedding to live next to other
metadata in a strongly-typed class, derive from TOrmWithEmbedding:
type
TOrmKnowledgeChunk = class(TOrmWithEmbedding)
protected
fLabel: RawUtf8;
fScore: integer;
published
property LabelName: RawUtf8 read fLabel write fLabel;
property Score: integer read fScore write fScore;
end;
initialization
PgVectorOrmRegister(TOrmKnowledgeChunk, 768);
Use the registered class in CRUD and DDL flows:
// CREATE TABLE with VECTOR(768) instead of BYTEA for Embedding
sql := PgVectorOrmCreateTableSql(TOrmKnowledgeChunk);
props.ExecuteNoResult(sql, []);
// build an instance and store its embedding
chunk := TOrmKnowledgeChunk.Create;
try
chunk.LabelName := 'demo';
chunk.Score := 42;
chunk.SetVector(emb.Embed('demo content'));
// ... save via mORMot ORM Add() ...
finally
chunk.Free;
end;
The base class publishes Embedding as RawByteString. Read-back is
byte-identical to what PgVectorToBinary produced; the GetVector
accessor decodes back to a TPgVector value.
CLI harness
If you want to write your own command-line program with the same
conventions as the three shipped CLIs, build a TCliSpec and call
CliParse:
var
spec: TCliSpec;
res: TCliResult;
argv: TRawUtf8DynArray;
begin
spec.ProgramName := 'my_rag_tool';
spec.Synopsis := 'Indexes my code and answers questions.';
SetLength(spec.Flags, 2);
spec.Flags[0].Name := '--src';
spec.Flags[0].Kind := cfRequired;
spec.Flags[0].MetaVar := 'DIR';
spec.Flags[0].Description := 'source directory to ingest';
spec.Flags[1].Name := '--query';
spec.Flags[1].Kind := cfRequired;
spec.Flags[1].MetaVar := 'TEXT';
spec.Flags[1].Description := 'question to ask';
// Argv := all command-line args except ParamStr(0) ...
res := CliParse(argv, spec);
if res.HelpRequested then
Writeln(CliFormatUsage(spec))
else if not res.Success then
Writeln(ErrOutput, 'error: ', res.ErrorMessage)
else
DoWork(res.Values[0], res.Values[1]);
end;
The harness recognizes --name value and --name=value, supports
required / optional with default / boolean flag kinds, and produces a
stable usage block via CliFormatUsage.