lib/zvex.ex

defmodule Zvex do
  @moduledoc """
  Elixir bindings for **zvec**, an in-process vector database.

  This module manages the zvec library lifecycle: initialization,
  shutdown, and version introspection. For collection operations, use
  the dedicated modules:

  | Module | Purpose |
  |---|---|
  | `Zvex.Collection` | Collection lifecycle and CRUD |
  | `Zvex.Collection.Schema` | Schema builder |
  | `Zvex.Document` | Typed document construction |
  | `Zvex.Query` | Fluent vector query builder |
  | `Zvex.Vector` | Vector packing/unpacking |
  | `Zvex.Config` | Library configuration |

  ## Quick Start

      alias Zvex.{Collection, Document, Query, Vector}
      alias Zvex.Collection.Schema

      # Initialize the library
      :ok = Zvex.initialize()

      # Define a schema
      schema =
        Schema.new("products")
        |> Schema.add_field("id", :string, primary_key: true)
        |> Schema.add_field("embedding", :vector_fp32,
             dimension: 128,
             index: [type: :hnsw, metric: :cosine])
        |> Schema.add_field("name", :string)

      # Create a collection and insert a document
      {:ok, coll} = Collection.create("/tmp/products", schema)

      doc =
        Document.new()
        |> Document.put_pk("prod-1")
        |> Document.put("name", "Widget")
        |> Document.put("embedding", Vector.from_list(List.duplicate(0.1, 128), :fp32))

      {:ok, _} = Collection.insert(coll, doc)

      # Query nearest neighbors
      query_vec = Vector.from_list(List.duplicate(0.1, 128), :fp32)

      {:ok, results} =
        Query.new()
        |> Query.field("embedding")
        |> Query.vector(query_vec)
        |> Query.top_k(5)
        |> Query.execute(coll)

      :ok = Collection.close(coll)
      :ok = Zvex.shutdown()

  ## Error Handling

  Every fallible function comes in two flavours:

  - `fun/n` returns `{:ok, result}` or `{:error, %Zvex.Error{}}`.
  - `fun!/n` returns the unwrapped result or raises.

  See `Zvex.Error` for the full error hierarchy.
  """

  @doc """
  Returns the zvec library version.

  ## Examples

      iex> version = Zvex.version()
      iex> is_integer(version.major) and is_integer(version.minor) and is_integer(version.patch)
      true
  """
  def version do
    %{
      major: Zvex.Native.version_major(),
      minor: Zvex.Native.version_minor(),
      patch: Zvex.Native.version_patch(),
      raw: Zvex.Native.version()
    }
  end

  @doc "Initializes the zvec library with default configuration."
  def initialize do
    Zvex.Native.initialize()
    |> Zvex.Error.from_native()
  end

  @doc "Initializes the zvec library with the given configuration."
  def initialize(%Zvex.Config{} = config) do
    with {:ok, _validated} <- Zvex.Config.validate(config) do
      config
      |> Zvex.Config.to_native_map()
      |> Zvex.Native.initialize_with_config()
      |> Zvex.Error.from_native()
    end
  end

  @doc "Initializes the zvec library. Raises on error."
  def initialize! do
    initialize() |> Zvex.Error.unwrap!()
  end

  @doc "Initializes the zvec library with the given configuration. Raises on error."
  def initialize!(%Zvex.Config{} = config) do
    initialize(config) |> Zvex.Error.unwrap!()
  end

  @doc "Shuts down the zvec library."
  def shutdown do
    Zvex.Native.shutdown() |> Zvex.Error.from_native()
  end

  @doc "Shuts down the zvec library. Raises on error."
  def shutdown! do
    shutdown() |> Zvex.Error.unwrap!()
  end

  @doc "Returns whether the zvec library is initialized."
  def initialized? do
    Zvex.Native.is_initialized()
  end

  @doc """
  Checks if the linked zvec library is compatible with the given version.

  Returns `true` if the library version is >= the requested version.

  ## Examples

      iex> Zvex.compatible?(0, 0, 1)
      true
  """
  @spec compatible?(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: boolean()
  def compatible?(major, minor, patch)
      when is_integer(major) and is_integer(minor) and is_integer(patch) do
    Zvex.Native.check_version(major, minor, patch)
  end
end