Current section
Files
Jump to
Current section
Files
lib/embed_ex.ex
defmodule EmbedEx do
@moduledoc """
EmbedEx - Vector embeddings service for the NSAI ecosystem.
A unified interface for generating and working with text embeddings across
multiple providers (OpenAI, Cohere, local models) with built-in caching,
batch processing, and similarity computations.
## Features
* Multiple provider support with unified API
* Automatic caching with TTL
* Efficient batch processing with parallelization
* Vector similarity computations using Nx
* GPU acceleration support (when available)
## Quick Start
# Single embedding
{:ok, embedding} = EmbedEx.embed("Hello world", provider: :openai)
# Batch embeddings
{:ok, embeddings} = EmbedEx.embed_batch([
"First text",
"Second text",
"Third text"
], provider: :openai)
# Compute similarity
similarity = EmbedEx.cosine_similarity(embedding1, embedding2)
# Find similar embeddings
{:ok, results} = EmbedEx.find_similar(
query_embedding,
corpus_embeddings,
top_k: 5
)
## Configuration
config :embed_ex,
default_provider: :openai
config :embed_ex, :cache,
enabled: true,
ttl: :timer.hours(24),
limit: 10_000
# Provider configuration
config :embed_ex, :openai,
api_key: System.get_env("OPENAI_API_KEY"),
default_model: "text-embedding-3-small"
"""
alias EmbedEx.{Batch, Cache, Embedding, Providers, Similarity}
@type embed_opts :: [
provider: atom() | module(),
model: String.t(),
use_cache: boolean(),
api_key: String.t()
]
@doc """
Embeds a single text string.
## Options
* `:provider` - Provider to use (`:openai`, `:cohere`, `:local`, or module)
(default: configured default or `:openai`)
* `:model` - Model to use (provider-specific)
* `:use_cache` - Whether to use caching (default: `true`)
* Provider-specific options (e.g., `:api_key` for OpenAI)
## Examples
# Using default provider (OpenAI)
{:ok, embedding} = EmbedEx.embed("Hello world")
# Specifying provider and model
{:ok, embedding} = EmbedEx.embed(
"Hello world",
provider: :openai,
model: "text-embedding-3-large"
)
# Disable caching for this request
{:ok, embedding} = EmbedEx.embed("Hello world", use_cache: false)
"""
@spec embed(String.t(), embed_opts()) :: {:ok, Embedding.t()} | {:error, term()}
def embed(text, opts \\ []) when is_binary(text) do
provider = get_provider(opts)
use_cache = Keyword.get(opts, :use_cache, true)
if use_cache do
Cache.fetch(text, opts, fn ->
case provider.embed(text, opts) do
{:ok, embedding} ->
Cache.put(embedding, opts)
{:ok, embedding}
error ->
error
end
end)
else
provider.embed(text, opts)
end
end
@doc """
Embeds a batch of text strings.
Automatically handles chunking, parallel processing, and caching.
## Options
* `:provider` - Provider to use (default: configured default or `:openai`)
* `:batch_size` - Maximum batch size per request (default: provider max)
* `:concurrency` - Number of concurrent requests (default: `4`)
* `:use_cache` - Whether to use caching (default: `true`)
* `:on_progress` - Progress callback `(completed, total -> any())`
* Provider-specific options
## Examples
{:ok, embeddings} = EmbedEx.embed_batch([
"First text",
"Second text",
"Third text"
])
# With progress tracking
{:ok, embeddings} = EmbedEx.embed_batch(
texts,
provider: :openai,
on_progress: fn completed, total ->
IO.puts("Progress: \#{completed}/\#{total}")
end
)
# Higher concurrency
{:ok, embeddings} = EmbedEx.embed_batch(
texts,
provider: :openai,
concurrency: 10
)
"""
@spec embed_batch([String.t()], embed_opts()) :: {:ok, [Embedding.t()]} | {:error, term()}
def embed_batch(texts, opts \\ []) when is_list(texts) do
opts = Keyword.put_new(opts, :provider, get_provider_name(opts))
Batch.embed_batch(texts, opts)
end
@doc """
Computes cosine similarity between two embeddings.
Returns a float between -1 and 1, where 1 means identical vectors.
## Examples
similarity = EmbedEx.cosine_similarity(embedding1, embedding2)
# => 0.87
"""
@spec cosine_similarity(Embedding.t() | list(float()), Embedding.t() | list(float())) ::
float()
def cosine_similarity(emb1, emb2) do
Similarity.cosine_similarity(emb1, emb2)
end
@doc """
Computes Euclidean distance between two embeddings.
Lower values indicate more similar embeddings.
## Examples
distance = EmbedEx.euclidean_distance(embedding1, embedding2)
# => 0.23
"""
@spec euclidean_distance(Embedding.t() | list(float()), Embedding.t() | list(float())) ::
float()
def euclidean_distance(emb1, emb2) do
Similarity.euclidean_distance(emb1, emb2)
end
@doc """
Computes dot product between two embeddings.
## Examples
dot = EmbedEx.dot_product(embedding1, embedding2)
# => 32.5
"""
@spec dot_product(Embedding.t() | list(float()), Embedding.t() | list(float())) :: float()
def dot_product(emb1, emb2) do
Similarity.dot_product(emb1, emb2)
end
@doc """
Finds the top-k most similar embeddings to a query.
## Options
* `:top_k` - Number of results to return (default: `10`)
* `:metric` - Similarity metric (`:cosine`, `:euclidean`, `:dot_product`)
(default: `:cosine`)
* `:threshold` - Minimum similarity threshold (optional)
Returns `{:ok, results}` where results is a list of `{score, index}` tuples
sorted by similarity.
## Examples
{:ok, results} = EmbedEx.find_similar(
query_embedding,
corpus_embeddings,
top_k: 5,
metric: :cosine
)
# => {:ok, [{0.95, 0}, {0.87, 2}, {0.82, 5}, {0.79, 1}, {0.75, 8}]}
# With threshold
{:ok, results} = EmbedEx.find_similar(
query_embedding,
corpus_embeddings,
top_k: 10,
threshold: 0.8
)
# Only returns results with similarity >= 0.8
"""
@spec find_similar(
Embedding.t() | list(float()),
[Embedding.t()] | [list(float())],
keyword()
) :: {:ok, [{float(), non_neg_integer()}]} | {:error, term()}
def find_similar(query, corpus, opts \\ []) do
Similarity.find_similar(query, corpus, opts)
end
@doc """
Computes a pairwise similarity matrix for a list of embeddings.
Returns an Nx tensor of shape {n, n}.
## Options
* `:metric` - Similarity metric (`:cosine`, `:euclidean`, `:dot_product`)
(default: `:cosine`)
## Examples
matrix = EmbedEx.pairwise_similarity([emb1, emb2, emb3])
# => #Nx.Tensor<...>
"""
@spec pairwise_similarity([Embedding.t()] | [list(float())], keyword()) :: Nx.Tensor.t()
def pairwise_similarity(embeddings, opts \\ []) do
Similarity.pairwise_similarity(embeddings, opts)
end
@doc """
Clears the embedding cache.
Returns `{:ok, count}` where count is the number of items cleared.
"""
@spec clear_cache() :: {:ok, non_neg_integer()}
def clear_cache do
Cache.clear()
end
@doc """
Returns cache statistics.
## Examples
{:ok, stats} = EmbedEx.cache_stats()
# => {:ok, %{hits: 150, misses: 50, ...}}
"""
@spec cache_stats() :: {:ok, map()} | {:error, term()}
def cache_stats do
Cache.stats()
end
@doc """
Returns information about available providers.
## Examples
EmbedEx.providers()
# => [
# %{
# name: :openai,
# module: EmbedEx.Providers.OpenAI,
# models: ["text-embedding-3-small", ...],
# max_batch_size: 2048
# },
# ...
# ]
"""
def providers do
[
%{
name: :openai,
module: Providers.OpenAI,
models: Providers.OpenAI.available_models(),
max_batch_size: Providers.OpenAI.max_batch_size()
},
%{
name: :cohere,
module: Providers.Cohere,
models: Providers.Cohere.available_models(),
max_batch_size: Providers.Cohere.max_batch_size()
},
%{
name: :voyage,
module: Providers.Voyage,
models: Providers.Voyage.available_models(),
max_batch_size: Providers.Voyage.max_batch_size()
}
]
end
# Private functions
defp get_provider(opts) do
case Keyword.get(opts, :provider) do
nil -> get_default_provider()
:openai -> Providers.OpenAI
:cohere -> Providers.Cohere
:voyage -> Providers.Voyage
module when is_atom(module) -> module
end
end
defp get_provider_name(opts) do
case Keyword.get(opts, :provider) do
nil -> get_default_provider_name()
name -> name
end
end
defp get_default_provider do
case Application.get_env(:embed_ex, :default_provider, :openai) do
:openai -> Providers.OpenAI
module when is_atom(module) -> module
end
end
defp get_default_provider_name do
Application.get_env(:embed_ex, :default_provider, :openai)
end
end