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
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build --output-on-failureLink 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
#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 byelips::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
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
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, returnsstd::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()— effectiveConfig.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
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
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
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
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
flockonLOCK; read-only opens take a shared lock. See Lock manager. - RAII everywhere.
LockManagerreleases on destruction,Transactionauto-rolls back if not committed,ElipsInstance's destructor checkpoints and swallows exceptions (Core Guideline E.16). - Not thread-safe. A single
ElipsInstanceassumes a single thread of mutation. The locking model is process-level, not intra-process. - Move-only.
ElipsInstanceis non-copyable; passstd::unique_ptr<ElipsInstance>by move.
Errors
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.
Vaultdepends onIndexPort, never on a concrete index; the composition root ismake_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, andGpuIndexPortso each consumer depends only on the slice it needs. - Purpose-built errors. One root, narrow subclasses, and
std::expectedfor GPU paths where failure is expected rather than exceptional.
Pitfalls
- Forgetting to
commit()aTransaction— the destructor will roll it back. This is intentional, not a bug. - Calling
seek_text/place_documentwithout a configured text embedder — raisesConfigErrorwith 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 owningElipsInstancehas been moved or destroyed — references are non-owning and the instance is the lifetime root.