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->collection("documents");
docs.add_document("alpha design note", {{"kind", std::string{"design"}}});
docs.add_document("beta incident runbook", {{"kind", std::string{"ops"}}});

const auto hits = docs.search_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::Database — top-level database handle returned by elips::open(). Move-only, non-copyable. The elips::Client / elips::PersistentClient wrappers layer a Chroma-style surface over it.
  • elips::Collection — 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::TransactionCollection — atomic batched writes.
  • elips::TextEmbedderPort — pluggable text embedder interface.

Config

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

struct HnswParams {
    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::hnsw)
    .hnsw_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.

IndexType::hnsw selects the graph index (IndexHNSW) and IndexType::flat selects exact brute force (IndexFlat). ELIPS also exposes FAISS-style index classes directly — Index (base), IndexFlat, IndexHNSW, IndexIVFFlat, IndexIVFPQ, IndexIVFScalarQuantizer — plus the index_factory helper for building one from a spec string.

Database

cpp
std::unique_ptr<Database> open(const std::string& path,
                               const Config& config = {});
  • collection(name) — returns a reference, creating lazily.
  • list_collections() — current collection 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 collection 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; collections under a read-only instance are immediately marked read-only.

Collection

cpp
RecordID add(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 add_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 add_many(const std::vector<Record>& records);

bool remove(const RecordID& id);
std::optional<Record> get(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;

CollectionInfo info() const;
void           rebuild_index();

Query & planner

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

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

std::vector<SearchResult> hybrid_search(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_query(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 Collection::plan_search() 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, payload, document, chunk, lineage, and the codec label, hydrated from the authoritative record store.

Transactions

cpp
auto txn = db->begin_transaction();
txn.collection("documents").add(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_add 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 collection 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, Database's destructor checkpoints and swallows exceptions (Core Guideline E.16).
  • Not thread-safe. A single Database assumes a single thread of mutation. The locking model is process-level, not intra-process.
  • Move-only. Database is non-copyable; pass std::unique_ptr<Database> by move.

Errors

std::runtime_errorstdelips::ElipsErrorevery elips throw inherits thisDimensionMismatchInvalidVectorConfigErrorNotFoundStorageErrorLockConflictparallel surfaces↳ elips::eql::ParseError — EQL surface↳ std::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{}) or by selecting a device. The GPU engine is built around GpuBackend (the backend abstraction), BackendSelector (device resolution and fallback), and QueryBatcher (coalesces concurrent queries into batched kernel launches). Backends: Metal (implemented on Apple (Beta, M4-validated); distance and top-k kernels), CUDA (experimental), HIP/ROCm (experimental), and CPU (always available). GPU paths accelerate exact brute force and the IVF family; the HNSW graph index remains CPU-resident. See Algorithms.

Design principles you can rely on

  • Dependency Inversion. Collection depends on the Index base interface, never on a concrete index; the composition root is make_index() / index_factory().
  • RAII. Locks, transactions, and the instance are bound to scope; nothing leaks on exception unwinding.
  • Interface Segregation. The GPU engine splits into GpuBackend, 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 search_text / add_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 Collection& after the owning Database has been moved or destroyed — references are non-owning and the instance is the lifetime root.