barrel_embed

v2.3.0

README

# barrel_embed

**Lightweight embedding generation for Erlang with 14 provider backends**

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![Version](https://img.shields.io/badge/Version-2.3.0-green.svg)]()

[Documentation](https://barrel-db.eu/docs/lib/embed/) | [Examples](./examples) | [barrel-db.eu](https://barrel-db.eu)

A standalone library for generating text and image embeddings with multiple provider backends and automatic fallback support.

Features

Multiple embedding providers with automatic fallback

Provider chain configuration for high availability

Batch embedding with configurable chunk size

Python integration via async port server (no NIF dependency)

Sparse, multi-vector, and cross-modal embeddings

Providers

Provider | Type | Requirements | Description
local | Dense | Python + sentence-transformers | Local CPU inference
ollama | Dense | Ollama server | Local Ollama API
fastembed | Dense | Python + fastembed | ONNX-based, lighter than sentence-transformers
openai | Dense | API key | OpenAI Embeddings API
cohere | Dense | API key | Cohere Embed API with input type optimization
voyage | Dense | API key | Voyage AI for RAG and domain-specific embeddings
jina | Dense | API key | Jina AI, 8K context, free tier
mistral | Dense | API key | Mistral AI, EU data residency
azure | Dense | Azure subscription | Azure OpenAI for enterprise compliance
bedrock | Dense | AWS credentials | AWS Bedrock (Titan, Cohere models)
vertex | Dense | GCP project | Google Vertex AI
splade | Sparse | Python + transformers + torch | Neural sparse embeddings for hybrid search
colbert | Multi-vector | Python + transformers + torch | Token-level embeddings for fine-grained matching
clip | Cross-modal | Python + transformers + torch + pillow | Image/text embeddings in same space

Installation

Add to your rebar.config:

{deps, [
    {barrel_embed, "~> 2.3"}
]}.

Note: Local Python providers require Python 3.9+ installed on your system.

Quick Start

%% Initialize with a single provider
{ok, State} = barrel_embed:init(#{
    embedder => {ollama, #{
        url => <<"http://localhost:11434">>,
        model => <<"nomic-embed-text">>
    }}
}).

%% Generate embedding
{ok, Vector} = barrel_embed:embed(<<"Hello world">>, State).

%% Batch embedding
{ok, Vectors} = barrel_embed:embed_batch([<<"text1">>, <<" text2">>], State).

Configuration

Single Provider

%% Ollama (recommended for local deployment)
#{embedder => {ollama, #{
    url => <<"http://localhost:11434">>,
    model => <<"nomic-embed-text">>
}}}

%% Local Python (sentence-transformers)
#{embedder => {local, #{
    python => "python3",
    model => "BAAI/bge-base-en-v1.5"
}}}

%% OpenAI
#{embedder => {openai, #{
    api_key => <<"sk-...">>,  %% or set OPENAI_API_KEY env var
    model => <<"text-embedding-3-small">>
}}}

%% FastEmbed (ONNX, lighter)
#{embedder => {fastembed, #{
    model => "BAAI/bge-small-en-v1.5"
}}}

Provider Chain (Fallback)

#{embedder => [
    {ollama, #{url => <<"http://localhost:11434">>}},
    {openai, #{api_key => <<"sk-...">>}},
    {local, #{}}  %% fallback to CPU
]}

Custom Dimensions and Batch Size

#{
    embedder => {local, #{}},
    dimensions => 768,
    batch_size => 64
}

Ollama Example

First, install Ollama and pull an embedding model:

# Install Ollama (macOS)
brew install ollama

# Start Ollama server
ollama serve

# Pull embedding model
ollama pull nomic-embed-text

Then use in Erlang:

%% Start the application
application:ensure_all_started(barrel_embed).

%% Initialize with Ollama
{ok, State} = barrel_embed:init(#{
    embedder => {ollama, #{
        url => <<"http://localhost:11434">>,
        model => <<"nomic-embed-text">>
    }},
    dimensions => 768
}).

%% Generate embeddings
{ok, Vec1} = barrel_embed:embed(<<"The quick brown fox">>, State).
{ok, Vec2} = barrel_embed:embed(<<"A fast auburn canine">>, State).

%% Calculate cosine similarity
Dot = lists:sum(lists:zipwith(fun(A, B) -> A * B end, Vec1, Vec2)).
Norm1 = math:sqrt(lists:sum([X * X || X <- Vec1])).
Norm2 = math:sqrt(lists:sum([X * X || X <- Vec2])).
Similarity = Dot / (Norm1 * Norm2).
%% => ~0.85 (semantically similar)

Specialized APIs

SPLADE Sparse Embeddings

{ok, State} = barrel_embed:init(#{embedder => {splade, #{}}}).

%% Get sparse vector (indices + values)
{ok, #{indices := Indices, values := Values}} =
    barrel_embed_splade:embed_sparse(<<"query text">>, Config).

ColBERT Multi-Vector Embeddings

{ok, State} = barrel_embed:init(#{embedder => {colbert, #{}}}).

%% Get token-level vectors
{ok, TokenVectors} = barrel_embed_colbert:embed_multi(<<"document">>, Config).

%% Calculate MaxSim score
Score = barrel_embed_colbert:maxsim_score(QueryVecs, DocVecs).

CLIP Image Embeddings

{ok, State} = barrel_embed:init(#{embedder => {clip, #{}}}).

%% Embed image (base64-encoded)
{ok, ImageVec} = barrel_embed_clip:embed_image(ImageBase64, Config).

%% Embed text (same vector space as images)
{ok, TextVec} = barrel_embed_clip:embed(<<"a photo of a cat">>, Config).

%% Now ImageVec and TextVec can be compared with cosine similarity

API Reference

barrel_embed

Function | Description
init(Config) | Initialize embedding state
embed(Text, State) | Generate embedding for single text
embed_batch(Texts, State) | Generate embeddings for multiple texts
embed_batch(Texts, Opts, State) | Batch embed with options
dimension(State) | Get embedding dimension
info(State) | Get provider information

Application Configuration

barrel_embed creates and manages its own Python virtualenv automatically on startup (default location: priv/barrel_embed/.venv). Configure a custom location in sys.config:

%% sys.config
[
    {barrel_embed, [
        {venv_dir, "/path/to/.venv"}  %% Optional: custom venv location
    ]}
].

Python Setup

Managed Virtualenv (Default)

No manual setup is required. barrel_embed creates the venv on application start and installs each provider's dependencies the first time it is used:

%% Venv created and configured automatically
{ok, State} = barrel_embed:init(#{
    embedder => {local, #{
        model => "BAAI/bge-base-en-v1.5"
    }}
}).

See venv-setup for the full venv management API.

Manual Installation

Install based on providers used:

# For local provider
pip install sentence-transformers

# For fastembed provider
pip install fastembed

# For splade/colbert providers
pip install transformers torch

# For clip provider
pip install transformers torch pillow

See venv-setup for detailed virtualenv instructions.

Support

Channel | For
GitHub Issues | Bug reports, feature requests
Email | Commercial inquiries

License

Apache-2.0. See LICENSE for details.

Built by Enki Multimedia | barrel-db.eu

Getting Started

Installation

Add barrel_embed to your rebar.config:

{deps, [
    {barrel_embed, "~> 2.3"}
]}.

Then fetch dependencies:

rebar3 get-deps

Choose Your Provider

Option 1: Ollama (Recommended for Local)

Ollama is the easiest way to run embeddings locally without Python dependencies.

# Install Ollama
brew install ollama  # macOS
# or see https://ollama.ai for other platforms

# Start server and pull model
ollama serve &
ollama pull nomic-embed-text

{ok, State} = barrel_embed:init(#{
    embedder => {ollama, #{
        url => <<"http://localhost:11434">>,
        model => <<"nomic-embed-text">>
    }}
}).

Option 2: OpenAI (Production)

Best quality embeddings with minimal setup.

{ok, State} = barrel_embed:init(#{
    embedder => {openai, #{
        api_key => <<"sk-...">>,  % or set OPENAI_API_KEY env var
        model => <<"text-embedding-3-small">>
    }}
}).

Option 3: FastEmbed (Lightweight Local)

ONNX-based embeddings without PyTorch (~100MB vs ~2GB).

%% Venv created automatically, deps installed on first use
{ok, State} = barrel_embed:init(#{
    embedder => {fastembed, #{
        model => "BAAI/bge-small-en-v1.5"
    }}
}).

Option 4: Local Python (sentence-transformers)

Full control with sentence-transformers (requires ~2GB for PyTorch).

%% Venv created automatically, deps installed on first use
{ok, State} = barrel_embed:init(#{
    embedder => {local, #{
        model => "BAAI/bge-base-en-v1.5"
    }}
}).

Basic Usage

Single Text

{ok, Vector} = barrel_embed:embed(<<"The quick brown fox">>, State).
%% Vector is a list of floats, e.g., [0.123, -0.456, ...]

Batch Embedding

Texts = [<<"Document 1">>, <<"Document 2">>, <<"Document 3">>],
{ok, Vectors} = barrel_embed:embed_batch(Texts, State).
%% Vectors is a list of embedding vectors

Check Configuration

%% Get embedding dimension
Dim = barrel_embed:dimension(State).
%% => 768

%% Get provider info
Info = barrel_embed:info(State).
%% => #{configured => true, providers => [...], dimension => 768}

Provider Chain (Fallback)

Configure multiple providers for high availability:

{ok, State} = barrel_embed:init(#{
    embedder => [
        {ollama, #{url => <<"http://localhost:11434">>}},
        {openai, #{api_key => <<"sk-...">>}},
        {local, #{}}  % fallback to CPU
    ]
}).

If the first provider fails, barrel_embed automatically tries the next one.

Cosine Similarity

Calculate similarity between embeddings:

cosine_similarity(V1, V2) ->
    Dot = lists:sum(lists:zipwith(fun(A, B) -> A * B end, V1, V2)),
    Norm1 = math:sqrt(lists:sum([X * X || X <- V1])),
    Norm2 = math:sqrt(lists:sum([X * X || X <- V2])),
    Dot / (Norm1 * Norm2).

Next Steps

Explore provider-specific features

See the complete API reference

Choosing a Provider

This guide helps you select the right embedding provider for your use case.

Quick Decision Tree

Need image embeddings?
  └─ Yes → CLIP
  └─ No ↓

Need sparse vectors for hybrid search?
  └─ Yes → SPLADE
  └─ No ↓

Need token-level matching?
  └─ Yes → ColBERT
  └─ No ↓

Can use cloud API?
  └─ No → Local (Ollama, Local, or FastEmbed)
  └─ Yes ↓

Need EU data residency?
  └─ Yes → Mistral or Azure (EU region)
  └─ No ↓

Need domain-specific models?
  └─ Yes → Voyage (code, law, finance) or Bedrock (Cohere)
  └─ No ↓

Need enterprise compliance?
  └─ Yes → Azure, Bedrock, or Vertex
  └─ No ↓

Need best retrieval quality?
  └─ Yes → Voyage or Cohere
  └─ No → OpenAI (general purpose)

Local only?
  └─ Have Ollama → Ollama (recommended)
  └─ Need lightweight → FastEmbed (~100MB)
  └─ Need any HF model → Local (~2GB)

Provider Comparison

Cloud Providers

Provider | Quality | Best For | Dimensions | EU Residency
OpenAI | ★★★★★ | General purpose | 256-3072 | No
Cohere | ★★★★★ | Input type optimization | 384-1024 | No
Voyage | ★★★★★ | Retrieval, domain-specific | 512-1536 | No
Jina | ★★★★☆ | Long context (8K), multilingual | 768-1024 | No
Mistral | ★★★★☆ | EU data residency | 1024 | Yes
Azure | ★★★★★ | Enterprise compliance | 1536-3072 | Yes (regional)
Bedrock | ★★★★★ | AWS ecosystem | 1024-1536 | Yes (regional)
Vertex | ★★★★☆ | GCP ecosystem | 768 | Yes (regional)

Local Providers

Provider | Quality | Speed | Install Size | Dependencies | Offline
Ollama | ★★★★☆ | Fast | ~2GB (model) | Ollama server | Yes
Local | ★★★★☆ | Medium | ~2GB | Python, PyTorch | Yes
FastEmbed | ★★★★☆ | Fast | ~100MB | Python, ONNX | Yes

Specialized Providers

Provider | Type | Output | Best For
SPLADE | Sparse | {indices, values} | Hybrid search, keyword expansion
ColBERT | Multi-vector | [[float]] per token | Fine-grained semantic matching
CLIP | Cross-modal | [float] | Image-text search

When to Use Each Provider

OpenAI

Best for: Production systems requiring highest quality

{openai, #{model => <<"text-embedding-3-small">>}}

✅ Use when:

Quality is the top priority

You have budget for API costs

Low latency to OpenAI servers

Don't need offline capability

❌ Avoid when:

Data privacy is critical (data sent to API)

Need offline/air-gapped operation

High volume with tight budget

Cohere

Best for: Production with input type optimization

{cohere, #{
    model => <<"embed-english-v3.0">>,
    input_type => <<"search_document">>  % or search_query
}}

✅ Use when:

Need separate document vs query embeddings

Want classification/clustering optimization

Building production search systems

Need multilingual support

❌ Avoid when:

Don't need input type distinction

Tight budget (comparable to OpenAI pricing)

Voyage AI

Best for: Best-in-class retrieval, domain-specific models

{voyage, #{model => <<"voyage-3">>}}
% Or domain-specific:
{voyage, #{model => <<"voyage-code-3">>}}    % code search
{voyage, #{model => <<"voyage-law-2">>}}     % legal
{voyage, #{model => <<"voyage-finance-2">>}} % financial

✅ Use when:

Building RAG systems (top MTEB scores)

Need domain-specific models (code, law, finance)

Retrieval quality is critical

❌ Avoid when:

Budget constrained

Don't need specialized retrieval

Jina AI

Best for: Long context, multilingual

{jina, #{model => <<"jina-embeddings-v3">>}}

✅ Use when:

Processing long documents (8K context)

Need multilingual with free tier

Budget conscious (free 1M tokens/month)

❌ Avoid when:

Need highest retrieval quality

Processing only short texts

Mistral

Best for: EU data residency

{mistral, #{model => <<"mistral-embed">>}}

✅ Use when:

EU data residency required

GDPR compliance important

Already using Mistral for LLMs

❌ Avoid when:

Don't need EU residency

Need domain-specific models

Azure OpenAI

Best for: Enterprise with Azure ecosystem

{azure, #{
    endpoint => <<"https://your-resource.cognitiveservices.azure.com">>,
    deployment => <<"text-embedding-3-small">>
}}

✅ Use when:

Need enterprise compliance (SOC 2, HIPAA)

Already in Azure ecosystem

Need VNet integration

Regional data residency required

❌ Avoid when:

Don't need enterprise features

Simpler setup preferred

AWS Bedrock

Best for: AWS ecosystem integration

{bedrock, #{
    model => <<"amazon.titan-embed-text-v2:0">>,
    region => <<"us-east-1">>
}}

✅ Use when:

Already in AWS ecosystem

Need IAM/VPC integration

Want choice of models (Titan, Cohere)

Need enterprise compliance

❌ Avoid when:

Don't use AWS

Need batch API (not supported)

Google Vertex AI

Best for: GCP ecosystem integration

{vertex, #{
    project => <<"my-project">>,
    model => <<"text-embedding-004">>
}}

✅ Use when:

Already in GCP ecosystem

Need BigQuery integration

Need VPC-SC, CMEK

❌ Avoid when:

Don't use GCP

Access token refresh is problematic

Ollama

Best for: Local deployment without Python complexity

{ollama, #{url => <<"http://localhost:11434">>, model => <<"nomic-embed-text">>}}

✅ Use when:

Want local inference without Python

Already using Ollama for LLMs

Need good quality with simple setup

Want to avoid API costs

❌ Avoid when:

Can't install Ollama

Need embedded solution (no server)

Memory constrained (models loaded in RAM)

Local (sentence-transformers)

Best for: Full control, access to any HuggingFace model

{local, #{model => "BAAI/bge-base-en-v1.5"}}

✅ Use when:

Need specific HuggingFace models

Already have PyTorch environment

Want maximum model flexibility

Need fine-tuning capability

❌ Avoid when:

Disk space is limited (~2GB install)

PyTorch dependency is problematic

Need fastest possible inference

FastEmbed

Best for: Lightweight local inference

{fastembed, #{model => "BAAI/bge-small-en-v1.5"}}

✅ Use when:

Need local inference with small footprint

Don't want PyTorch dependency

Deploying to resource-constrained environments

Quality similar to sentence-transformers is acceptable

❌ Avoid when:

Need models not supported by FastEmbed

Need absolute maximum quality

SPLADE

Best for: Hybrid lexical-semantic search

{splade, #{model => "prithivida/Splade_PP_en_v1"}}

✅ Use when:

Building hybrid search (BM25 + semantic)

Need keyword expansion (synonyms, related terms)

Want efficient inverted index storage

Combining with dense embeddings

❌ Avoid when:

Only need dense vector search

Memory constrained (sparse→dense is expensive)

Don't have hybrid search infrastructure

Example use case: E-commerce search where users type product names (lexical) but also want semantic matches.

ColBERT

Best for: Fine-grained passage retrieval

{colbert, #{model => "colbert-ir/colbertv2.0"}}

✅ Use when:

Single-vector similarity isn't precise enough

Building QA or passage retrieval systems

Need token-level relevance signals

Documents have varying relevant sections

❌ Avoid when:

Storage is limited (multiple vectors per doc)

Simple semantic similarity is sufficient

Real-time latency is critical

Example use case: Legal document search where specific clauses matter more than overall document similarity.

CLIP

Best for: Image and cross-modal search

{clip, #{model => "openai/clip-vit-base-patch32"}}

✅ Use when:

Searching images with text queries

Finding visually similar images

Building multi-modal applications

Need zero-shot image classification

❌ Avoid when:

Only working with text

Don't need image capabilities

Example use case: Stock photo search, content moderation, visual product search.

Combining Providers

Provider Chain (Fallback)

Use multiple providers for high availability:

#{embedder => [
    {ollama, #{url => <<"http://localhost:11434">>}},
    {openai, #{}},
    {local, #{}}
]}

If Ollama fails → try OpenAI → fall back to local Python.

Hybrid Search (SPLADE + Dense)

Combine sparse and dense for best retrieval:

%% Sparse for lexical matching
{ok, SpladeState} = barrel_embed:init(#{embedder => {splade, #{}}}).

%% Dense for semantic matching
{ok, DenseState} = barrel_embed:init(#{embedder => {ollama, #{...}}}).

%% Query both and combine scores
SparseScore = search_sparse(Query, SpladeState),
DenseScore = search_dense(Query, DenseState),
FinalScore = 0.3 * SparseScore + 0.7 * DenseScore.

Performance Benchmarks

Approximate performance on typical hardware (results vary):

Provider | First Request | Subsequent | Batch (100 texts)
OpenAI | 200ms | 100ms | 500ms
Ollama | 2s (model load) | 50ms | 2s
Local | 5s (model load) | 100ms | 3s
FastEmbed | 3s (model load) | 50ms | 2s

Summary Table

Use Case | Recommended Provider
Production, general purpose | OpenAI
Best retrieval quality | Voyage AI
Domain-specific (code/law/finance) | Voyage AI
Long context (8K tokens) | Jina AI
EU data residency | Mistral
Enterprise + Azure | Azure OpenAI
Enterprise + AWS | AWS Bedrock
Enterprise + GCP | Google Vertex AI
Input type optimization | Cohere
Local, simple setup | Ollama
Local, any HF model | Local
Local, lightweight | FastEmbed
Hybrid search | SPLADE + Dense
Passage retrieval, QA | ColBERT
Image search | CLIP
High availability | Provider chain

Python Virtualenv Setup

barrel_embed automatically manages a Python virtualenv with required dependencies.

Automatic Setup

barrel_embed automatically:

Creates a venv at priv/.venv on application startup

Installs uvloop for async performance (required on Unix)

Installs provider-specific dependencies when providers are initialized

No manual setup required.

%% Just use it - venv is created and configured automatically
{ok, State} = barrel_embed:init(#{
    embedder => {fastembed, #{}}
}).

Venv Management API

%% Get venv path
Path = barrel_embed:venv_path().

%% Check if uvloop is installed
barrel_embed:has_uvloop().
%% => true

%% Manually install provider deps
barrel_embed:install_provider(fastembed).

%% Recreate venv from scratch
barrel_embed:refresh_venv().

Custom Python Executable

The python option is only used as a fallback when the managed venv could not be created. When the managed venv exists (the default), its own interpreter is always used:

{ok, State} = barrel_embed:init(#{
    embedder => {fastembed, #{
        python => "/usr/local/bin/python3.11"
    }}
}).

Custom Venv Location

Set a custom venv path via application config:

%% In sys.config
{barrel_embed, [
    {venv_dir, "/opt/barrel_embed/venv"}
]}.

Provider Dependencies

Provider | Packages | Size
fastembed | fastembed | ~100MB
local | sentence-transformers | ~2GB
splade | transformers, torch | ~2GB
colbert | transformers, torch | ~2GB
clip | transformers, torch, pillow | ~2GB

Dependencies are installed on-demand when a provider is first initialized.

uvloop

uvloop is required on Unix systems and installed automatically. It provides significant performance improvements for the async embedding server.

Check uvloop status:

barrel_embed:has_uvloop().
%% => true (Unix) or false (Windows)

Troubleshooting

Venv Creation Failed

Check:

Python 3 is installed: python3 --version

venv module available: python3 -m venv --help

Write permissions to priv directory

uvloop Installation Failed

uvloop requires a C compiler. On Debian/Ubuntu:

apt-get install build-essential python3-dev

On macOS:

xcode-select --install

Adding a Provider

This guide explains how to add a new embedding provider to barrel_embed.

Provider Types

Cloud providers - Use HTTP APIs (OpenAI, Cohere, etc.)

Local providers - Use Python subprocess (sentence-transformers, SPLADE, etc.)

Adding a Cloud Provider

Cloud providers are simpler - they make HTTP requests to external APIs.

Step 1: Create Erlang Module

Create src/barrel_embed_myprovider.erl:

-module(barrel_embed_myprovider).
-behaviour(barrel_embed_provider).

-export([embed/2, embed_batch/2, dimension/1, name/0, init/1, available/1]).

-define(DEFAULT_URL, <<"https://api.myprovider.com/v1">>).
-define(DEFAULT_MODEL, <<"embed-model">>).
-define(DEFAULT_TIMEOUT, 30000).
-define(DEFAULT_DIMENSION, 1024).

name() -> myprovider.

dimension(Config) ->
    maps:get(dimension, Config, ?DEFAULT_DIMENSION).

init(Config) ->
    case get_api_key(Config) of
        undefined ->
            {error, api_key_not_configured};
        ApiKey ->
            {ok, maps:merge(#{
                url => ?DEFAULT_URL,
                model => ?DEFAULT_MODEL,
                timeout => ?DEFAULT_TIMEOUT,
                dimension => ?DEFAULT_DIMENSION
            }, Config#{api_key => ApiKey})}
    end.

available(Config) ->
    maps:get(api_key, Config, undefined) =/= undefined.

embed(Text, Config) ->
    case embed_batch([Text], Config) of
        {ok, [Vector]} -> {ok, Vector};
        {error, _} = E -> E
    end.

embed_batch(Texts, Config) ->
    Url = maps:get(url, Config),
    ApiKey = maps:get(api_key, Config),
    Model = maps:get(model, Config),
    Timeout = maps:get(timeout, Config),

    ApiUrl = <<Url/binary, "/embeddings">>,
    Body = json:encode(#{
        <<"input">> => Texts,
        <<"model">> => Model
    }),
    Headers = [
        {<<"Authorization">>, <<"Bearer ", ApiKey/binary>>},
        {<<"Content-Type">>, <<"application/json">>}
    ],

    case hackney:request(post, ApiUrl, Headers, Body,
                         [{recv_timeout, Timeout}, {with_body, true}]) of
        {ok, 200, _RespHeaders, RespBody} ->
            parse_response(RespBody);
        {ok, StatusCode, _RespHeaders, RespBody} ->
            {error, {http_error, StatusCode, RespBody}};
        {error, Reason} ->
            {error, {request_failed, Reason}}
    end.

%% Internal functions
get_api_key(Config) ->
    case maps:get(api_key, Config, undefined) of
        undefined ->
            case os:getenv("MYPROVIDER_API_KEY") of
                false -> undefined;
                Key -> list_to_binary(Key)
            end;
        Key when is_binary(Key) -> Key;
        Key when is_list(Key) -> list_to_binary(Key)
    end.

parse_response(Body) ->
    Response = json:decode(Body),
    case maps:find(<<"data">>, Response) of
        {ok, Data} ->
            Sorted = lists:sort(
                fun(A, B) ->
                    maps:get(<<"index">>, A, 0) < maps:get(<<"index">>, B, 0)
                end, Data),
            {ok, [maps:get(<<"embedding">>, Item) || Item <- Sorted]};
        _ ->
            {error, {invalid_response, no_data_field}}
    end.

Step 2: Register Provider

In src/barrel_embed.erl, add to provider_module/1:

provider_module(myprovider) -> barrel_embed_myprovider;

Step 3: Test

rebar3 compile
rebar3 shell

{ok, S} = barrel_embed:init(#{embedder => {myprovider, #{}}}).
barrel_embed:embed(<<"test">>, S).

Authentication Patterns

Different APIs use different auth headers:

Style | Header | Example Providers
Bearer token | Authorization: Bearer <key> | OpenAI, Cohere, Voyage, Jina, Mistral
API key header | api-key: <key> | Azure OpenAI
Custom header | x-goog-api-key: <key> | Google Vertex AI

%% Bearer token (most common)
{<<"Authorization">>, <<"Bearer ", ApiKey/binary>>}

%% Azure style
{<<"api-key">>, ApiKey}

%% Google style
{<<"x-goog-api-key">>, ApiKey}

Response Formats

OpenAI-compatible (most providers)

{"data": [{"embedding": [...], "index": 0}, {"embedding": [...], "index": 1}]}

parse_response(Body) ->
    Response = json:decode(Body),
    case maps:find(<<"data">>, Response) of
        {ok, Data} ->
            Sorted = lists:sort(
                fun(A, B) ->
                    maps:get(<<"index">>, A, 0) < maps:get(<<"index">>, B, 0)
                end, Data),
            {ok, [maps:get(<<"embedding">>, Item) || Item <- Sorted]};
        _ ->
            {error, {invalid_response, no_data_field}}
    end.

Cohere-style

{"embeddings": [[...], [...]]}

parse_response(Body) ->
    Response = json:decode(Body),
    case maps:find(<<"embeddings">>, Response) of
        {ok, Embeddings} -> {ok, Embeddings};
        _ -> {error, {invalid_response, no_embeddings_field}}
    end.

Vertex AI-style

{"predictions": [{"embeddings": {"values": [...]}}]}

parse_response(Body) ->
    Response = json:decode(Body),
    case maps:find(<<"predictions">>, Response) of
        {ok, Predictions} ->
            Embeddings = lists:map(
                fun(Pred) ->
                    EmbedObj = maps:get(<<"embeddings">>, Pred),
                    maps:get(<<"values">>, EmbedObj)
                end, Predictions),
            {ok, Embeddings};
        _ ->
            {error, {invalid_response, no_predictions_field}}
    end.

AWS SigV4 Signing (Bedrock)

For AWS Bedrock, you need AWS Signature Version 4. See barrel_embed_bedrock.erl for a complete implementation including:

sign_request/11 - Create SigV4 signature

canonical_headers/1 - Format headers for signing

hmac_sha256/2 - HMAC-SHA256 helper

Adding a Python Provider

Overview

Adding a provider requires:

Python: Create provider class extending AsyncEmbedServer

Python: Register in __main__.py

Erlang: Create provider module implementing barrel_embed_provider

Step 1: Create Python Provider

Create priv/barrel_embed/providers/mymodel.py:

"""MyModel embedding provider."""

from ..server import AsyncEmbedServer, logger

DEFAULT_MODEL = "org/model-name"

class MyModelServer(AsyncEmbedServer):
    """Embedding server using MyModel."""

    def __init__(self, model_name: str = None, max_workers: int = 4):
        super().__init__(max_workers=max_workers)
        self.model_name = model_name or DEFAULT_MODEL
        self.model = None
        self.dimension = None

    def load_model(self) -> bool:
        """Load the embedding model. Return True on success."""
        try:
            # Import your model library here (lazy import)
            from mylib import MyModel

            logger.info(f"Loading model: {self.model_name}")
            self.model = MyModel(self.model_name)
            self.dimension = self.model.get_dimension()
            logger.info(f"Model loaded: {self.dimension} dimensions")
            return True
        except Exception as e:
            logger.error(f"Failed to load model: {e}")
            return False

    def handle_info(self) -> dict:
        """Return model metadata."""
        return {
            "ok": True,
            "dimensions": self.dimension,
            "model": self.model_name,
            "backend": "mymodel",
        }

    def embed_sync(self, texts: list) -> dict:
        """Generate embeddings. Called in thread pool."""
        try:
            embeddings = self.model.encode(texts)
            return {"ok": True, "embeddings": embeddings.tolist()}
        except Exception as e:
            logger.error(f"Embedding failed: {e}")
            return {"ok": False, "error": str(e)}

Key Points

Lazy imports: Import heavy libraries in load_model(), not at module level

Logging: Use the provided logger for debugging

Error handling: Always catch exceptions and return {"ok": False, "error": ...}

Thread safety: embed_sync runs in a thread pool, ensure your model is thread-safe

Step 2: Register Provider

Update priv/barrel_embed/__main__.py

Add your provider to the argument choices:

parser.add_argument(
    "--provider",
    choices=["sentence_transformers", "fastembed", "splade", "colbert", "clip", "mymodel"],
    default="sentence_transformers",
    help="Embedding provider to use"
)

Add the import and instantiation:

elif args.provider == "mymodel":
    from .providers.mymodel import MyModelServer
    server = MyModelServer(
        model_name=args.model,
        max_workers=args.max_workers
    )

Update priv/barrel_embed/providers/__init__.py

from .mymodel import MyModelServer

__all__ = [
    "SentenceTransformerServer",
    "FastEmbedServer",
    "SpladeServer",
    "ColBERTServer",
    "CLIPServer",
    "MyModelServer",
]

Step 3: Create Erlang Provider

Create src/barrel_embed_mymodel.erl:

%%%-------------------------------------------------------------------
%%% @doc MyModel embedding provider
%%%
%%% Uses MyModel for embeddings.
%%%
%%% == Requirements ==
%%% ```
%%% pip install mylib
%%% '''
%%%
%%% == Configuration ==
%%% ```
%%% Config = #{
%%%     python => "python3",           %% Python executable
%%%     model => "org/model-name",     %% Model name
%%%     timeout => 120000              %% Timeout in ms
%%% }.
%%% '''
%%% @end
%%%-------------------------------------------------------------------
-module(barrel_embed_mymodel).
-behaviour(barrel_embed_provider).

-export([embed/2, embed_batch/2, dimension/1, name/0, init/1, available/1]).

-define(DEFAULT_PYTHON, "python3").
-define(DEFAULT_MODEL, "org/model-name").
-define(DEFAULT_TIMEOUT, 120000).
-define(DEFAULT_DIMENSION, 768).

%%====================================================================
%% Behaviour Callbacks
%%====================================================================

%% @doc Provider name.
name() -> mymodel.

%% @doc Get dimension for this provider.
dimension(Config) ->
    maps:get(dimension, Config, ?DEFAULT_DIMENSION).

%% @doc Initialize the provider.
init(Config) ->
    Python = maps:get(python, Config, ?DEFAULT_PYTHON),
    Model = maps:get(model, Config, ?DEFAULT_MODEL),
    Timeout = maps:get(timeout, Config, ?DEFAULT_TIMEOUT),

    Args = ["-m", "barrel_embed",
            "--provider", "mymodel",
            "--model", Model],

    Opts = [{timeout, Timeout}, {priv_dir, get_priv_dir()}],

    case barrel_embed_port_server:start_link(Python, Args, Opts) of
        {ok, Server} ->
            case barrel_embed_port_server:info(Server, Timeout) of
                {ok, #{dimensions := Dims}} ->
                    {ok, Config#{
                        server => Server,
                        dimension => Dims,
                        timeout => Timeout
                    }};
                {error, Reason} ->
                    barrel_embed_port_server:stop(Server),
                    {error, Reason}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

%% @doc Check if provider is available.
available(#{server := Server}) ->
    is_process_alive(Server);
available(_) ->
    false.

%% @doc Generate embedding for a single text.
embed(Text, Config) ->
    case embed_batch([Text], Config) of
        {ok, [Vector]} -> {ok, Vector};
        {error, _} = E -> E
    end.

%% @doc Generate embeddings for multiple texts.
embed_batch(Texts, #{server := Server, timeout := Timeout}) ->
    barrel_embed_port_server:embed_batch(Server, Texts, Timeout);
embed_batch(_, _) ->
    {error, server_not_initialized}.

%%====================================================================
%% Internal Functions
%%====================================================================

get_priv_dir() ->
    case code:priv_dir(barrel_embed) of
        {error, bad_name} -> "priv";
        Dir -> Dir
    end.

Step 4: Add Dependencies

Update priv/pyproject.toml:

[project.optional-dependencies]
mymodel = ["mylib>=1.0.0"]
all = [
    # ... existing deps ...
    "mylib>=1.0.0",
]

Step 5: Test

Test Python Directly

cd priv
python -m barrel_embed --provider mymodel --help
python -m barrel_embed --provider mymodel --model org/model-name

Then send JSON to stdin:

{"id": 1, "action": "info"}
{"id": 2, "action": "embed", "texts": ["hello world"]}

Test in Erlang

rebar3 shell

%% Initialize
{ok, State} = barrel_embed:init(#{embedder => {mymodel, #{}}}).

%% Get info
barrel_embed:info(State).

%% Generate embedding
{ok, Vector} = barrel_embed:embed(<<"test">>, State).
length(Vector).  %% Should match dimensions

%% Batch embedding
{ok, Vectors} = barrel_embed:embed_batch([<<"hello">>, <<"world">>], State).

Specialized Providers

Sparse Embeddings (SPLADE-style)

For providers that return sparse vectors, override the dispatch method:

async def dispatch(self, request: dict) -> dict:
    action = request.get("action")

    if action == "info":
        return self.handle_info()
    elif action == "embed":
        texts = request.get("texts", [])
        return await self.handle_embed_sparse(texts)
    else:
        return {"ok": False, "error": f"Unknown action: {action}"}

async def handle_embed_sparse(self, texts: list) -> dict:
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(self.executor, self.embed_sparse_sync, texts)
    return result

def embed_sparse_sync(self, texts: list) -> dict:
    """Return sparse vectors with indices and values."""
    try:
        results = []
        for text in texts:
            sparse = self.model.encode_sparse(text)
            results.append({
                "indices": sparse.indices.tolist(),
                "values": sparse.values.tolist()
            })
        return {"ok": True, "embeddings": results}
    except Exception as e:
        return {"ok": False, "error": str(e)}

See priv/barrel_embed/providers/splade.py for a complete example.

Image Embeddings (CLIP-style)

For multi-modal providers, add image handling:

async def dispatch(self, request: dict) -> dict:
    action = request.get("action")

    if action == "info":
        return self.handle_info()
    elif action == "embed":
        texts = request.get("texts", [])
        return await self.handle_embed(texts)
    elif action == "embed_image":
        images = request.get("images", [])
        return await self.handle_embed_image(images)
    else:
        return {"ok": False, "error": f"Unknown action: {action}"}

async def handle_embed_image(self, images: list) -> dict:
    loop = asyncio.get_event_loop()
    result = await loop.run_in_executor(self.executor, self.embed_image_sync, images)
    return result

def embed_image_sync(self, images: list) -> dict:
    """Embed base64-encoded images."""
    import base64
    from PIL import Image
    import io

    try:
        pil_images = []
        for img_b64 in images:
            img_bytes = base64.b64decode(img_b64)
            pil_images.append(Image.open(io.BytesIO(img_bytes)))

        embeddings = self.model.encode_images(pil_images)
        return {"ok": True, "embeddings": embeddings.tolist()}
    except Exception as e:
        return {"ok": False, "error": str(e)}

On the Erlang side, add image embedding functions:

%% In your provider module
-export([embed_image/2, embed_image_batch/2]).

embed_image(ImageBase64, Config) ->
    case embed_image_batch([ImageBase64], Config) of
        {ok, [Vector]} -> {ok, Vector};
        {error, _} = E -> E
    end.

embed_image_batch(Images, #{server := Server, timeout := Timeout}) ->
    barrel_embed_port_server:embed_image_batch(Server, Images, Timeout);
embed_image_batch(_, _) ->
    {error, server_not_initialized}.

See priv/barrel_embed/providers/clip.py for a complete example.

Multi-vector Embeddings (ColBERT-style)

For token-level embeddings:

def embed_sync(self, texts: list) -> dict:
    """Return multiple vectors per text (one per token)."""
    try:
        results = []
        for text in texts:
            # Returns [num_tokens, dimension] matrix
            token_embeddings = self.model.encode_tokens(text)
            results.append(token_embeddings.tolist())
        return {"ok": True, "embeddings": results}
    except Exception as e:
        return {"ok": False, "error": str(e)}

See priv/barrel_embed/providers/colbert.py for a complete example.

Best Practices

Error messages: Include enough context to debug issues

Model validation: Warn for unknown models but don't fail

Timeout configuration: Allow users to adjust for slow networks/large models

Memory: Be aware of model memory requirements

Thread safety: Most embedding models are thread-safe for inference, but verify

Logging: Log model loading, but not every request (too verbose)

Providers Overview

barrel_embed supports multiple embedding providers, each with different characteristics and use cases.

Cloud Providers

Production-ready cloud embedding APIs with high availability and no infrastructure to manage.

Provider | Requirements | Dimensions | Best For
OpenAI | API key | 256-3072 | Production, general purpose
Cohere | API key | 384-1024 | Production, input type optimization
Voyage AI | API key | 512-1536 | RAG, retrieval, domain-specific
Jina AI | API key | 768-1024 | Long context (8K), multilingual
Mistral | API key | 1024 | EU data residency
Azure OpenAI | Azure subscription | 1536-3072 | Enterprise, compliance
AWS Bedrock | AWS credentials | 1024-1536 | AWS ecosystem, enterprise
Google Vertex AI | GCP project | 768 | GCP ecosystem, enterprise

Local Providers

Run embedding models locally without external API calls.

Provider | Requirements | Dimensions | Best For
Ollama | Ollama server | Model-dependent | Local development
Local | Python + sentence-transformers | Model-dependent | Offline use
FastEmbed | Python + fastembed | Model-dependent | Lightweight local

Specialized Providers

Advanced embedding types for specific use cases.

Provider | Type | Requirements | Best For
SPLADE | Sparse | Python + transformers + torch | Hybrid search
ColBERT | Multi-vector | Python + transformers + torch | Fine-grained matching
CLIP | Cross-modal | Python + transformers + torch + pillow | Image-text search

Choosing a Provider

For Production

Cloud providers are recommended:

OpenAI - Best general-purpose quality

Cohere - Input type optimization (search_query vs search_document)

Voyage AI - Top retrieval performance, domain-specific models

Jina AI - Longest context length (8K tokens)

For Enterprise

Hyperscaler providers with compliance features:

Azure OpenAI - SOC 2, HIPAA, regional deployment

AWS Bedrock - VPC integration, IAM, CloudWatch

Google Vertex AI - VPC-SC, CMEK, BigQuery integration

For EU Data Residency

Mistral - EU-based company

Azure OpenAI - Deploy in EU regions

Google Vertex AI - Deploy in EU regions

For Local Development

Ollama is recommended:

No Python dependencies

Easy model management

Good performance

For Offline/Air-gapped

Local or FastEmbed:

No external API calls

Full data privacy

FastEmbed is lighter (~100MB vs ~2GB)

For Specialized Use Cases

Hybrid search: Use SPLADE for sparse + dense combination

Passage retrieval: Use ColBERT for token-level matching

Image search: Use CLIP for cross-modal embeddings

Provider Chain

Configure fallback providers for high availability:

#{embedder => [
    {voyage, #{api_key => <<"pa-...">>}},      % Primary: best retrieval
    {openai, #{api_key => <<"sk-...">>}},      % Fallback: OpenAI
    {ollama, #{url => <<"http://localhost:11434">>}}  % Offline fallback
]}

Providers are tried in order. If one fails, the next is attempted automatically.

Quick Comparison

Feature | OpenAI | Cohere | Voyage | Jina | Mistral | Azure | Bedrock | Vertex
Batch support | Yes | Yes | Yes | Yes | Yes | Yes | No | Yes
Free tier | No | Limited | No | Yes | No | No | No | No
EU residency | No | No | No | No | Yes | Yes | Yes | Yes
Max context | 8K | 512 | 32K | 8K | 8K | 8K | 8K | 2K
Domain models | No | No | Yes | No | No | No | Yes | No

Azure OpenAI Provider

Embedding generation using Azure OpenAI's Embeddings API. Provides enterprise-grade embeddings with Azure security, compliance, and regional deployment.

Requirements

Azure subscription with OpenAI resource

Deployed embedding model in Azure OpenAI Studio

API key from Azure Portal

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {azure, #{
        api_key => <<"...">>,                              % or use env var
        endpoint => <<"https://your-resource.cognitiveservices.azure.com">>,
        deployment => <<"text-embedding-3-small">>         % your deployment name
    }}
}).

Endpoint Format

Your endpoint may use either *.openai.azure.com or *.cognitiveservices.azure.com depending on your Azure configuration. Check Azure Portal for the exact URL.

Options

Option | Type | Default | Description
api_key | binary | AZURE_OPENAI_API_KEY env var | API key
endpoint | binary | AZURE_OPENAI_ENDPOINT env var | Azure OpenAI endpoint
deployment | binary | required | Deployment name
api_version | binary | <<"2024-02-01">> | API version

Using Environment Variables

Set environment variables instead of passing in config:

export AZURE_OPENAI_API_KEY=your-api-key
export AZURE_OPENAI_ENDPOINT=https://your-resource.cognitiveservices.azure.com

{ok, State} = barrel_embed:init(#{
    embedder => {azure, #{
        deployment => <<"text-embedding-3-small">>
    }}
}).

Supported Models

Deploy these models in Azure OpenAI Studio:

Model | Dimensions | Notes
text-embedding-3-small | 1536 | Fast, cost-effective
text-embedding-3-large | 3072 | Highest quality
text-embedding-ada-002 | 1536 | Legacy

Setup Steps

Create Azure OpenAI Resource

Go to Azure Portal

Create an "Azure OpenAI" resource

Note the endpoint URL

Deploy a Model

Go to Azure OpenAI Studio

Click "Deployments" -> "Create"

Select an embedding model

Note the deployment name

Get API Key

Go to Azure Portal -> Your Resource -> Keys and Endpoint

Copy Key 1 or Key 2

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {azure, #{
        api_key => <<"your-api-key">>,
        endpoint => <<"https://your-resource.cognitiveservices.azure.com">>,
        deployment => <<"text-embedding-3-small">>,
        api_version => <<"2024-02-01">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch (more efficient for multiple texts)
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Why Azure OpenAI?

Enterprise security and compliance (SOC 2, HIPAA, etc.)

Regional deployment (data residency)

Azure Active Directory integration

Private endpoints (VNet integration)

Same models as OpenAI with enterprise features

Pricing

Azure OpenAI pricing is similar to OpenAI. See azure.microsoft.com/pricing/details/cognitive-services/openai-service/ for current rates.

AWS Bedrock Provider

Embedding generation using AWS Bedrock's embedding API. Provides access to Amazon Titan and Cohere embedding models through AWS infrastructure.

Requirements

AWS account with Bedrock access

Model access enabled in Bedrock console

Authentication via API key (ABSK) or IAM credentials

Configuration

Option 1: API Key (Recommended)

Uses AWS Bedrock API Key (ABSK format, available since July 2025):

{ok, State} = barrel_embed:init(#{
    embedder => {bedrock, #{
        api_key => <<"ABSK...">>,                % or use env var
        region => <<"us-east-1">>,
        model => <<"amazon.titan-embed-text-v2:0">>
    }}
}).

Option 2: IAM Credentials

Uses AWS access keys with SigV4 signing:

{ok, State} = barrel_embed:init(#{
    embedder => {bedrock, #{
        access_key_id => <<"AKIA...">>,
        secret_access_key => <<"...">>,
        session_token => <<"...">>,              % optional, for temporary credentials
        region => <<"us-east-1">>,
        model => <<"amazon.titan-embed-text-v2:0">>
    }}
}).

Options

Option | Type | Default | Description
api_key | binary | BEDROCK_API_KEY env var | API key (ABSK format)
access_key_id | binary | AWS_ACCESS_KEY_ID env var | IAM access key
secret_access_key | binary | AWS_SECRET_ACCESS_KEY env var | IAM secret key
session_token | binary | AWS_SESSION_TOKEN env var | Session token (optional)
region | binary | <<"us-east-1">> | AWS region
model | binary | <<"amazon.titan-embed-text-v2:0">> | Model ID

Using Environment Variables

# Option 1: API key
export BEDROCK_API_KEY=ABSK...

# Option 2: IAM credentials
export AWS_ACCESS_KEY_ID=AKIA...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-east-1

{ok, State} = barrel_embed:init(#{
    embedder => {bedrock, #{
        model => <<"amazon.titan-embed-text-v2:0">>
    }}
}).

Supported Models

Enable model access in AWS Bedrock console first:

Model | Dimensions | Notes
amazon.titan-embed-text-v2:0 | 1024 | Default, Amazon Titan
amazon.titan-embed-text-v1 | 1536 | Legacy Titan
cohere.embed-english-v3 | 1024 | Cohere English
cohere.embed-multilingual-v3 | 1024 | Cohere multilingual

Setup Steps

Enable Model Access

Go to AWS Console -> Bedrock

Click "Model access"

Request access for embedding models

Get Credentials

For API key:

Go to Bedrock -> API keys

Create a new API key (ABSK format)

For IAM:

Create IAM user with Bedrock permissions

Generate access keys

Example

%% Initialize with API key
{ok, State} = barrel_embed:init(#{
    embedder => {bedrock, #{
        api_key => <<"ABSK...">>,
        region => <<"us-east-1">>,
        model => <<"amazon.titan-embed-text-v2:0">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch (processed sequentially - Bedrock doesn't support native batch)
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Regions

Bedrock is available in these regions:

us-east-1 (N. Virginia) - Most models

us-west-2 (Oregon)

eu-central-1 (Frankfurt)

eu-west-1 (Ireland)

ap-northeast-1 (Tokyo)

Why AWS Bedrock?

AWS infrastructure (VPC, IAM, CloudWatch)

Multiple model providers (Titan, Cohere) through one API

Pay-per-use pricing

Enterprise compliance (SOC, HIPAA, FedRAMP)

Pricing

See aws.amazon.com/bedrock/pricing for current rates.

CLIP Provider

Cross-modal embedding generation using CLIP models.

What is CLIP?

CLIP (Contrastive Language-Image Pre-training) encodes both images and text into the same vector space. This enables:

Searching images with text queries

Searching text with image queries

Finding similar images

Zero-shot image classification

Requirements

# Using virtualenv with uv (recommended)
./scripts/setup_venv.sh
uv pip install transformers torch pillow --python .venv/bin/python

# Or install manually
pip install transformers torch pillow

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {clip, #{
        python => "python3",                        % default, fallback only
        model => "openai/clip-vit-base-patch32",   % default
        timeout => 120000                           % default, ms
    }}
}).

Options

Option | Type | Default | Description
python | string | "python3" | Python executable, used only if the managed venv could not be created
model | string | "openai/clip-vit-base-patch32" | Model name
timeout | integer | 120000 | Timeout in milliseconds

barrel_embed manages its own Python virtualenv automatically and installs transformers, torch, and pillow into it on first use. See Python Virtualenv Setup for the managed venv API.

Supported Models

Model | Dimensions | Notes
openai/clip-vit-base-patch32 | 512 | Default, fast
openai/clip-vit-base-patch16 | 512 | Higher quality
openai/clip-vit-large-patch14 | 768 | Best quality
laion/CLIP-ViT-B-32-laion2B-s34B-b79K | 512 | LAION trained

API

Text Embedding

%% Text embeddings (same space as images)
{ok, TextVec} = barrel_embed:embed(<<"a photo of a cat">>, State).
{ok, TextVecs} = barrel_embed:embed_batch([<<"cat">>, <<"dog">>], State).

Image Embedding

Images must be base64-encoded:

%% Read and encode image
{ok, ImageData} = file:read_file("photo.jpg").
ImageBase64 = base64:encode(ImageData).

%% Get embedding
{ok, ImageVec} = barrel_embed_clip:embed_image(ImageBase64, Config).

%% Batch
{ok, ImageVecs} = barrel_embed_clip:embed_image_batch([Img1, Img2], Config).

Example: Image Search with Text

%% Initialize
{ok, State} = barrel_embed:init(#{embedder => {clip, #{}}}).
{_, Config} = hd(maps:get(providers, State)).

%% Index images (do once)
Images = [<<"img1.jpg">>, <<"img2.jpg">>, <<"img3.jpg">>],
ImageVecs = lists:map(fun(Path) ->
    {ok, Data} = file:read_file(Path),
    {ok, Vec} = barrel_embed_clip:embed_image(base64:encode(Data), Config),
    {Path, Vec}
end, Images).

%% Search with text query
{ok, QueryVec} = barrel_embed:embed(<<"a sunset over the ocean">>, State).

%% Find most similar images
Scores = [{Path, cosine_similarity(QueryVec, ImgVec)}
          || {Path, ImgVec} <- ImageVecs],
Ranked = lists:reverse(lists:keysort(2, Scores)).

Example: Similar Image Search

%% Find images similar to a reference image
{ok, RefData} = file:read_file("reference.jpg").
{ok, RefVec} = barrel_embed_clip:embed_image(base64:encode(RefData), Config).

%% Compare with other images
Scores = [{Path, cosine_similarity(RefVec, ImgVec)}
          || {Path, ImgVec} <- ImageVecs],
Similar = lists:reverse(lists:keysort(2, Scores)).

Example: Zero-Shot Classification

%% Classify an image into categories
Categories = [<<"a photo of a cat">>,
              <<"a photo of a dog">>,
              <<"a photo of a bird">>],
{ok, CategoryVecs} = barrel_embed:embed_batch(Categories, State).

{ok, ImageData} = file:read_file("mystery_animal.jpg"),
{ok, ImageVec} = barrel_embed_clip:embed_image(base64:encode(ImageData), Config).

%% Find best matching category
Scores = lists:zipwith(fun(Cat, CatVec) ->
    {Cat, cosine_similarity(ImageVec, CatVec)}
end, Categories, CategoryVecs),
[{BestCategory, _} | _] = lists:reverse(lists:keysort(2, Scores)).

Use Cases

Image search: Find images by description

Reverse image search: Find similar images

Content moderation: Classify images automatically

Multi-modal retrieval: Combined text and image search

Cohere Provider

Embedding generation using Cohere's Embed API.

Requirements

Cohere API key from dashboard.cohere.com/api-keys

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {cohere, #{
        api_key => <<"...">>,                    % or use env var
        model => <<"embed-english-v3.0">>        % default
    }}
}).

Options

Option | Type | Default | Description
api_key | binary | COHERE_API_KEY env var | API key
model | binary | <<"embed-english-v3.0">> | Model name
input_type | binary | <<"search_document">> | Input type for optimization
url | binary | <<"https://api.cohere.com/v1">> | API endpoint

Using Environment Variable

Set COHERE_API_KEY instead of passing in config:

export COHERE_API_KEY=your-api-key

{ok, State} = barrel_embed:init(#{
    embedder => {cohere, #{}}  % uses env var
}).

Supported Models

Model | Dimensions | Notes
embed-english-v3.0 | 1024 | Default, English
embed-multilingual-v3.0 | 1024 | 100+ languages
embed-english-light-v3.0 | 384 | Faster, cheaper
embed-multilingual-light-v3.0 | 384 | Multilingual light

Input Types

Cohere supports different input types for optimization:

Input Type | Use Case
search_document | Documents to be searched (default)
search_query | Search queries
classification | Classification tasks
clustering | Clustering tasks

{ok, State} = barrel_embed:init(#{
    embedder => {cohere, #{
        input_type => <<"search_query">>  % for queries
    }}
}).

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {cohere, #{
        api_key => <<"your-api-key">>,
        model => <<"embed-multilingual-v3.0">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch (more efficient for multiple texts)
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Pricing

Cohere charges per token. See cohere.com/pricing for current rates.

For high-volume usage:

Use batch embedding instead of single calls

Consider embed-english-light-v3.0 for cost savings

Cache embeddings when possible

ColBERT Provider

Multi-vector embedding generation using ColBERT models.

What is ColBERT?

ColBERT (Contextualized Late Interaction over BERT) produces multiple vectors per text - one for each token. This enables fine-grained, token-level matching.

Late Interaction: Query and document are encoded independently, then compared token-by-token using MaxSim scoring.

Requirements

# Using virtualenv with uv (recommended)
./scripts/setup_venv.sh
uv pip install transformers torch --python .venv/bin/python

# Or install manually
pip install transformers torch

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {colbert, #{
        python => "python3",                    % default, fallback only
        model => "colbert-ir/colbertv2.0",     % default
        timeout => 120000                       % default, ms
    }}
}).

Options

Option | Type | Default | Description
python | string | "python3" | Python executable, used only if the managed venv could not be created
model | string | "colbert-ir/colbertv2.0" | Model name
timeout | integer | 120000 | Timeout in milliseconds

barrel_embed manages its own Python virtualenv automatically and installs transformers and torch into it on first use. See Python Virtualenv Setup for the managed venv API.

Supported Models

Model | Dimensions | Notes
colbert-ir/colbertv2.0 | 128 | Default, standard ColBERT
answerdotai/answerai-colbert-small-v1 | 96 | Smaller, faster
jinaai/jina-colbert-v2 | 128 | Long context (8192 tokens)

Multi-Vector Format

ColBERT returns a list of token vectors:

[
    [0.1, 0.2, ...],  % vector for token 1
    [0.3, 0.4, ...],  % vector for token 2
    [0.5, 0.6, ...],  % vector for token 3
    ...
]

API

Native Multi-Vector API

%% Single text - returns list of token vectors
{ok, TokenVecs} = barrel_embed_colbert:embed_multi(<<"document text">>, Config).

%% Batch
{ok, MultiVecs} = barrel_embed_colbert:embed_batch_multi(Texts, Config).

MaxSim Scoring

ColBERT uses MaxSim for scoring:

Score(Q, D) = Σ max(qi · dj) for all qi in Q, dj in D

%% Calculate MaxSim score between query and document
QueryVecs = ...,  % from embed_multi
DocVecs = ...,    % from embed_multi
Score = barrel_embed_colbert:maxsim_score(QueryVecs, DocVecs).

Dense API (Mean Pooling)

For compatibility, ColBERT can return a single vector via mean pooling:

%% Returns mean-pooled single vector
{ok, Vec} = barrel_embed:embed(<<"text">>, State).

Note

Mean pooling loses the fine-grained matching capability. Use embed_multi/2 for best results.

Example: Passage Retrieval

%% Initialize
{ok, State} = barrel_embed:init(#{embedder => {colbert, #{}}}).
{_, Config} = hd(maps:get(providers, State)).

%% Embed query and documents
{ok, QueryVecs} = barrel_embed_colbert:embed_multi(<<"What is machine learning?">>, Config).
{ok, Doc1Vecs} = barrel_embed_colbert:embed_multi(Doc1, Config).
{ok, Doc2Vecs} = barrel_embed_colbert:embed_multi(Doc2, Config).

%% Score and rank
Score1 = barrel_embed_colbert:maxsim_score(QueryVecs, Doc1Vecs).
Score2 = barrel_embed_colbert:maxsim_score(QueryVecs, Doc2Vecs).

Use Cases

Passage retrieval: Fine-grained matching for QA systems

Semantic search: When single-vector similarity isn't precise enough

Long documents: Token-level matching captures local relevance

FastEmbed Provider

Lightweight local embedding using FastEmbed (ONNX-based).

Requirements

# Using virtualenv with uv (recommended)
./scripts/setup_venv.sh
uv pip install fastembed --python .venv/bin/python

# Or install manually
pip install fastembed

Why FastEmbed?

FastEmbed is a lighter alternative to sentence-transformers:

| FastEmbed | sentence-transformers
Install size | ~100MB | ~2GB+
Dependencies | ONNX Runtime | PyTorch
Performance | Optimized inference | Standard
Quality | Similar | Similar

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {fastembed, #{
        python => "python3",                    % default, fallback only
        model => "BAAI/bge-small-en-v1.5",     % default
        timeout => 120000                       % default, ms
    }}
}).

Options

Option | Type | Default | Description
python | string | "python3" | Python executable, used only if the managed venv could not be created
model | string | "BAAI/bge-small-en-v1.5" | Model name
timeout | integer | 120000 | Timeout in milliseconds

barrel_embed manages its own Python virtualenv automatically and installs fastembed into it on first use. See Python Virtualenv Setup for the managed venv API.

Supported Models

Model | Dimensions | Notes
BAAI/bge-small-en-v1.5 | 384 | Default, fast
BAAI/bge-base-en-v1.5 | 768 | Good balance
BAAI/bge-large-en-v1.5 | 1024 | Highest quality
sentence-transformers/all-MiniLM-L6-v2 | 384 | Popular lightweight
nomic-ai/nomic-embed-text-v1.5 | 768 | Good general-purpose

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {fastembed, #{
        model => "BAAI/bge-small-en-v1.5"
    }}
}).

%% Generate embedding
{ok, Vec} = barrel_embed:embed(<<"Fast and lightweight">>, State).
384 = length(Vec).

%% Batch embedding
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document 1">>,
    <<"Document 2">>
], State).

When to Use FastEmbed

Choose FastEmbed when:

Disk space is limited

PyTorch is not desired

You need lightweight local inference

Quality requirements are standard (not maximum)

Choose sentence-transformers (local provider) when:

You need access to all HuggingFace models

You're already using PyTorch

You need maximum compatibility

Jina AI Provider

Embedding generation using Jina AI's Embeddings API. Jina provides multilingual embeddings with 8K context length.

Requirements

Jina AI API key from jina.ai/api (free tier available)

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {jina, #{
        api_key => <<"...">>,                    % or use env var
        model => <<"jina-embeddings-v3">>        % default
    }}
}).

Options

Option | Type | Default | Description
api_key | binary | JINA_API_KEY env var | API key
model | binary | <<"jina-embeddings-v3">> | Model name
url | binary | <<"https://api.jina.ai/v1">> | API endpoint

Using Environment Variable

Set JINA_API_KEY instead of passing in config:

export JINA_API_KEY=jina_...

{ok, State} = barrel_embed:init(#{
    embedder => {jina, #{}}  % uses env var
}).

Supported Models

Model | Dimensions | Notes
jina-embeddings-v3 | 1024 | Default, 8K context, multilingual
jina-embeddings-v2-base-en | 768 | English
jina-embeddings-v2-base-de | 768 | German
jina-embeddings-v2-base-es | 768 | Spanish
jina-embeddings-v2-base-zh | 768 | Chinese
jina-colbert-v2 | 128 | Late interaction (per token)
jina-clip-v1 | 768 | Multimodal (text + images)

Language-Specific Models

For single-language use cases, language-specific models may perform better:

%% German documents
{ok, State} = barrel_embed:init(#{
    embedder => {jina, #{model => <<"jina-embeddings-v2-base-de">>}}
}).

%% Chinese documents
{ok, State} = barrel_embed:init(#{
    embedder => {jina, #{model => <<"jina-embeddings-v2-base-zh">>}}
}).

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {jina, #{
        api_key => <<"jina_...">>,
        model => <<"jina-embeddings-v3">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch (more efficient for multiple texts)
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Why Jina AI?

8K context length (longest among embedding providers)

Free tier available (1M tokens/month)

Multilingual support (100+ languages)

Language-specific models available

OpenAI-compatible API format

Pricing

Jina offers a generous free tier. See jina.ai/api for pricing details.

Local Provider

Local embedding generation using Python and sentence-transformers.

Requirements

# Using virtualenv with uv (recommended)
./scripts/setup_venv.sh

# Or install manually
pip install sentence-transformers

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {local, #{
        python => "python3",                    % default, fallback only
        model => "BAAI/bge-base-en-v1.5",      % default
        timeout => 120000                       % default, ms
    }}
}).

Options

Option | Type | Default | Description
python | string | "python3" | Python executable, used only if the managed venv could not be created
model | string | "BAAI/bge-base-en-v1.5" | Model name
timeout | integer | 120000 | Timeout in milliseconds

barrel_embed manages its own Python virtualenv automatically and installs sentence-transformers into it on first use. See Python Virtualenv Setup for the managed venv API and how to change its location.

Supported Models

Any model from HuggingFace that works with sentence-transformers:

Model | Dimensions | Notes
BAAI/bge-base-en-v1.5 | 768 | Good general-purpose (default)
BAAI/bge-large-en-v1.5 | 1024 | Higher quality
BAAI/bge-small-en-v1.5 | 384 | Faster, smaller
sentence-transformers/all-MiniLM-L6-v2 | 384 | Lightweight
sentence-transformers/all-mpnet-base-v2 | 768 | High quality

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {local, #{
        model => "sentence-transformers/all-MiniLM-L6-v2"
    }}
}).

%% Generate embedding
{ok, Vec} = barrel_embed:embed(<<"Hello world">>, State).
384 = length(Vec).

%% Batch embedding
{ok, Vecs} = barrel_embed:embed_batch([
    <<"First text">>,
    <<"Second text">>
], State).

Python Environment

Managed Virtualenv (Default)

barrel_embed creates a venv under its priv directory on application startup and installs sentence-transformers there automatically:

{ok, State} = barrel_embed:init(#{
    embedder => {local, #{
        model => "BAAI/bge-base-en-v1.5"
    }}
}).

Set a custom venv location via the venv_dir application env (see Python Virtualenv Setup).

Using a Different Python Interpreter

If the managed venv could not be created, barrel_embed falls back to the python option. This is also useful for Conda environments:

{ok, State} = barrel_embed:init(#{
    embedder => {local, #{
        python => "/path/to/conda/envs/myenv/bin/python"
    }}
}).

First Request Latency

The first request may be slow as it:

Starts the Python subprocess

Loads the model into memory

Subsequent requests reuse the loaded model and are much faster.

Troubleshooting

ModuleNotFoundError: sentence_transformers

Install the dependency:

pip install sentence-transformers

Model download slow

Models are downloaded from HuggingFace on first use. Set HF_HOME to control cache location:

export HF_HOME=/path/to/cache

Mistral Provider

Embedding generation using Mistral AI's Embeddings API. Mistral provides high-quality embeddings with EU data residency.

Requirements

Mistral API key from console.mistral.ai/api-keys

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {mistral, #{
        api_key => <<"...">>,                    % or use env var
        model => <<"mistral-embed">>             % default
    }}
}).

Options

Option | Type | Default | Description
api_key | binary | MISTRAL_API_KEY env var | API key
model | binary | <<"mistral-embed">> | Model name
url | binary | <<"https://api.mistral.ai/v1">> | API endpoint

Using Environment Variable

Set MISTRAL_API_KEY instead of passing in config:

export MISTRAL_API_KEY=your-api-key

{ok, State} = barrel_embed:init(#{
    embedder => {mistral, #{}}  % uses env var
}).

Supported Models

Model | Dimensions | Notes
mistral-embed | 1024 | Default, multilingual

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {mistral, #{
        api_key => <<"your-api-key">>,
        model => <<"mistral-embed">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch (more efficient for multiple texts)
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Why Mistral?

EU-based company (data residency compliance)

OpenAI-compatible API format

Competitive pricing

Strong multilingual support

Pricing

See mistral.ai/pricing for current rates.

Ollama Provider

Local embedding generation using Ollama server.

Requirements

Ollama installed and running

Setup

# Install Ollama
brew install ollama  # macOS
# or download from https://ollama.ai

# Start server
ollama serve

# Pull an embedding model
ollama pull nomic-embed-text

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {ollama, #{
        url => <<"http://localhost:11434">>,  % default
        model => <<"nomic-embed-text">>       % required
    }}
}).

Options

Option | Type | Default | Description
url | binary | <<"http://localhost:11434">> | Ollama server URL
model | binary | required | Model name

Supported Models

Popular embedding models available in Ollama:

Model | Dimensions | Notes
nomic-embed-text | 768 | Good general-purpose
mxbai-embed-large | 1024 | High quality
all-minilm | 384 | Lightweight
snowflake-arctic-embed | 1024 | Multilingual

List available models:

ollama list

Example

%% Start application
application:ensure_all_started(barrel_embed).

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {ollama, #{
        url => <<"http://localhost:11434">>,
        model => <<"nomic-embed-text">>
    }}
}).

%% Single embedding
{ok, Vec} = barrel_embed:embed(<<"Hello world">>, State).
768 = length(Vec).

%% Batch embedding
{ok, Vecs} = barrel_embed:embed_batch([
    <<"First document">>,
    <<"Second document">>,
    <<"Third document">>
], State).
3 = length(Vecs).

API Compatibility

The Ollama provider supports both API versions:

/api/embed (newer, preferred)

/api/embeddings (legacy fallback)

It automatically detects and uses the appropriate endpoint.

Troubleshooting

Connection refused

Ensure Ollama is running:

ollama serve

Model not found

Pull the model first:

ollama pull nomic-embed-text

Slow first request

The first request may be slow as Ollama loads the model into memory. Subsequent requests will be fast.

OpenAI Provider

Embedding generation using OpenAI's Embeddings API.

Requirements

OpenAI API key

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {openai, #{
        api_key => <<"sk-...">>,                    % or use env var
        model => <<"text-embedding-3-small">>       % default
    }}
}).

Options

Option | Type | Default | Description
api_key | binary | OPENAI_API_KEY env var | API key
model | binary | <<"text-embedding-3-small">> | Model name
url | binary | <<"https://api.openai.com/v1/embeddings">> | API endpoint

Using Environment Variable

Set OPENAI_API_KEY instead of passing in config:

export OPENAI_API_KEY=sk-...

{ok, State} = barrel_embed:init(#{
    embedder => {openai, #{}}  % uses env var
}).

Supported Models

Model | Dimensions | Max Tokens | Notes
text-embedding-3-small | 1536 | 8191 | Fast, cost-effective
text-embedding-3-large | 3072 | 8191 | Highest quality
text-embedding-ada-002 | 1536 | 8191 | Legacy

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {openai, #{
        api_key => <<"sk-proj-...">>,
        model => <<"text-embedding-3-small">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch (more efficient for multiple texts)
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Rate Limiting

OpenAI has rate limits. For high-volume usage, consider:

Using batch embedding instead of single calls

Implementing retry logic at the application level

Using the provider chain with fallback

#{embedder => [
    {openai, #{}},
    {ollama, #{url => <<"http://localhost:11434">>}}  % fallback
]}

Cost Considerations

OpenAI charges per token. To optimize costs:

Use text-embedding-3-small for most use cases

Batch multiple texts in single API calls

Cache embeddings when possible

SPLADE Provider

Sparse embedding generation using SPLADE models.

What is SPLADE?

SPLADE (Sparse Lexical and Expansion) produces sparse vectors instead of dense vectors. Each dimension corresponds to a vocabulary token, enabling:

Lexical matching (like BM25)

Semantic expansion (synonyms, related terms)

Efficient inverted index storage

Requirements

# Using virtualenv with uv (recommended)
./scripts/setup_venv.sh
uv pip install transformers torch --python .venv/bin/python

# Or install manually
pip install transformers torch

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {splade, #{
        python => "python3",                        % default, fallback only
        model => "prithivida/Splade_PP_en_v1",     % default
        timeout => 120000                           % default, ms
    }}
}).

Options

Option | Type | Default | Description
python | string | "python3" | Python executable, used only if the managed venv could not be created
model | string | "prithivida/Splade_PP_en_v1" | Model name
timeout | integer | 120000 | Timeout in milliseconds

barrel_embed manages its own Python virtualenv automatically and installs transformers and torch into it on first use. See Python Virtualenv Setup for the managed venv API.

Supported Models

Model | Notes
prithivida/Splade_PP_en_v1 | Default, SPLADE++
naver/splade-cocondenser-ensembledistil | NAVER's SPLADE

Sparse Vector Format

Unlike dense embeddings, SPLADE returns sparse vectors:

#{
    indices => [1, 5, 10, 42, ...],   % vocabulary token IDs
    values => [0.5, 0.3, 0.8, 0.2, ...]  % weights
}

API

Native Sparse API

%% Single text
{ok, SparseVec} = barrel_embed_splade:embed_sparse(<<"query text">>, Config).
#{indices := Indices, values := Values} = SparseVec.

%% Batch
{ok, SparseVecs} = barrel_embed_splade:embed_batch_sparse(Texts, Config).

Dense API (Compatibility)

For compatibility with dense search, SPLADE can convert to dense vectors:

%% Returns dense vector (sparse converted to dense)
{ok, DenseVec} = barrel_embed:embed(<<"text">>, State).

Warning

Dense conversion is memory-intensive for large vocabularies (~30k dimensions). Use native sparse API when possible.

Example: Hybrid Search

Combine SPLADE with dense embeddings for hybrid search:

%% SPLADE for lexical matching
{ok, SpladeState} = barrel_embed:init(#{embedder => {splade, #{}}}).
{ok, SparseVec} = barrel_embed_splade:embed_sparse(Query, SpladeConfig).

%% Dense for semantic matching
{ok, DenseState} = barrel_embed:init(#{embedder => {ollama, #{...}}}).
{ok, DenseVec} = barrel_embed:embed(Query, DenseState).

%% Combine scores (application-specific)
FinalScore = Alpha * SparseScore + (1 - Alpha) * DenseScore.

Use Cases

Keyword search with semantic expansion: SPLADE expands queries with related terms

Hybrid retrieval: Combine with dense embeddings

Efficient storage: Sparse vectors compress well

Google Vertex AI Provider

Embedding generation using Google Vertex AI's embedding API. Provides access to Google's embedding models including the Gecko family.

Requirements

Google Cloud project with Vertex AI API enabled

Authentication via access token or API key

Configuration

Option 1: Access Token (Recommended for development)

Get a token with gcloud auth print-access-token:

{ok, State} = barrel_embed:init(#{
    embedder => {vertex, #{
        access_token => <<"ya29...">>,           % or use env var
        project => <<"my-project">>,
        region => <<"us-central1">>,
        model => <<"text-embedding-004">>
    }}
}).

Option 2: API Key

{ok, State} = barrel_embed:init(#{
    embedder => {vertex, #{
        api_key => <<"...">>,                    % or use env var
        project => <<"my-project">>,
        region => <<"us-central1">>,
        model => <<"text-embedding-004">>
    }}
}).

Options

Option | Type | Default | Description
access_token | binary | GOOGLE_ACCESS_TOKEN env var | OAuth access token
api_key | binary | GOOGLE_API_KEY env var | API key
project | binary | GOOGLE_CLOUD_PROJECT env var | GCP project ID
region | binary | <<"us-central1">> | GCP region
model | binary | <<"text-embedding-004">> | Model name

Using Environment Variables

# Get access token
export GOOGLE_ACCESS_TOKEN=$(gcloud auth print-access-token)
export GOOGLE_CLOUD_PROJECT=my-project

{ok, State} = barrel_embed:init(#{
    embedder => {vertex, #{}}
}).

Supported Models

Model | Dimensions | Notes
text-embedding-004 | 768 | Default, latest
text-embedding-005 | 768 | Newest
textembedding-gecko@001 | 768 | Legacy
textembedding-gecko@003 | 768 | Legacy
textembedding-gecko-multilingual@001 | 768 | Multilingual legacy
text-multilingual-embedding-002 | 768 | Multilingual

Setup Steps

Enable Vertex AI API

Go to GCP Console

Enable "Vertex AI API"

Get Authentication

For development (access token):

gcloud auth login
gcloud auth print-access-token

For production (service account):

Create service account with Vertex AI User role

Download JSON key

Use GOOGLE_APPLICATION_CREDENTIALS

Note Project ID

gcloud config get-value project

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {vertex, #{
        access_token => <<"ya29...">>,
        project => <<"my-project">>,
        region => <<"us-central1">>,
        model => <<"text-embedding-004">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Regions

Vertex AI is available in many regions. Common choices:

us-central1 (Iowa) - Default

us-east1 (South Carolina)

europe-west1 (Belgium)

asia-northeast1 (Tokyo)

Access Token Refresh

Access tokens expire after 1 hour. For long-running applications:

Use service account authentication

Or refresh token periodically:

%% Refresh token
NewToken = os:cmd("gcloud auth print-access-token"),
{ok, NewState} = barrel_embed:init(#{
    embedder => {vertex, #{
        access_token => list_to_binary(string:trim(NewToken)),
        project => <<"my-project">>
    }}
}).

Why Vertex AI?

Google's latest embedding models

Tight integration with GCP services

BigQuery, Cloud Storage integration

Enterprise security (VPC-SC, CMEK)

Pricing

See cloud.google.com/vertex-ai/pricing for current rates.

Voyage AI Provider

Embedding generation using Voyage AI's Embeddings API. Voyage provides state-of-the-art embeddings optimized for retrieval and RAG applications.

Requirements

Voyage AI API key from dash.voyageai.com/api-keys

Configuration

{ok, State} = barrel_embed:init(#{
    embedder => {voyage, #{
        api_key => <<"...">>,                    % or use env var
        model => <<"voyage-3">>                  % default
    }}
}).

Options

Option | Type | Default | Description
api_key | binary | VOYAGE_API_KEY env var | API key
model | binary | <<"voyage-3">> | Model name
url | binary | <<"https://api.voyageai.com/v1">> | API endpoint

Using Environment Variable

Set VOYAGE_API_KEY instead of passing in config:

export VOYAGE_API_KEY=pa-...

{ok, State} = barrel_embed:init(#{
    embedder => {voyage, #{}}  % uses env var
}).

Supported Models

Model | Dimensions | Notes
voyage-3 | 1024 | Default, best quality
voyage-3-lite | 512 | Faster, cheaper
voyage-code-3 | 1024 | Optimized for code
voyage-finance-2 | 1024 | Financial domain
voyage-law-2 | 1024 | Legal domain
voyage-large-2 | 1536 | Legacy, high quality

Domain-Specific Models

Voyage offers specialized models for specific domains:

%% For code search
{ok, State} = barrel_embed:init(#{
    embedder => {voyage, #{model => <<"voyage-code-3">>}}
}).

%% For legal documents
{ok, State} = barrel_embed:init(#{
    embedder => {voyage, #{model => <<"voyage-law-2">>}}
}).

%% For financial documents
{ok, State} = barrel_embed:init(#{
    embedder => {voyage, #{model => <<"voyage-finance-2">>}}
}).

Example

%% Initialize
{ok, State} = barrel_embed:init(#{
    embedder => {voyage, #{
        api_key => <<"pa-...">>,
        model => <<"voyage-3">>
    }}
}).

%% Generate embeddings
{ok, Vec} = barrel_embed:embed(<<"Machine learning is fascinating">>, State).

%% Batch (more efficient for multiple texts)
{ok, Vecs} = barrel_embed:embed_batch([
    <<"Document about AI">>,
    <<"Document about databases">>,
    <<"Document about networking">>
], State).

Why Voyage AI?

Top-ranked on MTEB benchmark for retrieval

Optimized specifically for RAG and search

Domain-specific models for code, law, finance

OpenAI-compatible API format

Pricing

See voyageai.com/pricing for current rates.

Design

This document describes the architecture and internals of barrel_embed.

Architecture Overview

barrel_embed uses a two-layer architecture that bridges Erlang and Python:

┌─────────────────────────────────────────────────────────┐
│                    Erlang Application                    │
├─────────────────────────────────────────────────────────┤
│  barrel_embed (API)                                      │
│       ↓                                                  │
│  barrel_embed_local/fastembed/splade/... (Providers)    │
│       ↓                                                  │
│  barrel_embed_port_server (gen_server)                  │
│       ↓ JSON-lines over stdio                           │
├─────────────────────────────────────────────────────────┤
│  Python Process                                          │
│       ↓                                                  │
│  barrel_embed (asyncio server)                          │
│       ↓                                                  │
│  providers/* (sentence_transformers, fastembed, etc.)   │
└─────────────────────────────────────────────────────────┘

Key components:

barrel_embed - Main API module that coordinates providers

Provider modules - Implement the barrel_embed_provider behaviour

barrel_embed_port_server - gen_server managing Python port communication

Python async server - Asyncio-based server handling embedding requests

Python providers - Model-specific implementations

Request Multiplexing

The Problem

Without multiplexing, concurrent Erlang processes calling embed_batch would serialize - each would wait for the previous to complete. This is problematic because:

Embedding generation is CPU-bound (model inference)

Multiple Erlang processes often need embeddings concurrently

Serializing requests significantly increases latency

The Solution

Request multiplexing allows concurrent requests to the same model:

Erlang gen_server assigns a unique ID to each request

All requests are sent immediately to Python (non-blocking)

Python processes requests concurrently using a thread pool

Python includes the request ID in each response

gen_server routes responses to the correct caller

Sequence Diagram

Process A ──┐
Process B ──┼──► gen_server ──► Python ──► Model
Process C ──┘        │              │
                     │◄─────────────┘
                     │ (routes by ID)
            ┌────────┼────────┐
            ↓        ↓        ↓
         Proc A   Proc B   Proc C

Each process receives its response as soon as it's ready, not waiting for other processes.

Erlang Port Server

barrel_embed_port_server is a gen_server that manages Python port communication.

State Structure

-record(state, {
    port :: port(),                                    % Erlang port to Python
    pending = #{} :: #{integer() => {pid(), reference()}},  % Request ID to caller
    next_id = 1 :: integer(),                          % Next request ID
    timeout :: timeout(),                              % Default timeout
    buffer = <<>> :: binary()                          % Partial line buffer
}).

Request Flow

Receive call - handle_call({embed_batch, Texts}, From, State)

Build request - JSON with action, texts, and unique ID

Send to port - port_command(Port, [Json, "\n"])

Track caller - Store From in pending map with ID as key

Return noreply - Caller blocks waiting for response

Response Flow

Port data arrives - handle_info({Port, {data, {eol, Line}}}, State)

Parse JSON - Extract response and ID

Lookup caller - Find From in pending map by ID

Reply - gen_server:reply(From, Result)

Cleanup - Remove ID from pending map

Buffer Handling

Large embedding responses may exceed line buffer size:

handle_info({Port, {data, {noeol, Partial}}}, State) ->
    %% Accumulate partial data
    {noreply, State#state{buffer = <<Buffer/binary, Partial/binary>>}};

handle_info({Port, {data, {eol, Line}}}, State) ->
    %% Combine buffer with complete line
    FullLine = <<Buffer/binary, Line/binary>>,
    handle_response(FullLine, State#state{buffer = <<>>});

Python Async Server

AsyncEmbedServer Base Class

The AsyncEmbedServer provides the async infrastructure:

class AsyncEmbedServer(ABC):
    def __init__(self, max_workers: int = 4):
        self.executor = ThreadPoolExecutor(max_workers=max_workers)
        self.write_lock = asyncio.Lock()

    async def run(self):
        # Setup async stdin/stdout
        # Main loop: read request, dispatch, write response

    async def handle_request(self, line: bytes, writer):
        # Parse JSON, dispatch, include ID in response

    async def dispatch(self, request: dict) -> dict:
        # Route to appropriate handler

    async def handle_embed(self, texts: list) -> dict:
        # Run embed_sync in thread pool
        result = await loop.run_in_executor(self.executor, self.embed_sync, texts)
        return result

Concurrency Model

asyncio - Non-blocking I/O for stdin/stdout

ThreadPoolExecutor - CPU-bound embedding work runs in threads

write_lock - Prevents output interleaving from concurrent handlers

uvloop (optional) - Higher performance event loop

Request Handling

async def handle_request(self, line: bytes, writer):
    request_id = None
    try:
        request = json.loads(line.decode())
        request_id = request.get("id")
        response = await self.dispatch(request)
    except Exception as e:
        response = {"ok": False, "error": str(e)}

    if request_id is not None:
        response["id"] = request_id

    async with self.write_lock:
        output = json.dumps(response) + "\n"
        writer.write(output.encode())
        await writer.drain()

JSON Protocol

Communication uses newline-delimited JSON (JSON Lines).

Request Format

{"id": 1, "action": "embed", "texts": ["hello", "world"]}
{"id": 2, "action": "info"}
{"id": 3, "action": "embed_image", "images": ["base64..."]}

Field | Type | Description
id | integer | Unique request identifier
action | string | One of: embed, embed_image, info
texts | array | Text strings to embed
images | array | Base64-encoded images

Response Format

Success:

{"id": 1, "ok": true, "embeddings": [[0.1, 0.2, ...], [0.3, 0.4, ...]]}
{"id": 2, "ok": true, "dimensions": 768, "model": "BAAI/bge-base-en-v1.5", "backend": "fastembed"}

Error:

{"id": 3, "ok": false, "error": "Invalid image data"}

Field | Type | Description
id | integer | Matches request ID
ok | boolean | Success indicator
embeddings | array | List of embedding vectors
error | string | Error message (when ok=false)

Provider Lifecycle

Initialization

Erlang provider init - barrel_embed_fastembed:init(Config)

Start port server - barrel_embed_port_server:start_link(Python, Args, Opts)

Python starts - Parses args, imports provider module

Load model - server.load_model() downloads/loads model

Get info - Erlang calls info/2 to get dimensions

Ready - Provider stores server pid in config

Embedding Request

User calls - barrel_embed:embed(Text, State)

Provider dispatches - barrel_embed_fastembed:embed_batch([Text], Config)

Port server sends - JSON request to Python

Python embeds - Thread pool runs model inference

Response returns - JSON with embeddings

Result delivered - To original caller

Cleanup

When the Erlang process terminates:

Port server's terminate/2 called

port_close(Port) signals Python

Python's stdin closes, async loop exits

Thread pool shuts down

One Server Per Model

Each provider instance creates its own:

gen_server process

Python port/process

Loaded model in memory

This is intentional:

Different models need separate Python processes

Model isolation prevents memory/state conflicts

Multiplexing benefits concurrent calls to the SAME model

If you need multiple embedding models, initialize multiple provider states:

{ok, BgeState} = barrel_embed:init(#{embedder => {fastembed, #{model => "BAAI/bge-small-en-v1.5"}}}).
{ok, NomicState} = barrel_embed:init(#{embedder => {fastembed, #{model => "nomic-ai/nomic-embed-text-v1.5"}}}).

Error Handling

Python Errors

try:
    embeddings = self.model.encode(texts)
    return {"ok": True, "embeddings": embeddings.tolist()}
except Exception as e:
    return {"ok": False, "error": str(e)}

Erlang Error Decoding

decode_response(#{<<"ok">> := false, <<"error">> := Err}) ->
    {error, {python_error, Err}};

Port Exit Handling

If Python crashes:

handle_info({Port, {exit_status, Status}}, State) ->
    %% Fail all pending requests
    maps:foreach(fun(_, From) ->
        gen_server:reply(From, {error, {port_exited, Status}})
    end, Pending),
    {stop, {port_exited, Status}, State}.

All pending callers receive an error tuple rather than hanging.

Performance Considerations

Thread Pool Sizing

Default max_workers=4 balances:

CPU utilization - Multiple concurrent embeddings

Memory - Each thread may hold model state

Contention - Too many threads cause lock contention

Large Batches

For very large batches, consider:

Using batch_size option to chunk requests

The line buffer is 10MB but very large responses may need buffering

uvloop

Install uvloop for better async performance:

pip install uvloop

The server auto-detects and uses it when available.

Changelog

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

2.3.0 - 2026-07-08

Coordinated umbrella release.

Added

barrel_embed_request_tests: mocks hackney to assert that the OpenAI and Cohere adapters build the right URL, auth header, and JSON body, and parse responses back in index order, without hitting the network

CI now runs these backend-free request-building tests as part of the umbrella job (venv- and Ollama-dependent tests stay excluded)

rebar3_hex added to the plugin list so the app can be published standalone to Hex

Changed

hackney pinned to 4.4.5 to match barrel_vectordb in the umbrella build

cover_enabled turned on for parity with the other umbrella apps

Package links point at the umbrella repository (https://github.com/barrel-db/barrel) instead of a per-app repo

Documentation migrated from MkDocs to ExDoc; see the guides linked from this package's HexDocs page

2.2.1 - 2026-04-02

Changed

Removed erlang_python dependency from documentation

Updated README to reflect port-based Python integration

Regenerated rebar.lock without erlang_python

2.2.0 - 2026-04-02

Added

Managed Virtual Environment

Auto-create Python venv at priv/.venv on application startup

Auto-install provider dependencies when providers are initialized

uvloop is now required on Unix systems for optimal async performance

New barrel_embed_venv module for venv lifecycle management

New API Functions

barrel_embed:refresh_venv/0 - Recreate managed venv from scratch

barrel_embed:install_provider/1 - Install dependencies for a specific provider

barrel_embed:venv_path/0 - Get the managed venv path

barrel_embed:has_uvloop/0 - Check if uvloop is installed

Configuration

{barrel_embed, [{venv_dir, "/custom/path"}]} - Custom venv location

Providers auto-use managed venv when no explicit venv option is set

Changed

Restored Port-Based Backend

Breaking: Reverted from erlang_python NIF to port-based JSON communication

Removed erlang_python dependency (no more NIF compilation required)

Restored barrel_embed_port_server for Python process management

Async Python server with request multiplexing via newline-delimited JSON

Simpler deployment: no NIF compilation, works on any platform with Python

Providers (local, fastembed, splade, colbert, clip) now default to managed venv

uvloop installation failures now cause venv creation to fail (was silent warning)

2.1.1 - 2026-03-31

Added

Ollama integration tests (barrel_embed_ollama_tests)

Tests against real Ollama instance when available

Covers init, embed, embed_batch, dimension, and availability

Error handling tests for unavailable endpoints

2.0.1 - 2026-03-07

Added

Apache-2.0 LICENSE file

examples/basic_usage.erl with ollama, openai, and local provider examples

ex_doc configuration in rebar.config for HexDocs generation

Hex.pm links in app.src (GitHub, Documentation)

Changed

README now uses Hex.pm dependency format

Documentation links point to docs.barrel-db.eu

Support links changed from GitLab to GitHub Issues

2.0.0 - 2026-02-20

Changed

New erlang_python NIF Backend

Breaking: Replaced port-based stdio JSON communication with erlang_python 1.5.0 NIF integration

All Python providers now use direct py:call instead of subprocess communication

2.7x improvement in batch throughput (936 vs 348 texts/sec with bge-small-en-v1.5)

Requires erlang_python 1.5.0+ as a dependency

New Modules

barrel_embed_py - Erlang wrapper for py:call with timeout support

priv/barrel_embed/nif_api.py - Thread-safe Python API for model caching

Removed

barrel_embed_port_server - Port-based Python server (replaced by NIF)

barrel_embed_python_queue - Rate limiting queue (erlang_python handles this)

priv/barrel_embed/server.py - Async stdio server

priv/barrel_embed/__main__.py - CLI entry point

priv/barrel_embed/providers/ - Provider classes (now in nif_api.py)

Migration

If you were using custom provider configurations, the API remains the same. The venv option is still supported and recommended:

{ok, State} = barrel_embed:init(#{
    embedder => {local, #{
        venv => "/path/to/.venv"
    }}
}).

1.0.0 - 2026-01-27

Added

Virtual Environment Support

Added venv configuration option for all Python providers (local, fastembed, splade, colbert, clip)

Proper venv activation in port environment (sets VIRTUAL_ENV, PATH, PYTHONPATH)

New scripts/setup_venv.sh for fast venv setup using uv

Requirements files for different installation profiles:

priv/requirements.txt - Default (sentence-transformers + uvloop)

priv/requirements-minimal.txt - Minimal (no ML libs)

priv/requirements-full.txt - All providers

Documentation: docs/venv-setup.md

CI Improvements

Added integration:venv job to test Erlang-Python venv communication

Python tests now use uv for faster dependency installation

Changed

setup_python_venv.sh now uses uv when available (falls back to pip)

Python queue default limit changed from schedulers/2 + 1 to schedulers * 2 + 1

Updated all Python provider documentation with venv examples

0.2.0 - 2026-01-27

Added

Cloud Providers

cohere - Cohere Embed API with input type optimization

voyage - Voyage AI for RAG and domain-specific embeddings (code, law, finance)

jina - Jina AI with 8K context and free tier

mistral - Mistral AI with EU data residency

azure - Azure OpenAI for enterprise compliance

bedrock - AWS Bedrock (Titan, Cohere models) with IAM and API key auth

vertex - Google Vertex AI for GCP ecosystem

Documentation

Provider comparison guide (docs/choosing-provider.md)

Developer guide for adding cloud providers (docs/dev/adding-provider.md)

Individual documentation pages for all cloud providers

Tooling

scripts/setup_python_venv.sh for one-command Python venv setup

Added --dev option for installing dev dependencies (test + uvloop)

Added --uvloop option to install uvloop via pyproject.toml extra

Added --test option to run Python tests after setup

Added -h/--help option for usage information

GitLab CI configuration (.gitlab-ci.yml)

Erlang tests with rebar3 eunit

Python tests with pytest

Dialyzer type checking

Xref cross-reference checks

Testing

Python test suite for uvloop integration (priv/tests/test_server.py)

uvloop detection and event loop policy tests

AsyncEmbedServer dispatch and handler tests

Concurrent task execution tests

Python Engine

Async request multiplexing for concurrent embeddings

Improved error handling and logging

Changed

Updated hackney dependency to 2.0.1 for HTTP/2 support

Provider init now properly loads modules before checking exports

Removed redundant application:ensure_all_started(hackney) from providers (hackney starts via app.src)

0.1.0 - 2026-01-14

Added

Initial release extracted from barrel_vectordb

Core embedding coordinator (barrel_embed) with provider chain and fallback support

Provider behaviour (barrel_embed_provider) for implementing custom providers

Python execution rate limiter (barrel_embed_python_queue)

Providers

local - Local Python with sentence-transformers

ollama - Ollama server API (supports both /api/embed and /api/embeddings)

openai - OpenAI Embeddings API

fastembed - FastEmbed ONNX-based embeddings (lighter than sentence-transformers)

splade - SPLADE sparse embeddings for hybrid search

embed_sparse/2, embed_batch_sparse/2 for native sparse vectors

Automatic sparse-to-dense conversion for compatibility

colbert - ColBERT multi-vector embeddings for fine-grained matching

embed_multi/2, embed_batch_multi/2 for token-level vectors

maxsim_score/2 for late interaction scoring

clip - CLIP image/text cross-modal embeddings

embed_image/2, embed_image_batch/2 for image embeddings

Text embeddings in same vector space for cross-modal search

Features

Batch embedding with configurable chunk size

Provider chain with automatic fallback on failure

Application supervision tree with ETS-based rate limiting

EUnit test suite

LICENSE

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to the Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   Copyright 2024 Benoit Chesneau

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

barrel_embed

Embedding coordinator

Lightweight embedding generation for Erlang. Manages a chain of embedding providers with automatic fallback.

Providers

Local: - local: Local Python with sentence-transformers (CPU, no external calls) - ollama: Local Ollama server - fastembed: FastEmbed ONNX-based embeddings (lighter than sentence-transformers)

Cloud: - openai: OpenAI Embeddings API - cohere: Cohere Embed API - voyage: Voyage AI Embeddings - jina: Jina AI Embeddings - mistral: Mistral AI Embeddings (EU data residency) - azure: Azure OpenAI Embeddings - bedrock: AWS Bedrock Embeddings (Titan, Cohere) - vertex: Google Vertex AI Embeddings

Specialized: - splade: SPLADE sparse embeddings for hybrid search - colbert: ColBERT multi-vector embeddings for fine-grained matching - clip: CLIP image/text cross-modal embeddings

Configuration

   %% Local Python embeddings (requires Python + sentence-transformers)
   Embedder = {local, #{
       python => "python3",
       model => "BAAI/bge-base-en-v1.5"
   }}.

   %% Ollama server
   Embedder = {ollama, #{
       url => <<"http://localhost:11434">>,
       model => <<"nomic-embed-text">>
   }}.

   %% OpenAI API (requires API key)
   Embedder = {openai, #{
       api_key => <<"sk-...">>,  %% or set OPENAI_API_KEY env var
       model => <<"text-embedding-3-small">>
   }}.

   %% Provider chain with fallback
   Embedder = [
       {ollama, #{url => <<"http://localhost:11434">>}},
       {local, #{}}  %% fallback to CPU
   ].

Summary

Types

embed_state/0

provider/0

provider_chain/0

Functions

dimension(_)

Get the dimension of embeddings.

embed(Text, State)

Generate embedding for a single text.

embed_batch(Texts, State)

Generate embeddings for multiple texts.

embed_batch(Texts, Options, _)

Generate embeddings for multiple texts with options.

has_uvloop()

Check if uvloop is installed in the managed venv.

info(_)

Get information about the current embedding configuration.

init(Config)

Initialize embedding state from configuration.

install_provider(Provider)

Install dependencies for a specific provider.

refresh_venv()

Refresh the managed venv. Deletes and recreates the venv with base dependencies.

venv_path()

Get the managed venv path.

Types

embed_state/0

-type embed_state() ::
          #{providers := [provider()], dimension := pos_integer(), batch_size := pos_integer()}.

provider/0

-type provider() :: {atom(), map()}.

provider_chain/0

-type provider_chain() :: [provider()] | provider().

Functions

dimension(_)

-spec dimension(embed_state() | undefined) -> pos_integer() | undefined.

Get the dimension of embeddings.

embed(Text, State)

-spec embed(binary(), embed_state() | undefined) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, State)

-spec embed_batch([binary()], embed_state() | undefined) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts.

embed_batch(Texts, Options, _)

-spec embed_batch([binary()], map(), embed_state() | undefined) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts with options.

has_uvloop()

-spec has_uvloop() -> boolean().

Check if uvloop is installed in the managed venv.

info(_)

-spec info(embed_state() | undefined) -> map().

Get information about the current embedding configuration.

init(Config)

-spec init(map()) -> {ok, embed_state() | undefined} | {error, term()}.

Initialize embedding state from configuration.

If no embedder key is present in Config, returns {ok, undefined}.

install_provider(Provider)

-spec install_provider(atom()) -> ok | {error, term()}.

Install dependencies for a specific provider.

refresh_venv()

-spec refresh_venv() -> {ok, string()} | {error, term()}.

Refresh the managed venv. Deletes and recreates the venv with base dependencies.

venv_path()

-spec venv_path() -> string().

Get the managed venv path.

barrel_embed_app

barrel_embed application callback module

Summary

Functions

start(StartType, StartArgs)

stop(State)

Functions

start(StartType, StartArgs)

stop(State)

barrel_embed_azure

Azure OpenAI embedding provider

Uses Azure OpenAI's Embeddings API for embedding generation. Provides enterprise-grade embeddings with Azure security and compliance.

Requirements

- Azure OpenAI resource with an embedding model deployment - API key from Azure Portal -> Your Resource -> Keys

Configuration requires either: - Environment variables: AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT - Or passed in config directly

Configuration

   Config = #{
       api_key => <<"...">>,                           %% API key (or use env var)
       endpoint => <<"https://your-resource.openai.azure.com">>,  %% Azure endpoint
       deployment => <<"text-embedding-ada-002">>,     %% Deployment name
       api_version => <<"2024-02-01">>,                %% API version (default)
       timeout => 30000,                               %% Timeout in ms (default)
       dimension => 1536                               %% Vector dimension (default)
   }.

Supported Models

Deploy these models in Azure OpenAI Studio: - <<"text-embedding-3-small">> - 1536 dims, fast and cost-effective - <<"text-embedding-3-large">> - 3072 dims, highest quality - <<"text-embedding-ada-002">> - 1536 dims, legacy

Summary

Functions

available(Config)

Check if Azure OpenAI API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Azure OpenAI supports native batch embedding.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Azure OpenAI API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Azure OpenAI supports native batch embedding.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_bedrock

AWS Bedrock embedding provider

Uses AWS Bedrock's embedding API for embedding generation. Provides access to Titan and Cohere embedding models on AWS.

Requirements

AWS credentials, either via API key (ABSK format) or IAM credentials.

Option 1: API Key (Recommended)

Uses AWS Bedrock API Key (ABSK format, available since July 2025): - Set BEDROCK_API_KEY environment variable - Or pass api_key => <<"ABSK...">>> in config

Option 2: IAM Credentials

- Set AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and optionally AWS_SESSION_TOKEN environment variables - Or pass access_key_id, secret_access_key in config

Configuration

   %% Using API key (simpler)
   Config = #{
       api_key => <<"ABSK...">>,                       %% API key (or use env var)
       region => <<"us-east-1">>,                      %% AWS region (default)
       model => <<"amazon.titan-embed-text-v2:0">>,    %% Model ID (default)
       timeout => 30000,                               %% Timeout in ms (default)
       dimension => 1024                               %% Vector dimension (default)
   }.

   %% Using IAM credentials (requires SigV4 signing)
   Config = #{
       access_key_id => <<"AKIA...">>,
       secret_access_key => <<"...">>,
       session_token => <<"...">>,                     %% Optional, for temporary credentials
       region => <<"us-east-1">>,
       model => <<"amazon.titan-embed-text-v2:0">>
   }.

Supported Models

- <<"amazon.titan-embed-text-v2:0">> - Default, 1024 dims - <<"amazon.titan-embed-text-v1">> - 1536 dims, legacy - <<"cohere.embed-english-v3">> - 1024 dims, Cohere on Bedrock - <<"cohere.embed-multilingual-v3">> - 1024 dims, multilingual

Summary

Functions

available(Config)

Check if Bedrock API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Bedrock doesn't support native batch, so we process sequentially.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Bedrock API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Bedrock doesn't support native batch, so we process sequentially.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_clip

CLIP image/text embedding provider

Uses CLIP (Contrastive Language-Image Pre-training) models for cross-modal embeddings. Both images and text are encoded into the same vector space, enabling image-text similarity search.

Dependencies (transformers, torch, pillow) are installed automatically in the managed venv on first use.

Configuration

   Config = #{
       model => "openai/clip-vit-base-patch32",   %% Model name (default)
       python => "python3",                       %% Python executable (default)
       timeout => 120000                          %% Timeout in ms (default)
   }.

Cross-Modal Search

CLIP enables searching images with text queries and vice versa:

   %% Embed an image
   {ok, ImgVec} = embed_image(ImageBase64, Config),

   %% Embed a text query (in same space!)
   {ok, TextVec} = embed(<<"a photo of a cat">>, Config),

   %% Now you can compare ImgVec and TextVec with cosine similarity

Supported Models

- "openai/clip-vit-base-patch32" - Default, 512 dimensions, fast - "openai/clip-vit-base-patch16" - 512 dimensions, higher quality - "openai/clip-vit-large-patch14" - 768 dimensions, best quality - "laion/CLIP-ViT-B-32-laion2B-s34B-b79K" - 512 dims, LAION trained

Use Cases

- Image search with text queries - Finding similar images - Multi-modal content retrieval - Zero-shot image classification

Summary

Functions

available(Config)

Check if provider is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate text embedding (for cross-modal search). Text embeddings are in the same space as image embeddings.

embed_batch(Texts, Config)

Generate text embeddings for batch.

embed_image(ImageBase64, Config)

Generate embedding for a single image. Image should be base64-encoded.

embed_image_batch(Images, Config)

Generate embeddings for multiple images. Images should be base64-encoded.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if provider is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate text embedding (for cross-modal search). Text embeddings are in the same space as image embeddings.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate text embeddings for batch.

embed_image(ImageBase64, Config)

-spec embed_image(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single image. Image should be base64-encoded.

embed_image_batch(Images, Config)

-spec embed_image_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple images. Images should be base64-encoded.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_cohere

Cohere embedding provider

Uses Cohere's Embed API for embedding generation. Requires a Cohere API key.

Requirements

A Cohere API key, either: - Set via COHERE_API_KEY environment variable - Passed in config as api_key => <<"...">>>

Configuration

   Config = #{
       api_key => <<"...">>,                           %% API key (or use env var)
       url => <<"https://api.cohere.com/v1">>,         %% API base URL (default)
       model => <<"embed-english-v3.0">>,              %% Model name (default, 1024 dims)
       input_type => <<"search_document">>,            %% Input type (default)
       timeout => 30000,                               %% Timeout in ms (default)
       dimension => 1024                               %% Vector dimension (default)
   }.

Input Types

- <<"search_document">> - For documents to be searched - <<"search_query">> - For search queries - <<"classification">> - For classification tasks - <<"clustering">> - For clustering tasks

Supported Models

- <<"embed-english-v3.0">> - Default, 1024 dims, English - <<"embed-multilingual-v3.0">> - 1024 dims, 100+ languages - <<"embed-english-light-v3.0">> - 384 dims, faster/cheaper - <<"embed-multilingual-light-v3.0">> - 384 dims, multilingual light

Summary

Functions

available(Config)

Check if Cohere API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Cohere supports native batch embedding.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Cohere API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Cohere supports native batch embedding.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_colbert

ColBERT late interaction embedding provider

Uses ColBERT models for multi-vector embeddings. Each document produces multiple vectors (one per token) for fine-grained matching.

Dependencies (transformers, torch) are installed automatically in the managed venv on first use.

Configuration

   Config = #{
       model => "colbert-ir/colbertv2.0",     %% Model name (default, 128 dims)
       python => "python3",                   %% Python executable (default)
       timeout => 120000                      %% Timeout in ms (default)
   }.

Multi-Vector Format

Unlike single-vector embeddings, ColBERT produces a list of vectors:

   [[0.1, 0.2, ...], [0.3, 0.4, ...], ...]  %% One vector per token

Late Interaction

ColBERT scoring uses MaxSim:

   Score(Q, D) = sum(max(qi · dj for all dj in D) for all qi in Q)

This enables fine-grained token-level matching.

Supported Models

- "colbert-ir/colbertv2.0" - Default, 128 dimensions - "answerdotai/answerai-colbert-small-v1" - 96 dimensions, smaller - "jinaai/jina-colbert-v2" - 128 dimensions, long context (8192 tokens)

Use Cases

- Fine-grained semantic matching - Passage retrieval with token-level scoring - Question answering

Summary

Types

multi_vector/0

Functions

available(Config)

Check if provider is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate single-vector embedding (mean pooling of token vectors). Note: For ColBERT, use embed_multi/2 to get full multi-vector output.

embed_batch(Texts, Config)

Generate single-vector embeddings for batch (mean pooling).

embed_batch_multi(Texts, Config)

Generate multi-vector embeddings for multiple texts.

embed_multi(Text, Config)

Generate multi-vector embedding for a single text. Returns a list of token vectors.

init(Config)

Initialize the provider.

maxsim_score(QueryVecs, DocVecs)

Calculate MaxSim score between query and document multi-vectors. This is the standard ColBERT scoring function. Score = sum(max(qi · dj for all dj in D) for all qi in Q)

name()

Provider name.

Types

multi_vector/0

-type multi_vector() :: [[float()]].

Functions

available(Config)

-spec available(map()) -> boolean().

Check if provider is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate single-vector embedding (mean pooling of token vectors). Note: For ColBERT, use embed_multi/2 to get full multi-vector output.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate single-vector embeddings for batch (mean pooling).

embed_batch_multi(Texts, Config)

-spec embed_batch_multi([binary()], map()) -> {ok, [multi_vector()]} | {error, term()}.

Generate multi-vector embeddings for multiple texts.

embed_multi(Text, Config)

-spec embed_multi(binary(), map()) -> {ok, multi_vector()} | {error, term()}.

Generate multi-vector embedding for a single text. Returns a list of token vectors.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

maxsim_score(QueryVecs, DocVecs)

-spec maxsim_score(multi_vector(), multi_vector()) -> float().

Calculate MaxSim score between query and document multi-vectors. This is the standard ColBERT scoring function. Score = sum(max(qi · dj for all dj in D) for all qi in Q)

name()

-spec name() -> atom().

Provider name.

barrel_embed_fastembed

FastEmbed embedding provider

Uses FastEmbed (ONNX-based) for lightweight, fast embeddings. Lighter alternative to sentence-transformers with similar quality.

Dependencies (fastembed) are installed automatically in the managed venv on first use.

Configuration

   Config = #{
       model => "BAAI/bge-small-en-v1.5",       %% Model name (default, 384 dims)
       python => "python3",                     %% Python executable (default)
       timeout => 120000                        %% Timeout in ms (default)
   }.

Advantages over sentence-transformers

- Smaller install size (~100MB vs ~2GB+) - No PyTorch dependency - Uses ONNX Runtime for optimized inference - Similar embedding quality

Summary

Functions

available(Config)

Check if provider is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts.

init(Config)

Initialize the provider. Starts the Python port server.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if provider is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider. Starts the Python port server.

name()

-spec name() -> atom().

Provider name.

barrel_embed_jina

Jina AI embedding provider

Uses Jina AI's Embeddings API for embedding generation. Jina provides multilingual embeddings with 8K context length.

Requirements

A Jina AI API key, either: - Set via JINA_API_KEY environment variable - Passed in config as api_key => <<"...">>>

Configuration

   Config = #{
       api_key => <<"...">>,                           %% API key (or use env var)
       url => <<"https://api.jina.ai/v1">>,            %% API base URL (default)
       model => <<"jina-embeddings-v3">>,              %% Model name (default, 1024 dims)
       timeout => 30000,                               %% Timeout in ms (default)
       dimension => 1024                               %% Vector dimension (default)
   }.

Supported Models

- <<"jina-embeddings-v3">> - Default, 1024 dims, 8K context, multilingual - <<"jina-embeddings-v2-base-en">> - 768 dims, English - <<"jina-embeddings-v2-base-de">> - 768 dims, German - <<"jina-embeddings-v2-base-es">> - 768 dims, Spanish - <<"jina-embeddings-v2-base-zh">> - 768 dims, Chinese - <<"jina-colbert-v2">> - 128 dims per token, late interaction - <<"jina-clip-v1">> - 768 dims, multimodal (text + images)

Summary

Functions

available(Config)

Check if Jina AI API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Jina AI supports native batch embedding.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Jina AI API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Jina AI supports native batch embedding.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_local

Local Python embedding provider

Uses a Python port with sentence-transformers for CPU-based embeddings. No GPU required, runs entirely on CPU.

Dependencies (sentence-transformers) are installed automatically in the managed venv on first use.

Configuration

   Config = #{
       model => "BAAI/bge-base-en-v1.5",        %% Model name (default, 768 dims)
       python => "python3",                     %% Python executable (default)
       timeout => 120000                        %% Timeout in ms (default)
   }.

Supported Models

Any model from sentence-transformers or HuggingFace.

Common models: - "BAAI/bge-base-en-v1.5" - Default, 768 dimensions, good quality/speed - "BAAI/bge-small-en-v1.5" - 384 dimensions, faster - "BAAI/bge-large-en-v1.5" - 1024 dimensions, best quality - "sentence-transformers/all-MiniLM-L6-v2" - 384 dims, fast - "sentence-transformers/all-mpnet-base-v2" - 768 dims, high quality - "nomic-ai/nomic-embed-text-v1.5" - 768 dims, long context

Summary

Functions

available(Config)

Check if provider is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts.

init(Config)

Initialize the provider. Starts the Python port server.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if provider is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider. Starts the Python port server.

name()

-spec name() -> atom().

Provider name.

barrel_embed_mistral

Mistral AI embedding provider

Uses Mistral AI's Embeddings API for embedding generation. Mistral provides high-quality embeddings with EU data residency.

Requirements

A Mistral API key, either: - Set via MISTRAL_API_KEY environment variable - Passed in config as api_key => <<"...">>>

Configuration

   Config = #{
       api_key => <<"...">>,                           %% API key (or use env var)
       url => <<"https://api.mistral.ai/v1">>,         %% API base URL (default)
       model => <<"mistral-embed">>,                   %% Model name (default, 1024 dims)
       timeout => 30000,                               %% Timeout in ms (default)
       dimension => 1024                               %% Vector dimension (default)
   }.

Supported Models

- <<"mistral-embed">> - Default, 1024 dims, multilingual

Summary

Functions

available(Config)

Check if Mistral API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Mistral supports native batch embedding.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Mistral API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Mistral supports native batch embedding.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_ollama

Ollama embedding provider

Uses Ollama's local API for embedding generation. Requires Ollama to be running locally.

Requirements

   # Install Ollama from https://ollama.ai
   # Pull an embedding model:
   ollama pull nomic-embed-text

Configuration

   Config = #{
       url => <<"http://localhost:11434">>,   %% Ollama API URL (default)
       model => <<"nomic-embed-text">>,       %% Model name (default, 768 dims)
       timeout => 30000                       %% Timeout in ms (default)
   }.

Supported Models

Any Ollama embedding model. Popular choices:

- <<"nomic-embed-text">> - Default, 768 dims, general purpose - <<"mxbai-embed-large">> - 1024 dims, high quality - <<"all-minilm">> - 384 dims, fast - <<"snowflake-arctic-embed">> - 1024 dims, multilingual

Summary

Functions

available(Config)

Check if Ollama is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Ollama /api/embed supports batch natively.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Ollama is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Ollama /api/embed supports batch natively.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_openai

OpenAI embedding provider

Uses OpenAI's Embeddings API for embedding generation. Requires an OpenAI API key.

Requirements

An OpenAI API key, either: - Set via OPENAI_API_KEY environment variable - Passed in config as api_key => <<"sk-...">>>

Configuration

   Config = #{
       api_key => <<"sk-...">>,                    %% API key (or use env var)
       url => <<"https://api.openai.com/v1">>,     %% API base URL (default)
       model => <<"text-embedding-3-small">>,      %% Model name (default, 1536 dims)
       timeout => 30000,                           %% Timeout in ms (default)
       dimension => 1536                           %% Vector dimension (default)
   }.

Supported Models

- <<"text-embedding-3-small">> - Default, 1536 dims, fast and cheap - <<"text-embedding-3-large">> - 3072 dims, higher quality - <<"text-embedding-ada-002">> - 1536 dims, legacy model

Summary

Functions

available(Config)

Check if OpenAI API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. OpenAI supports native batch embedding.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if OpenAI API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. OpenAI supports native batch embedding.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_port_server

Async Python port server with request multiplexing

A gen_server that manages communication with a Python embedding server. Supports concurrent requests by tracking request IDs and routing responses back to the correct callers.

Usage

   %% Start the server
   {ok, Server} = barrel_embed_port_server:start_link(Python, Args, Opts).

   %% Make concurrent embedding requests
   spawn(fun() -> barrel_embed_port_server:embed_batch(Server, Texts1, 60000) end).
   spawn(fun() -> barrel_embed_port_server:embed_batch(Server, Texts2, 60000) end).

   %% Stop when done
   barrel_embed_port_server:stop(Server).

Request Multiplexing

Multiple Erlang processes can call embed_batch/3 concurrently. Each request is assigned a unique ID and sent to Python. The Python server includes the ID in its response, allowing this gen_server to route responses to the correct waiting callers.

Summary

Functions

embed_batch(Server, Texts, Timeout)

Generate embeddings for texts.

embed_image_batch(Server, Images, Timeout)

Generate embeddings for images (base64 encoded).

embed_multi_batch(Server, Texts, Timeout)

Generate multi-vector embeddings.

embed_sparse_batch(Server, Texts, Timeout)

Generate sparse embeddings.

handle_call(_, From, State)

handle_cast(Msg, State)

handle_info(Info, State)

info(Server, Timeout)

Get model info.

init(_)

start_link(Python, Args, Opts)

Start the port server. Args are passed to python -m barrel_embed.

stop(Server)

Stop the server and close the port.

terminate(Reason, State)

Functions

embed_batch(Server, Texts, Timeout)

-spec embed_batch(pid(), [binary()], timeout()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for texts.

embed_image_batch(Server, Images, Timeout)

-spec embed_image_batch(pid(), [binary()], timeout()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for images (base64 encoded).

embed_multi_batch(Server, Texts, Timeout)

-spec embed_multi_batch(pid(), [binary()], timeout()) -> {ok, [[[float()]]]} | {error, term()}.

Generate multi-vector embeddings.

embed_sparse_batch(Server, Texts, Timeout)

-spec embed_sparse_batch(pid(), [binary()], timeout()) -> {ok, [map()]} | {error, term()}.

Generate sparse embeddings.

handle_call(_, From, State)

handle_cast(Msg, State)

handle_info(Info, State)

info(Server, Timeout)

-spec info(pid(), timeout()) -> {ok, map()} | {error, term()}.

Get model info.

init(_)

start_link(Python, Args, Opts)

-spec start_link(string(), [string()], proplists:proplist()) -> {ok, pid()} | {error, term()}.

Start the port server. Args are passed to python -m barrel_embed.

stop(Server)

-spec stop(pid()) -> ok.

Stop the server and close the port.

terminate(Reason, State)

barrel_embed_provider behaviour

Embedding provider behaviour definition

Implement this behaviour to add new embedding providers.

Example Implementation

   -module(my_custom_embedder).
   -behaviour(barrel_embed_provider).

   -export([embed/2, embed_batch/2, dimension/1, name/0]).

   embed(Text, Config) ->
       %% Your embedding logic here
       {ok, Vector}.

   embed_batch(Texts, Config) ->
       {ok, [embed(T, Config) || T <- Texts]}.

   dimension(_Config) -> 768.

   name() -> my_custom.

Summary

Callbacks

available/1

dimension/1

embed/2

embed_batch/2

init/1

name/0

Functions

call_embed(Module, Text, Config)

Call embed on a provider module. Wraps the call with error handling.

call_embed_batch(Module, Texts, Config)

Call embed_batch on a provider module. Wraps the call with error handling.

check_available(Module, Config)

Check if a provider is available. Returns true if the provider doesn't implement available/1.

Callbacks

available/1

(optional)

-callback available(Config :: map()) -> boolean().

dimension/1

-callback dimension(Config :: map()) -> pos_integer().

embed/2

-callback embed(Text :: binary(), Config :: map()) -> {ok, Vector :: [float()]} | {error, term()}.

embed_batch/2

-callback embed_batch(Texts :: [binary()], Config :: map()) ->
                         {ok, Vectors :: [[float()]]} | {error, term()}.

init/1

(optional)

-callback init(Config :: map()) -> {ok, NewConfig :: map()} | {error, term()}.

name/0

-callback name() -> atom().

Functions

call_embed(Module, Text, Config)

-spec call_embed(module(), binary(), map()) -> {ok, [float()]} | {error, term()}.

Call embed on a provider module. Wraps the call with error handling.

call_embed_batch(Module, Texts, Config)

-spec call_embed_batch(module(), [binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Call embed_batch on a provider module. Wraps the call with error handling.

check_available(Module, Config)

-spec check_available(module(), map()) -> boolean().

Check if a provider is available. Returns true if the provider doesn't implement available/1.

barrel_embed_splade

SPLADE sparse embedding provider

Uses SPLADE (Sparse Lexical and Expansion) models for neural sparse embeddings. Produces sparse vectors suitable for inverted index search.

Dependencies (transformers, torch) are installed automatically in the managed venv on first use.

Configuration

   Config = #{
       model => "prithivida/Splade_PP_en_v1",   %% Model name (default)
       python => "python3",                     %% Python executable (default)
       timeout => 120000                        %% Timeout in ms (default)
   }.

Sparse Vector Format

Unlike dense embeddings, SPLADE produces sparse vectors:

   #{indices => [1, 5, 10], values => [0.5, 0.3, 0.8]}

Where indices are vocabulary token IDs and values are weights.

Supported Models

- "prithivida/Splade_PP_en_v1" - Default, SPLADE++ English - "naver/splade-cocondenser-ensembledistil" - NAVER's SPLADE

Use Cases

- Lexical-semantic hybrid search - Term expansion (captures synonyms and related terms) - Efficient inverted index storage

Summary

Types

sparse_vector/0

Functions

available(Config)

Check if provider is available.

dimension(Config)

Get dimension (vocab size) for this provider. For sparse vectors, dimension is the vocabulary size.

embed(Text, Config)

Generate dense embedding (converts sparse to dense). Note: This is inefficient for large vocab sizes. Use embed_sparse/2 instead.

embed_batch(Texts, Config)

Generate dense embeddings for batch (converts sparse to dense).

embed_batch_sparse(Texts, Config)

Generate sparse embeddings for multiple texts.

embed_sparse(Text, Config)

Generate sparse embedding for a single text.

init(Config)

Initialize the provider.

name()

Provider name.

Types

sparse_vector/0

-type sparse_vector() :: #{indices := [non_neg_integer()], values := [float()]}.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if provider is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension (vocab size) for this provider. For sparse vectors, dimension is the vocabulary size.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate dense embedding (converts sparse to dense). Note: This is inefficient for large vocab sizes. Use embed_sparse/2 instead.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate dense embeddings for batch (converts sparse to dense).

embed_batch_sparse(Texts, Config)

-spec embed_batch_sparse([binary()], map()) -> {ok, [sparse_vector()]} | {error, term()}.

Generate sparse embeddings for multiple texts.

embed_sparse(Text, Config)

-spec embed_sparse(binary(), map()) -> {ok, sparse_vector()} | {error, term()}.

Generate sparse embedding for a single text.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_sup

barrel_embed top level supervisor

Summary

Functions

init(_)

start_link()

Functions

init(_)

start_link()

barrel_embed_venv

Managed virtualenv for barrel_embed

Automatically creates and manages a Python virtualenv with dependencies required by embedding providers.

Venv Location

Default: priv/barrel_embed/.venv Configurable via app env: {barrel_embed, [{venv_dir, "/custom/path"}]}

Provider Dependencies

- fastembed: fastembed (~100MB) - local: sentence-transformers (~2GB) - splade: transformers, torch - colbert: transformers, torch - clip: transformers, torch, pillow

Summary

Functions

create_venv()

Create the managed venv. Finds Python3 and creates venv at configured path.

ensure_venv()

Ensure venv exists and is valid. Creates if missing, returns path on success.

has_uvloop()

Check if uvloop is installed in the managed venv.

install_deps(Provider)

Install dependencies for a provider. Automatically installs required packages for the given provider.

is_valid()

Check if managed venv is valid.

refresh()

Refresh the managed venv. Deletes and recreates the venv.

venv_path()

Get the managed venv path. Returns custom path from app env or default priv/.venv

Functions

create_venv()

-spec create_venv() -> {ok, string()} | {error, term()}.

Create the managed venv. Finds Python3 and creates venv at configured path.

ensure_venv()

-spec ensure_venv() -> {ok, string()} | {error, term()}.

Ensure venv exists and is valid. Creates if missing, returns path on success.

has_uvloop()

-spec has_uvloop() -> boolean().

Check if uvloop is installed in the managed venv.

install_deps(Provider)

-spec install_deps(atom()) -> ok | {error, term()}.

Install dependencies for a provider. Automatically installs required packages for the given provider.

is_valid()

-spec is_valid() -> boolean().

Check if managed venv is valid.

refresh()

-spec refresh() -> {ok, string()} | {error, term()}.

Refresh the managed venv. Deletes and recreates the venv.

venv_path()

-spec venv_path() -> string().

Get the managed venv path. Returns custom path from app env or default priv/.venv

barrel_embed_vertex

Google Vertex AI embedding provider

Uses Google Vertex AI's embedding API for embedding generation. Provides access to Google's embedding models including Gecko and newer models.

Requirements

- A Google Cloud project with Vertex AI API enabled - Authentication via access token or API key

Option 1: Access Token (Recommended for development)

Get a token with: gcloud auth print-access-token - Set GOOGLE_ACCESS_TOKEN environment variable - Or pass access_token => <<"ya29...">>> in config

Option 2: API Key

Create an API key in Google Cloud Console - Set GOOGLE_API_KEY environment variable - Or pass api_key => <<"...">>> in config

Configuration

   Config = #{
       access_token => <<"ya29...">>,                  %% Access token (or use env var)
       project => <<"my-project">>,                    %% GCP project ID (required)
       region => <<"us-central1">>,                    %% GCP region (default)
       model => <<"text-embedding-004">>,              %% Model name (default, 768 dims)
       timeout => 30000,                               %% Timeout in ms (default)
       dimension => 768                                %% Vector dimension (default)
   }.

Supported Models

- <<"text-embedding-004">> - Default, 768 dims, latest - <<"text-embedding-005">> - 768 dims, newest - <<"textembedding-gecko@001">> - 768 dims, legacy - <<"textembedding-gecko@003">> - 768 dims - <<"textembedding-gecko-multilingual@001">> - 768 dims, multilingual - <<"text-multilingual-embedding-002">> - 768 dims, multilingual

Summary

Functions

available(Config)

Check if Vertex AI API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Vertex AI supports batch embedding.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Vertex AI API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Vertex AI supports batch embedding.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.

barrel_embed_voyage

Voyage AI embedding provider

Uses Voyage AI's Embeddings API for embedding generation. Voyage AI provides state-of-the-art embeddings optimized for retrieval.

Requirements

A Voyage AI API key, either: - Set via VOYAGE_API_KEY environment variable - Passed in config as api_key => <<"...">>>

Configuration

   Config = #{
       api_key => <<"...">>,                           %% API key (or use env var)
       url => <<"https://api.voyageai.com/v1">>,       %% API base URL (default)
       model => <<"voyage-3">>,                        %% Model name (default, 1024 dims)
       timeout => 30000,                               %% Timeout in ms (default)
       dimension => 1024                               %% Vector dimension (default)
   }.

Supported Models

- <<"voyage-3">> - Default, 1024 dims, best quality - <<"voyage-3-lite">> - 512 dims, faster/cheaper - <<"voyage-code-3">> - 1024 dims, optimized for code - <<"voyage-finance-2">> - 1024 dims, financial domain - <<"voyage-law-2">> - 1024 dims, legal domain - <<"voyage-large-2">> - 1536 dims, legacy high quality

Summary

Functions

available(Config)

Check if Voyage AI API is available.

dimension(Config)

Get dimension for this provider.

embed(Text, Config)

Generate embedding for a single text.

embed_batch(Texts, Config)

Generate embeddings for multiple texts. Voyage AI supports native batch embedding.

init(Config)

Initialize the provider.

name()

Provider name.

Functions

available(Config)

-spec available(map()) -> boolean().

Check if Voyage AI API is available.

dimension(Config)

-spec dimension(map()) -> pos_integer().

Get dimension for this provider.

embed(Text, Config)

-spec embed(binary(), map()) -> {ok, [float()]} | {error, term()}.

Generate embedding for a single text.

embed_batch(Texts, Config)

-spec embed_batch([binary()], map()) -> {ok, [[float()]]} | {error, term()}.

Generate embeddings for multiple texts. Voyage AI supports native batch embedding.

init(Config)

-spec init(map()) -> {ok, map()} | {error, term()}.

Initialize the provider.

name()

-spec name() -> atom().

Provider name.