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.