--- title: "Working with Embeddings" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Working with Embeddings} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} # This vignette runs against recorded, credential-free API fixtures. When the # fixtures are present (they are recorded once with data-raw/record-doc-outputs.R # and committed under vignettes/embeddings/), every foundry_*() call below is # executed and its real output is shown. When they are absent, the API chunks are # not evaluated so the vignette still builds anywhere without Azure credentials. fixture_dir <- "embeddings" recording <- nzchar(Sys.getenv("FOUNDRY_RECORD_DOCS")) have_fixtures <- dir.exists(fixture_dir) && length(list.files(fixture_dir)) > 0 run_api <- requireNamespace("httptest2", quietly = TRUE) && (recording || have_fixtures) # Attach foundryR before start_vignette(): httptest2 only sources the package's # inst/httptest2/start-vignette.R (which sets replay placeholders) from attached # packages. library(foundryR) if (run_api) { httptest2::start_vignette(fixture_dir) } knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = run_api ) ``` ## What embeddings are for Embeddings are numerical representations of text that capture semantic meaning. When you convert text to an embedding, you get a vector of numbers (often 1,536 or 3,072 dimensions depending on the model). Texts with similar meanings tend to have similar vectors. Use embeddings when you need to: - Find documents related to a query by meaning, not only keywords. - Measure how similar two pieces of text are. - Cluster open-ended responses into themes. - Find near-duplicate responses or records. - Feed text-derived numeric predictors into downstream models. Unlike keyword matching, embeddings understand that "automobile" and "car" are semantically similar, even though they share no letters. ## Generating embeddings with foundry_embed() The examples below embed real sentences from Jane Austen's *Pride and Prejudice*, available in the `janeaustenr` package. Using a well-known public-domain text makes the output easy to reason about: the opening lines share vocabulary and sentiment, so their embeddings should sit close together. ```{r austen-lines} library(foundryR) austen_lines <- c( "It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.", "However little known the feelings or views of such a man may be on his first entering a neighbourhood.", "Mr. Bennet was so odd a mixture of quick parts, sarcastic humour, reserve, and caprice." ) embedding <- foundry_embed(austen_lines[1], model = "text-embedding-3-small") embedding ``` The result is a tibble with: - `text`: the original input text. - `embedding`: a list-column holding the numeric vector. - `n_dims`: the dimensionality of the embedding. ### Embedding multiple texts Pass a character vector to embed several texts in one call: ```{r multiple-embed} doc_embeddings <- foundry_embed(austen_lines, model = "text-embedding-3-small") doc_embeddings ``` ### Controlling dimensions The `text-embedding-3-small` and `text-embedding-3-large` models can return shorter vectors. Smaller dimensions mean faster similarity computations and less storage, with some trade-off in precision: ```{r reduced-dims} compact <- foundry_embed( austen_lines[1], model = "text-embedding-3-small", dimensions = 256 ) compact$n_dims ``` ## Computing similarity with foundry_similarity() Cosine similarity measures how close two embeddings are, from -1 (opposite) to 1 (identical). `foundry_similarity()` computes every pairwise similarity in a tibble of embeddings. Here we contrast the Austen lines with two sentences from a different domain so the split is visible. ```{r similarity} mixed <- c( "It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.", "Mr. Bennet was so odd a mixture of quick parts, sarcastic humour, reserve, and caprice.", "The quarterly revenue report showed a sharp rise in cloud subscriptions.", "Analysts raised their earnings forecast after the strong cloud numbers." ) similarities <- foundry_embed(mixed, model = "text-embedding-3-small") |> foundry_similarity() similarities ``` Results are sorted by similarity. The two Austen lines pair together and the two finance lines pair together, while cross-domain pairs score lower. The plots in this vignette are shown when the suggested `ggplot2` package is installed. ```{r similarity-heatmap, echo = FALSE, eval = run_api && requireNamespace("ggplot2", quietly = TRUE), fig.alt = "Heatmap of cosine similarity across four sentences from two domains."} label_for <- function(x) { dplyr::case_when( startsWith(x, "It is a truth") ~ "Austen 1", startsWith(x, "Mr. Bennet") ~ "Austen 2", startsWith(x, "The quarterly") ~ "Finance 1", TRUE ~ "Finance 2" ) } heatmap_data <- similarities |> dplyr::mutate( a = label_for(text_1), b = label_for(text_2) ) ggplot2::ggplot(heatmap_data, ggplot2::aes(x = b, y = a, fill = similarity)) + ggplot2::geom_tile(color = "white", linewidth = 0.6) + ggplot2::geom_text(ggplot2::aes(label = sprintf("%.2f", similarity)), size = 3.2) + ggplot2::scale_fill_gradient(low = "#E6F2FB", high = "#0078D4") + ggplot2::labs( title = "Cosine similarity separates the two domains", x = NULL, y = NULL, fill = "Similarity" ) + ggplot2::theme_minimal(base_size = 12) + ggplot2::theme(panel.grid = ggplot2::element_blank(), legend.position = "bottom") ``` ## Use case: finding similar documents A common application is ranking documents by relevance to a query. Embed the documents and the query, then sort by cosine similarity: ```{r semantic-search} library(dplyr) documents <- c( "How to install R packages using install.packages()", "Data visualization with ggplot2 in R", "Introduction to machine learning with Python", "Statistical hypothesis testing explained", "Building web applications with Shiny", "Deep learning with TensorFlow and Keras" ) doc_embeddings <- foundry_embed(documents, model = "text-embedding-3-small") query_embedding <- foundry_embed( "How do I create charts and graphs in R?", model = "text-embedding-3-small" ) cosine <- function(a, b) sum(a * b) / (sqrt(sum(a^2)) * sqrt(sum(b^2))) query_vec <- query_embedding$embedding[[1]] doc_embeddings |> mutate(similarity = vapply(embedding, cosine, numeric(1), b = query_vec)) |> arrange(desc(similarity)) |> select(text, similarity) |> head(3) ``` ## Use case: clustering text Embeddings work well as features for clustering. Here `stats::kmeans()` groups a mix of programming, food, and sports sentences without any labels: ```{r clustering} texts <- c( "Python is great for machine learning", "R excels at statistical analysis", "JavaScript powers modern web applications", "Italian pasta with tomato sauce", "Sushi is a popular Japanese dish", "French croissants are flaky and buttery", "Soccer is the world's most popular sport", "Basketball requires speed and agility", "Tennis matches can last for hours" ) cluster_embeddings <- foundry_embed(texts, model = "text-embedding-3-small") embedding_matrix <- do.call(rbind, cluster_embeddings$embedding) set.seed(42) clusters <- kmeans(embedding_matrix, centers = 3, nstart = 10) cluster_embeddings |> mutate(cluster = clusters$cluster) |> arrange(cluster) |> select(text, cluster) ``` ```{r projection, echo = FALSE, eval = run_api && requireNamespace("ggplot2", quietly = TRUE), fig.alt = "Two-dimensional PCA projection of sentence embeddings, colored by k-means cluster."} pca <- prcomp(embedding_matrix, rank. = 2) projection <- tibble::tibble( pc1 = pca$x[, 1], pc2 = pca$x[, 2], cluster = factor(clusters$cluster), label = substr(texts, 1, 18) ) ggplot2::ggplot(projection, ggplot2::aes(pc1, pc2, color = cluster, label = label)) + ggplot2::geom_point(size = 3.2, alpha = 0.9) + ggplot2::geom_text(nudge_y = 0.15, size = 3, show.legend = FALSE) + ggplot2::scale_color_manual(values = c("#0078D4", "#107C10", "#5C2D91")) + ggplot2::labs( title = "A PCA projection makes the clusters visible", x = "PC 1", y = "PC 2", color = "Cluster" ) + ggplot2::theme_minimal(base_size = 12) + ggplot2::theme(legend.position = "bottom", panel.grid.minor = ggplot2::element_blank()) ``` The clusters recover the three topics from the raw text alone. ## Tips for working with embeddings ### Choosing a model | Model | Dimensions | Notes | |-------|-----------|-------| | text-embedding-ada-002 | 1,536 | Previous generation, widely used | | text-embedding-3-small | 1,536 (configurable) | Newer, supports dimension reduction | | text-embedding-3-large | 3,072 (configurable) | Highest quality, more expensive | For most use cases, `text-embedding-3-small` balances quality and cost. ### Dimension trade-offs Higher dimensions capture more nuance but need more storage, take longer to compare, and may not improve simple tasks. Consider reduced dimensions (256-512) for large-scale applications where speed matters more than precision. ### Handling large collections 1. **Batch processing**: embed documents in batches to respect rate limits. 2. **Caching**: store embeddings in a database rather than regenerating them. 3. **Approximate nearest neighbors**: use libraries like `RcppAnnoy` for fast similarity search on large datasets. ```{r batch-example, eval = TRUE} # Defining this helper is local; calling it requires Azure credentials. batch_embed <- function(texts, model, batch_size = 100) { n_batches <- ceiling(length(texts) / batch_size) results <- vector("list", n_batches) for (i in seq_len(n_batches)) { start_idx <- (i - 1) * batch_size + 1 end_idx <- min(i * batch_size, length(texts)) results[[i]] <- foundry_embed(texts[start_idx:end_idx], model = model) Sys.sleep(0.5) } dplyr::bind_rows(results) } ``` ```{r cleanup, include = FALSE} if (run_api) { httptest2::end_vignette() } ```