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->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 byelips::open(). Move-only, non-copyable. Theelips::Client/elips::PersistentClientwrappers 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
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
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, returnsstd::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()— 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; collections under a read-only instance are immediately marked read-only.
Collection
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
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
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
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
flockonLOCK; read-only opens take a shared lock. See Lock manager. - RAII everywhere.
LockManagerreleases on destruction,Transactionauto-rolls back if not committed,Database's destructor checkpoints and swallows exceptions (Core Guideline E.16). - Not thread-safe. A single
Databaseassumes a single thread of mutation. The locking model is process-level, not intra-process. - Move-only.
Databaseis non-copyable; passstd::unique_ptr<Database>by move.
Errors
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.
Collectiondepends on theIndexbase interface, never on a concrete index; the composition root ismake_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, 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
search_text/add_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
Collection&after the owningDatabasehas been moved or destroyed — references are non-owning and the instance is the lifetime root.