knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE)
This vignette builds a small retrieval-augmented generation (RAG) corpus end to end: convert documents, chunk them with awareness of your embedding model's tokenizer, attach embeddings, and run a similarity search.
Chunk size should respect the context window of the embedding model you will
use. docling_chunk() is token-aware: point it at the same tokenizer as your
embedder so the max_tokens budget is measured in the right units.
library(doclingr) doc <- docling_convert("paper.pdf") chunks <- docling_chunk( doc, tokenizer = "BAAI/bge-small-en-v1.5", max_tokens = 512 ) chunks
Each chunk's text is contextualized -- prefixed with its heading path and
table context -- which is the form you should embed. The unmodified passage is
kept in raw_text, and headings/pages let you cite sources later.
doclingr does not lock you into an embedding provider. You supply a function that
maps a character vector to vectors; docling_embed() handles batching and tidy
assembly. Here is a sketch against an HTTP embeddings API:
embed_api <- function(texts) { # POST `texts` to your endpoint and return a matrix: one row per text. # e.g. with httr2: resp <- httr2::request("https://api.example.com/v1/embeddings") |> httr2::req_headers(Authorization = paste("Bearer", Sys.getenv("EMBED_KEY"))) |> httr2::req_body_json(list(model = "bge-small", input = texts)) |> httr2::req_perform() do.call(rbind, lapply(httr2::resp_body_json(resp)$data, \(d) unlist(d$embedding))) } corpus <- docling_embed(chunks, embed_api, batch_size = 64) corpus
A local model works just as well -- anything that returns a matrix or a list of numeric vectors:
# Using a sentence-transformers model through reticulate st <- reticulate::import("sentence_transformers") model <- st$SentenceTransformer("BAAI/bge-small-en-v1.5") embed_local <- function(texts) model$encode(texts) corpus <- docling_embed(chunks, embed_local, batch_size = 64)
With embeddings in a matrix, retrieval is plain R. Embed the query the same way, then rank chunks by cosine similarity:
emb <- do.call(rbind, corpus$embedding) cosine_top <- function(query, k = 5) { q <- as.numeric(embed_api(query)) sims <- as.numeric(emb %*% q) / (sqrt(rowSums(emb^2)) * sqrt(sum(q^2))) corpus[order(sims, decreasing = TRUE)[seq_len(k)], c("text", "headings", "pages")] } cosine_top("What datasets were used for evaluation?")
For larger corpora, push the embeddings into a dedicated vector store instead of an in-memory matrix.
Converting and embedding are the expensive steps. Save the corpus so you only
pay once. docling_write_chunks() stores every column — including the
embedding and doc_items list-columns — in Parquet or Feather:
docling_write_chunks(corpus, "corpus.parquet") # later (or from Python/DuckDB -- it is a plain Parquet file): corpus <- arrow::read_parquet("corpus.parquet")
docling_convert() accepts a vector of sources and converts them in one batch.
Combine that with the chunk/embed steps to build a corpus over a folder:
files <- list.files("docs", pattern = "[.](pdf|docx|html)$", full.names = TRUE) docs <- docling_convert(files) corpus <- purrr::imap(docs, \(d, src) { docling_chunk(d, tokenizer = "BAAI/bge-small-en-v1.5", max_tokens = 512) |> docling_embed(embed_api, batch_size = 64) |> dplyr::mutate(source = src) }) |> purrr::list_rbind()
That corpus -- chunk text, headings, pages, source and embeddings in one tidy
table -- is everything you need to power retrieval for an LLM.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.