elips/docs
Reference · C++

C++ SDK

The C++ surface is the runtime's source of truth. It exposes typed configuration, document-aware records, planner introspection, persistence control, and optional GPU-backed indexes. Everything else — the Python bindings, the CLI, EQL — runs through the same headers.

Build & install

bash
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failure

Link against the produced static/shared library and include elips/elips.hpp. C++23 is required; the project targets the C++ Core Guidelines (see ADR-0001).

Minimal example

cpp
#include "elips/elips.hpp"

auto db = elips::open(
    ":memory:",
    elips::Config{}
        .dimension(2)
        .metric(elips::Metric::cosine));

auto& docs = db->vault("documents");
docs.place_document("alpha design note", {{"kind", std::string{"design"}}});
docs.place_document("beta incident runbook", {{"kind", std::string{"ops"}}});

const auto hits = docs.seek_text("alpha", 2);

New databases attach ELIPS' built-in local embedder automatically unless you disable it with Config::auto_text_embedder(false).

Primary types

  • elips::Config — fluent configuration builder.
  • elips::ElipsInstance — top-level database handle returned by elips::open(). Move-only, non-copyable.
  • elips::Vault — per-collection record store and query surface.
  • elips::Record, elips::DocumentAttachment, elips::ChunkInfo, elips::EmbeddingLineage — domain types.
  • elips::Filter — predicate tree used by both the fluent builder and EQL.
  • elips::QueryPlan — planner output for vector and hybrid queries.
  • elips::Transaction / elips::TransactionVault — atomic batched writes.
  • elips::TextEmbedderPort — pluggable text embedder interface.

Config

cpp
enum class Metric      { cosine, euclidean, dot_product };
enum class IndexType   { graph, exact };
enum class Durability  { paranoid, standard, relaxed, ephemeral };
enum class AccessMode  { read_write, read_only };

struct GraphParams {
    std::size_t max_connections{16};
    std::size_t ef_construction{200};
    std::size_t ef_search{50};
};

elips::Config{}
    .dimension(768)
    .metric(elips::Metric::cosine)
    .index(elips::IndexType::graph)
    .graph_params({.max_connections = 32, .ef_construction = 400, .ef_search = 100})
    .durability(elips::Durability::standard)
    .access_mode(elips::AccessMode::read_write)
    .segmented_storage(true)
    .metadata_acceleration(true)
    .auto_text_embedder(true);

Setters mirror getters one-for-one. Notable behaviour:dimension() must be non-zero for new persistent databases and every ":memory:" open; existing databases reopen with the persisted identity; access_mode(read_only) requires an existing database; metadata_acceleration(true) enables exact candidate narrowing through MetadataIndex.

ElipsInstance

cpp
std::unique_ptr<ElipsInstance> open(const std::string& path,
                                    const Config& config = {});
  • vault(name) — returns a reference, creating lazily.
  • list_vaults() — current vault names.
  • begin_transaction() — atomic write transaction.
  • query(eql, bindings={}) — single EQL statement, returns std::vector<SearchResult>.
  • checkpoint() — flush manifest+segments (or snapshot) and truncate the WAL.
  • compact() — rebuild every vault index from stored records and checkpoint.
  • close() — graceful shutdown: checkpoint, detach WAL, release lock.
  • abandon() — testing hook that suppresses destructor checkpointing.
  • config() — effective Config.
  • gpu_info() / gpu_stats() — only in GPU builds.

Persistent instances checkpoint on destruction unless already closed or opened read-only. Read-only instances never attach a WAL writer; vaults under a read-only instance are immediately marked read-only.

Vault

cpp
RecordID place(const Vector& vector,
               Payload payload = {},
               std::optional<RecordID> id = std::nullopt,
               std::optional<DocumentAttachment> document = std::nullopt,
               std::optional<ChunkInfo> chunk = std::nullopt,
               std::optional<EmbeddingLineage> lineage = std::nullopt);

RecordID place_document(std::string text,
                        Payload payload = {},
                        std::optional<RecordID> id = std::nullopt,
                        std::optional<ChunkInfo> chunk = std::nullopt,
                        std::optional<EmbeddingLineage> lineage = std::nullopt);

void place_many(const std::vector<Record>& records);

bool erase(const RecordID& id);
std::optional<Record> fetch(const RecordID& id) const;
std::vector<Record> scan(const Filter& filter = {},
                         std::size_t offset = 0,
                         std::size_t limit  = std::numeric_limits<std::size_t>::max()) const;

VaultInfo info() const;
void      rebuild_index();

Query & planner

cpp
std::vector<SearchResult> seek       (const Vector& q, std::size_t top,
                                      const Filter& = {},
                                      std::optional<float> threshold = std::nullopt) const;

std::vector<SearchResult> seek_text  (std::string_view text, std::size_t top,
                                      const Filter& = {},
                                      std::optional<float> threshold = std::nullopt) const;

std::vector<SearchResult> seek_hybrid(const Vector& q, std::string_view text,
                                      std::size_t top, const Filter& = {},
                                      std::optional<float> threshold = std::nullopt,
                                      float lexical_weight = 0.25F) const;

QueryPlan explain_seek(const Vector& q, std::size_t top,
                       const Filter& = {},
                       std::optional<float> threshold = std::nullopt,
                       bool has_text_component = false) const;

Every vector or hybrid query passes through Vault::plan_seek() first. QueryPlan exposes the chosen strategy (ann_index, exact_candidates, full_scan, text_probe, hybrid_fusion), candidate count, the metadata acceleration flag, the GPU flag, and the index type name. SearchResult carries id, distance, data, document, chunk, and lineage hydrated from the authoritative record store.

Transactions

cpp
auto txn = db->begin_transaction();
txn.vault("documents").place(elips::Vector{{1.0F, 0.0F}});
txn.commit();   // or txn.rollback();

Transaction is RAII: if the destructor runs without an explicit commit() or rollback(), it calls rollback() automatically — buffered operations are discarded. enqueue_place validates dimension and finiteness eagerly, so commit() never fails mid-batch on validation grounds. See Transaction engine.

Embedders

cpp
class TextEmbedderPort {
public:
    virtual ~TextEmbedderPort() = default;
    virtual Vector embed(std::string_view text) const = 0;
    virtual std::vector<Vector> embed_batch(
        const std::vector<std::string>& texts) const;
    virtual std::string_view provider_name() const noexcept = 0;
    virtual std::string_view model_name()    const noexcept = 0;
    virtual std::string_view revision_name() const noexcept;
    virtual std::string_view backend_name()  const noexcept;
    virtual std::uint16_t    output_dimension() const noexcept;
};

The built-in local embedder is rehydratable — its identity lives in TEXT_EMBEDDER.manifest plus an artifact under text_embedder/. Custom TextEmbedderPort implementations work through Config::text_embedder(...), but ELIPS can only persist their metadata; a later reopen must provide the same embedder before text-first APIs can be used again.

Persistence

Two on-disk layouts. Segmented (default) writes a root elips.manifest plus one segment file per vault under segments/. Snapshot mode writes a single elips.snapshot for compatibility. Every mutation is WAL-appended before the in-memory store changes; WAL replay rebuilds documents, chunks, and lineage on open. See Storage & recovery.

Locking, threading, ownership

  • Single writer, many readers. The writer holds an exclusive flock on LOCK; read-only opens take a shared lock. See Lock manager.
  • RAII everywhere. LockManager releases on destruction, Transaction auto-rolls back if not committed, ElipsInstance's destructor checkpoints and swallows exceptions (Core Guideline E.16).
  • Not thread-safe. A single ElipsInstance assumes a single thread of mutation. The locking model is process-level, not intra-process.
  • Move-only. ElipsInstance is non-copyable; pass std::unique_ptr<ElipsInstance> by move.

Errors

std::runtime_errorstdelips::ElipsErrorevery elips throw inherits thisDimensionMismatchInvalidVectorConfigErrorNotFoundStorageErrorLockConflictparallel surfaceselips::eql::ParseError — EQL surfacestd::expected<T, GpuError> — GPU surface
Six concrete throws, one base. EQL parsing and GPU calls deliberately sit on their own surfaces.

GPU configuration

Available only in GPU builds. Configure through Config::gpu(gpu::GpuConfig{}). Supported algorithms include brute_force, ivf_flat, ivf_pq, and cagra. See Algorithms.

Design principles you can rely on

  • Dependency Inversion. Vault depends on IndexPort, never on a concrete index; the composition root is make_index().
  • RAII. Locks, transactions, and the instance are bound to scope; nothing leaks on exception unwinding.
  • Interface Segregation. The GPU engine splits into GpuPort, GpuMemoryPort, GpuKernelPort, GpuStreamPort, and GpuIndexPort so each consumer depends only on the slice it needs.
  • Purpose-built errors. One root, narrow subclasses, and std::expected for GPU paths where failure is expected rather than exceptional.

Pitfalls

  • Forgetting to commit() a Transaction — the destructor will roll it back. This is intentional, not a bug.
  • Calling seek_text / place_document without a configured text embedder — raises ConfigError with an actionable message; ELIPS never silently switches to lexical-only behaviour.
  • Opening the same database twice for writing — the second open raises LockConflict.
  • Reusing a Vault& after the owning ElipsInstance has been moved or destroyed — references are non-owning and the instance is the lifetime root.