RAG in Scala
To answer questions over your own documents in Scala, use llm4s-rag: one RAG pipeline chunks and embeds what you
ingest, stores the vectors, retrieves the passages that match a question and has a model answer from them, with the
passages it used. The vectors live in memory, in SQLite or in PostgreSQL with pgvector, and the quality can be
measured with RAGAS metrics.
Add the module
1
2
3
4
libraryDependencies ++= Seq(
"org.llm4s" %% "llm4s-rag" % "0.5.1",
"org.llm4s" %% "llm4s-openai" % "0.5.1"
)
The provider module supplies the embedding model as well as the chat model: OpenAI here; Ollama, Voyage AI, Jina AI
and Cohere embeddings have modules of their own (embeddings). The embedding model is chosen in
configuration, for example EMBEDDING_MODEL=openai/text-embedding-3-small.
Build a pipeline
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.llm4s.config.Llm4sConfig
import org.llm4s.llmconnect.LLMClient
import org.llm4s.model.ModelRegistryService
import org.llm4s.rag.RAG
import org.llm4s.rag.RAG.RAGConfigOps
import org.llm4s.types.Result
def docsPipeline(client: LLMClient): Result[RAG] = for {
registryService <- Llm4sConfig.modelRegistryService()
given ModelRegistryService = registryService
selected <- Llm4sConfig.embeddings()
(provider, embeddingConfig) = selected
rag <- RAG
.builder()
.withEmbeddings(provider)
.withPgVector()
.withLLM(client)
.build(_ => Right(embeddingConfig))
} yield rag
withPgVector() keeps the vectors in PostgreSQL with the pgvector extension, on localhost:5432 (its overloads take
a connection string, user, password and table). Leave it out and the pipeline keeps everything in memory, which suits
a test or a demo; withSQLite(path) keeps it in a file. client is the chat client that writes the answers, built as
on the first-call page.
Ingest and ask
1
2
3
4
5
6
7
8
import org.llm4s.rag.{ RAG, RAGAnswerResult }
import org.llm4s.types.Result
def askTheDocs(rag: RAG, question: String): Result[RAGAnswerResult] = for {
_ <- rag.ingestText("Opaque types hide their representation outside the defining scope.", "opaque-types")
_ <- rag.ingestText("Given instances supply the values of context parameters.", "givens")
answer <- rag.queryWithAnswer(question)
} yield answer
queryWithAnswer returns the answer and the passages it was given (contexts), so you can show sources. Instead
of text, rag.ingest(path) reads a file or a whole directory, and rag.query(question) returns the passages without
asking the model. Retrieval is hybrid: vector similarity and keyword (BM25) search, fused, with optional reranking by
Cohere or by a model (vector store guide).
Other stores
Besides the pipeline’s own storage, the VectorStore API has backends for Qdrant, and, in separate modules,
Elasticsearch, OpenSearch, Redis and Valkey. They are covered in the vector store guide.
Evaluate
A RAG pipeline needs measuring, not eyeballing. LLM4S computes the RAGAS metrics (faithfulness, answer relevancy, context precision and context recall) and has a benchmark harness that compares chunking strategies, fusion strategies and embedding providers on a dataset: see RAG evaluation.
Going further
- Modular RAG example: a pipeline assembled step by step.
- Query transformers: rewrite a question before retrieval.
- Permission-based RAG: retrieve only what the asking user may see.
- Cookbook: answer questions over a folder of files and document Q&A, both runnable with no API key.
- From Java:
llm4s-java-rag.