Current section

Files

Jump to
kvex src kvex.erl
Raw

src/kvex.erl

%%% @doc
%%% kvex — approximate k-NN vector search on the BEAM.
%%%
%%% Backed by the TurboQuant algorithm (Google Research, ICLR 2026) via
%%% the `turbovec' Rust crate. Vectors are compressed to 2–4 bits per
%%% coordinate; no training phase, incremental inserts supported.
%%%
%%% == Quick start ==
%%% ```
%%% {ok, Ix} = kvex:new(128),
%%% 0 = kvex:size(Ix),
%%% ok = kvex:delete(Ix).
%%% '''
%%% @end
-module(kvex).
-export([version/0, new/1, new/2, delete/1, size/1, add/3, add_batch/2, search/3]).
-type index() :: reference().
-type opts() :: #{bits => 2 | 3 | 4}.
-type id() :: non_neg_integer() | binary().
-type vector() :: [float()] | binary().
-export_type([index/0, opts/0, id/0, vector/0]).
-define(DEFAULT_BITS, 4).
-spec version() -> binary().
%% @doc Returns the library version.
%%
%% Example:
%% ```
%% <<"0.1.0">> = kvex:version().
%% '''
version() ->
<<"0.1.0">>.
-spec new(Dim :: pos_integer()) -> {ok, index()} | {error, term()}.
%% @doc Equivalent to `new(Dim, #{bits => 4})'.
%%
%% Example:
%% ```
%% {ok, Ix} = kvex:new(128).
%% '''
new(Dim) ->
new(Dim, #{}).
-spec new(Dim :: pos_integer(), opts()) -> {ok, index()} | {error, term()}.
%% @doc Creates an empty index of dimension `Dim'.
%%
%% `Dim' must be a positive integer multiple of 8. The `bits' option
%% selects TurboQuant's bit-width per coordinate (2, 3 or 4 — default 4).
%%
%% Returns `{ok, Ref}' where `Ref' is an opaque handle, or
%% `{error, {bad_dim, Dim}}' / `{error, {bad_option, bits, Value}}'.
%%
%% Example:
%% ```
%% {ok, Ix} = kvex:new(128, #{bits => 2}),
%% {error, {bad_dim, 7}} = kvex:new(7).
%% '''
new(Dim, Opts) when is_integer(Dim), is_map(Opts) ->
Bits = maps:get(bits, Opts, ?DEFAULT_BITS),
kvex_nif:new_index(Dim, Bits).
-spec size(index()) -> non_neg_integer().
%% @doc Returns the number of indexed vectors.
%%
%% Example:
%% ```
%% {ok, Ix} = kvex:new(128),
%% 0 = kvex:size(Ix).
%% '''
size(Ref) ->
kvex_nif:size(Ref).
-spec delete(index()) -> ok.
%% @doc Explicitly drops the index reference. Equivalent to letting the
%% reference be garbage-collected; useful for tests and for predictable
%% native memory release.
%%
%% Example:
%% ```
%% {ok, Ix} = kvex:new(128),
%% ok = kvex:delete(Ix).
%% '''
delete(_Ref) ->
ok.
-spec add(index(), id(), vector()) -> ok | {error, term()}.
%% @doc Inserts a single vector under the given id.
%%
%% Ids may be non-negative integers or binaries (e.g. UUIDs). Vectors
%% may be passed as a list of floats or as a little-endian f32 binary
%% of length `4 * Dim'.
%%
%% Returns `ok' on success, or `{error, {dim_mismatch, Expected, Got}}'.
%%
%% Example:
%% ```
%% {ok, Ix} = kvex:new(128),
%% Vec = [rand:uniform() || _ <- lists:seq(1, 128)],
%% ok = kvex:add(Ix, 42, Vec).
%% '''
add(Ref, Id, Vec) when is_list(Vec); is_binary(Vec) ->
kvex_nif:add_vec(Ref, Id, Vec).
-spec add_batch(index(), [{id(), vector()}]) -> ok | {error, term()}.
%% @doc Inserts a list of `{Id, Vector}' pairs atomically.
%%
%% If any vector has the wrong dimension the whole batch is rejected
%% with `{error, {dim_mismatch, Position, Expected, Got}}' where
%% `Position' is the 0-based index of the offending entry.
%%
%% Runs on a dirty CPU scheduler — safe to call with large batches
%% without blocking a BEAM scheduler thread.
%%
%% Example:
%% ```
%% {ok, Ix} = kvex:new(128),
%% Pairs = [{I, [rand:uniform() || _ <- lists:seq(1, 128)]}
%% || I <- lists:seq(1, 10000)],
%% ok = kvex:add_batch(Ix, Pairs).
%% '''
add_batch(Ref, Pairs) when is_list(Pairs) ->
kvex_nif:add_batch(Ref, Pairs).
-spec search(index(), Query :: vector(), K :: pos_integer()) ->
{ok, [{id(), Score :: float()}]} | {error, term()}.
%% @doc Returns up to `K' most-similar vectors to `Query', sorted
%% descending by score (higher = more similar).
%%
%% Scores are raw TurboQuant similarity scores — monotone with inner
%% product on the rotated / quantized representation. They are
%% comparable within a single index but not calibrated to any specific
%% metric. To get cosine similarity, L2-normalize vectors before `add'
%% and before `search'.
%%
%% Errors: `{error, empty_index}' if the index has no vectors,
%% `{error, {dim_mismatch, Expected, Got}}' on size mismatch.
%%
%% Example:
%% ```
%% {ok, Ix} = kvex:new(128),
%% ok = kvex:add(Ix, 1, Vec),
%% {ok, Results} = kvex:search(Ix, Query, 10).
%% '''
search(Ref, Query, K) when (is_list(Query) orelse is_binary(Query)),
is_integer(K), K > 0 ->
kvex_nif:search_vec(Ref, Query, K).