Skip to main content
Get from zero to a working RAG application with this complete tutorial. You’ll ingest documents, search for relevant content, and ask questions powered by AI.

Prerequisites

  • Elixir 1.14+ and Phoenix 1.7+
  • PostgreSQL running with pgvector extension
  • OpenAI API key (or use local embeddings)
If you haven’t installed Arcana yet, follow the Installation Guide first.

Complete Working Example

This tutorial walks through building a simple RAG system for Elixir documentation. Copy and run each step in your IEx session.
1

Start your application

Open an IEx session with your Phoenix app:
iex -S mix
2

Ingest some documents

Add content to your vector store. We’ll ingest a few facts about Elixir:
# Ingest documents about Elixir
{:ok, doc1} = Arcana.ingest(
  "Elixir is a dynamic, functional programming language designed for building scalable and maintainable applications. It runs on the Erlang VM (BEAM), which is known for creating low-latency, distributed, and fault-tolerant systems.",
  repo: MyApp.Repo,
  metadata: %{"topic" => "basics", "source" => "elixir-lang.org"},
  collection: "elixir-docs"
)

{:ok, doc2} = Arcana.ingest(
  "Phoenix is a web framework written in Elixir that enables building rich, interactive web applications with server-side rendering, real-time features using LiveView, and excellent performance.",
  repo: MyApp.Repo,
  metadata: %{"topic" => "phoenix", "source" => "phoenixframework.org"},
  collection: "elixir-docs"
)

{:ok, doc3} = Arcana.ingest(
  "OTP (Open Telecom Platform) is a set of libraries and design principles for building concurrent, distributed systems in Erlang and Elixir. GenServer is the most commonly used OTP behavior for implementing server processes.",
  repo: MyApp.Repo,
  metadata: %{"topic" => "otp", "source" => "elixir-lang.org"},
  collection: "elixir-docs"
)
Behind the scenes, Arcana is chunking the text, generating embeddings, and storing them in PostgreSQL with pgvector.
3

Search for relevant content

Now search for content using semantic similarity:
# Semantic search - finds content by meaning
{:ok, results} = Arcana.search(
  "What is Phoenix used for?",
  repo: MyApp.Repo,
  limit: 3
)

# Inspect the results
Enum.each(results, fn chunk ->
  IO.puts("Score: #{chunk.similarity}")
  IO.puts("Text: #{chunk.text}\n")
end)
Expected output:
Score: 0.89
Text: Phoenix is a web framework written in Elixir that enables building rich, interactive web applications...

Score: 0.72
Text: Elixir is a dynamic, functional programming language designed for building scalable...
4

Ask a question

Use RAG to answer questions based on your documents:
# Configure your LLM (requires OPENAI_API_KEY environment variable)
{:ok, answer} = Arcana.ask(
  "What is Phoenix and what can I build with it?",
  repo: MyApp.Repo,
  llm: "openai:gpt-4o-mini"
)

IO.puts(answer)
Expected output:
Phoenix is a web framework written in Elixir that enables you to build rich, 
interactive web applications. You can create applications with server-side 
rendering, add real-time features using LiveView, and benefit from excellent 
performance characteristics.
The ask/2 function retrieves relevant chunks via semantic search, then sends them to the LLM as context for generating the answer.

Try Different Search Modes

Arcana supports multiple search strategies. Here’s how they compare:

Add More Advanced Features

Filter by Collection

Organize documents into collections for better routing:
# Search only in specific collection
{:ok, results} = Arcana.search(
  "web framework",
  repo: MyApp.Repo,
  collection: "elixir-docs"
)

Set Similarity Threshold

Filter out low-quality matches:
# Only return results with similarity > 0.7
{:ok, results} = Arcana.search(
  "distributed systems",
  repo: MyApp.Repo,
  threshold: 0.7,
  limit: 5
)

Ingest Files

Load content from text, markdown, or PDF files:
# Supports .txt, .md, .pdf
{:ok, document} = Arcana.ingest_file(
  "path/to/elixir-guide.pdf",
  repo: MyApp.Repo,
  collection: "elixir-docs",
  metadata: %{"type" => "guide"}
)

Complete End-to-End Example

Here’s a complete working script you can save and run:
complete_example.exs
# Start with a clean slate (optional - deletes existing documents)
# Arcana.Document |> MyApp.Repo.delete_all()

# Ingest a small knowledge base
documents = [
  {"Elixir is a functional language that runs on the BEAM VM, known for concurrency and fault tolerance.", "elixir-basics"},
  {"Phoenix Framework enables building real-time web applications with LiveView and channels.", "phoenix-intro"},
  {"GenServer is an OTP behavior for implementing server processes that maintain state.", "otp-genserver"},
  {"Ecto is the database wrapper for Elixir, providing schemas, queries, and migrations.", "ecto-intro"}
]

Enum.each(documents, fn {text, source} ->
  {:ok, _doc} = Arcana.ingest(
    text,
    repo: MyApp.Repo,
    collection: "elixir-docs",
    metadata: %{"source" => source}
  )
  IO.puts("✓ Ingested: #{source}")
end)

# Search for relevant content
IO.puts("\n=== Semantic Search Results ===")
{:ok, search_results} = Arcana.search(
  "How do I build real-time web apps?",
  repo: MyApp.Repo,
  limit: 2
)

Enum.each(search_results, fn chunk ->
  IO.puts("Score: #{Float.round(chunk.similarity, 2)} | #{String.slice(chunk.text, 0..80)}...")
end)

# Ask a question with RAG
IO.puts("\n=== RAG Answer ===")
{:ok, answer} = Arcana.ask(
  "What is Phoenix Framework and what are its main features?",
  repo: MyApp.Repo,
  llm: "openai:gpt-4o-mini"
)

IO.puts(answer)
Run it with:
mix run complete_example.exs

Using Local Embeddings

Don’t want to use OpenAI? Use local embeddings instead:
1

Configure local embeddings

config/config.exs
# Use local Bumblebee embeddings (no API keys needed)
config :arcana, embedder: :local

# Configure Nx backend
config :nx,
  default_backend: EXLA.Backend,
  default_defn_options: [compiler: EXLA]
2

Add to supervision tree

lib/my_app/application.ex
def start(_type, _args) do
  children = [
    MyApp.Repo,
    Arcana.Embedder.Local  # Start local embedder
  ]
  
  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end
3

Use local LLM (optional)

For a completely local setup, use a local LLM:
# With Ollama running locally
local_llm = fn prompt ->
  case Req.post("http://localhost:11434/api/generate",
    json: %{model: "llama2", prompt: prompt}) do
    {:ok, %{body: %{"response" => response}}} ->
      {:ok, response}
    error ->
      error
  end
end

{:ok, answer} = Arcana.ask(
  "What is Elixir?",
  repo: MyApp.Repo,
  llm: local_llm
)

Next Steps

Agentic RAG

Build multi-step RAG pipelines with query expansion and re-ranking

GraphRAG

Extract entities and relationships for knowledge graph search

Search Algorithms

Deep dive into semantic, full-text, and hybrid search

Dashboard

Use the LiveView UI to manage documents and test queries

Common Issues

Check that documents were ingested successfully:
# Count documents
MyApp.Repo.aggregate(Arcana.Document, :count)

# Count chunks
MyApp.Repo.aggregate(Arcana.Chunk, :count)
Lower the similarity threshold:
{:ok, results} = Arcana.search(
  "your query",
  repo: MyApp.Repo,
  threshold: 0.5  # Lower threshold
)
Make sure your API key is set:
export OPENAI_API_KEY="sk-..."
Verify it’s accessible:
System.get_env("OPENAI_API_KEY")
Or pass the LLM configuration explicitly:
{:ok, answer} = Arcana.ask(
  "question",
  repo: MyApp.Repo,
  llm: "openai:gpt-4o-mini"
)
Local embeddings download models on first use (~133MB for default model). This is a one-time download:
  • Default (BGE Small): 133MB
  • BGE Base: 438MB
  • BGE Large: 1.3GB
Subsequent runs use cached models.For faster startup, use OpenAI embeddings instead:
config :arcana, embedder: :openai

Build docs developers (and LLMs) love