defmodule HNSWLib.BFIndex do
@moduledoc """
Documentation for `HNSWLib.BFIndex`.
"""
defstruct [:space, :dim, :reference]
alias __MODULE__, as: T
alias HNSWLib.Helper
@doc """
Construct a new BFIndex
##### Positional Parameters
- *space*: `:cosine` | `:ip` | `:l2`.
An atom that indicates the vector space. Valid values are
- `:cosine`, cosine space
- `:ip`, inner product space
- `:l2`, L2 space
- *dim*: `non_neg_integer()`.
Number of dimensions for each vector.
- *max_elements*: `non_neg_integer()`.
Number of maximum elements.
"""
@spec new(:cosine | :ip | :l2, non_neg_integer(), non_neg_integer()) ::
{:ok, %T{}} | {:error, String.t()}
def new(space, dim, max_elements)
when (space == :l2 or space == :ip or space == :cosine) and is_integer(dim) and dim >= 0 and
is_integer(max_elements) and max_elements >= 0 do
with {:ok, ref} <- HNSWLib.Nif.bfindex_new(space, dim, max_elements) do
{:ok,
%T{
space: space,
dim: dim,
reference: ref
}}
else
{:error, reason} ->
{:error, reason}
end
end
@doc """
Query the index with a single vector or a list of vectors.
##### Positional Parameters
- *query*: `Nx.Tensor.t() | binary() | [binary()]`.
A vector or a list of vectors to query.
If *query* is a list of vectors, the vectors must be of the same dimension.
##### Keyword Paramters
- *k*: `pos_integer()`.
Number of nearest neighbors to return.
"""
@spec knn_query(%T{}, Nx.Tensor.t() | binary() | [binary()], [
{:k, pos_integer()}
]) :: :ok | {:error, String.t()}
def knn_query(self, query, opts \\ [])
def knn_query(self = %T{}, query, opts) when is_binary(query) do
k = Helper.get_keyword!(opts, :k, :pos_integer, 1)
Helper.might_be_float_data!(query)
features = trunc(byte_size(query) / HNSWLib.Nif.float_size())
Helper.ensure_vector_dimension!(self, features, true)
_do_knn_query(self, query, k, nil, 1, features)
end
def knn_query(self = %T{}, query, opts) when is_list(query) do
k = Helper.get_keyword!(opts, :k, :pos_integer, 1)
filter = Helper.get_keyword!(opts, :filter, {:function, 1}, nil, true)
{rows, features} = Helper.list_of_binary(query)
Helper.ensure_vector_dimension!(self, features, true)
_do_knn_query(self, IO.iodata_to_binary(query), k, filter, rows, features)
end
def knn_query(self = %T{}, query = %Nx.Tensor{}, opts) do
k = Helper.get_keyword!(opts, :k, :pos_integer, 1)
filter = Helper.get_keyword!(opts, :filter, {:function, 1}, nil, true)
{f32_data, rows, features} = Helper.verify_data_tensor!(self, query)
_do_knn_query(self, f32_data, k, filter, rows, features)
end
defp _do_knn_query(self, query, k, filter, rows, features) do
case HNSWLib.Nif.bfindex_knn_query(self.reference, query, k, filter, rows, features) do
{:ok, labels, dists, rows, k, label_bits, dist_bits} ->
labels = Nx.reshape(Nx.from_binary(labels, :"u#{label_bits}"), {rows, k})
dists = Nx.reshape(Nx.from_binary(dists, :"f#{dist_bits}"), {rows, k})
{:ok, labels, dists}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Add items to the index.
##### Positional Parameters
- *data*: `Nx.Tensor.t()`.
Data to add to the index.
##### Keyword Parameters
- *ids*: `Nx.Tensor.t() | [non_neg_integer()] | nil`.
IDs to assign to the data.
If `nil`, IDs will be assigned sequentially starting from 0.
Defaults to `nil`.
"""
@spec add_items(%T{}, Nx.Tensor.t(), [
{:ids, Nx.Tensor.t() | [non_neg_integer()] | nil}
]) :: :ok | {:error, String.t()}
def add_items(self, data, opts \\ [])
def add_items(self = %T{}, data = %Nx.Tensor{}, opts) when is_list(opts) do
ids = Helper.normalize_ids!(opts[:ids])
{f32_data, rows, features} = Helper.verify_data_tensor!(self, data)
HNSWLib.Nif.bfindex_add_items(self.reference, f32_data, ids, rows, features)
end
@doc """
Delete vectors with the given labels from the index.
"""
def delete_vector(self = %T{}, label) when is_integer(label) do
HNSWLib.Nif.bfindex_delete_vector(self.reference, label)
end
@doc """
Save current index to disk.
##### Positional Parameters
- *path*: `Path.t()`.
Path to save the index to.
"""
@spec save_index(%T{}, Path.t()) :: {:ok, integer()} | {:error, String.t()}
def save_index(self = %T{}, path) when is_binary(path) do
HNSWLib.Nif.bfindex_save_index(self.reference, path)
end
@doc """
Load index from disk.
##### Positional Parameters
- *space*: `:cosine` | `:ip` | `:l2`.
An atom that indicates the vector space. Valid values are
- `:cosine`, cosine space
- `:ip`, inner product space
- `:l2`, L2 space
- *dim*: `non_neg_integer()`.
Number of dimensions for each vector.
- *path*: `Path.t()`.
Path to load the index from.
##### Keyword Parameters
- *max_elements*: `non_neg_integer()`.
Maximum number of elements to load from the index.
If set to 0, all elements will be loaded.
Defaults to 0.
"""
@spec load_index(:cosine | :ip | :l2, non_neg_integer(), Path.t(), [
{:max_elements, non_neg_integer()}
]) :: {:ok, %T{}} | {:error, String.t()}
def load_index(space, dim, path, opts \\ [])
when (space == :l2 or space == :ip or space == :cosine) and is_integer(dim) and dim >= 0 and
is_binary(path) and is_list(opts) do
max_elements = Helper.get_keyword!(opts, :max_elements, :non_neg_integer, 0)
with {:ok, ref} <- HNSWLib.Nif.bfindex_load_index(space, dim, path, max_elements) do
{:ok,
%T{
space: space,
dim: dim,
reference: ref
}}
else
{:error, reason} ->
{:error, reason}
end
end
end