Emperor Palpatine's closest match in my Star Wars embedding experiment was Mace Windu, the Jedi who confronted him. Jedi and Sith shared a neighborhood, while Boba Fett and Din Djarin formed the strongest pair in the entire set, at 0.940 similarity. The galaxy's moral divide wasn't the map's organizing principle.

I embedded fifteen short character descriptions to see which relationships a model would recover. Rank and shared story arcs stood out more clearly than light and dark: five characters described as "Master" clustered together, regardless of allegiance. That makes this a useful little experiment in reading embeddings. A convincing map can answer a different question from the one you thought you asked.

Building character descriptions

The first step is writing text descriptions that capture each character's essential traits. I'm not going for exhaustive Wookieepedia entries. I want concise summaries that encode what we care about: faction, role, Force sensitivity, era, and key relationships.

characters = { "Luke Skywalker": "Jedi Knight, son of Anakin Skywalker, strong in the Force, " "hero of the Rebellion, trained by Obi-Wan and Yoda, redeemed his father", "Darth Vader": "Sith Lord, formerly Anakin Skywalker, fallen Jedi, serves the Emperor, " "father of Luke and Leia, mechanical suit, immense Force power", "Han Solo": "Smuggler, captain of the Millennium Falcon, no Force sensitivity, " "pragmatic, allied with the Rebellion, close friend of Chewbacca", "Leia Organa": "Princess of Alderaan, leader of the Rebellion, Force sensitive, " "twin sister of Luke, diplomat and military strategist", "Emperor Palpatine": "Sith Master, ruler of the Galactic Empire, manipulator, " "immense dark side power, orchestrated the Clone Wars", "Yoda": "Grand Master of the Jedi Order, 900 years old, immense Force wisdom, " "trained generations of Jedi, exiled on Dagobah", "Obi-Wan Kenobi": "Jedi Master, trained Anakin Skywalker, guardian of Luke, " "wise and disciplined, killed by Darth Vader on the Death Star", "Boba Fett": "Bounty hunter, clone of Jango Fett, no Force sensitivity, " "works for the highest bidder, Mandalorian armor", "Ahsoka Tano": "Former Jedi Padawan of Anakin Skywalker, left the Jedi Order, " "Force sensitive, independent warrior, fought in the Clone Wars", "Din Djarin": "Mandalorian bounty hunter, no Force sensitivity, foundling, " "protector of Grogu, follows the Mandalorian creed", "Kylo Ren": "Dark side Force user, son of Han Solo and Leia Organa, " "formerly Ben Solo, trained by Luke then Snoke, conflicted", "Rey": "Jedi, granddaughter of Palpatine, strong in the Force, scavenger from Jakku, " "trained by Luke and Leia, chose the Skywalker name", "Mace Windu": "Jedi Master, member of the Jedi Council, invented Vaapad lightsaber form, " "strong in the Force, confronted Palpatine", "Count Dooku": "Sith apprentice, formerly a Jedi Master, trained by Yoda, " "leader of the Separatists, elegant duelist", "Chewbacca": "Wookiee warrior, co-pilot of the Millennium Falcon, loyal companion " "of Han Solo, fought in the Clone Wars and Galactic Civil War", }

I wrote them deliberately to lean on relational and factional attributes. "Trained by Yoda" creates a link between two characters. "No Force sensitivity" gives the model another distinction to work with. Keep an eye on the word "Master", though. It appears in five descriptions whose characters end up close together. That is a clue about the wording, not yet proof that this one word caused the grouping.

Generating embeddings

A sentence embedding model converts each description into a dense vector, and cosine similarity between vectors stands in for semantic similarity. My first run used all-MiniLM-L6-v2, a small sentence embedding model. On these fifteen descriptions it mostly picked up the Skywalker family story: Darth Vader's nearest neighbor was Luke (0.69), Palpatine's closest match after Vader was Yoda, and the average Jedi to Sith score (0.559) was nearly the same as Jedi to Jedi (0.564). No light side, no dark side, just one big family.

So I compared nine open models on the same text (MiniLM, MPNet, three BGE sizes, two E5 sizes, GTE, and mxbai) and measured how well each one separated the groups the descriptions talk about. E5-large-v2 came out on top, with the best overall group separation and the tightest Jedi Council cluster. This is an exploratory comparison on fifteen descriptions, with model selection and interpretation using the same data. It does not establish which model will work best on unseen text.

The saved experiment uses the passage: prefix on every description. I retain it here so the code matches the measured scores. The E5 model card recommends query: for symmetric similarity and clustering tasks; switching prefixes would be a separate experiment, not a way to reproduce these numbers:

from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer('intfloat/e5-large-v2') names = list(characters.keys()) descriptions = ['passage: ' + d for d in characters.values()] embeddings = model.encode(descriptions, normalize_embeddings=True) # Vectors are unit length, so the dot product is the cosine similarity sim_matrix = embeddings @ embeddings.T

The E5 scores sit in a narrow band. Every pair of characters here lands between 0.80 and 0.94. Here, 0.87 selects roughly the top sixth of pairs. It has no universal meaning across models: compare rankings and score distributions within each model.

The matrix already tells a story, just a different one from what I predicted. Luke's nearest neighbors are Rey (0.878), Darth Vader, and Obi-Wan. Leia is 11th of 14 for him. Vader's nearest is Palpatine (0.874), with Dooku right behind. The strongest pair in the whole set is Boba Fett and Din Djarin at 0.940, which is not a surprise for two Mandalorian bounty hunters.

Building a similarity graph

A raw similarity matrix is hard to read. A graph makes the relationships visual and lets you actually explore them.

import networkx as nx G = nx.Graph() threshold = 0.87 # E5 scores are compressed; this keeps the top ~16% of pairs for i, name_i in enumerate(names): G.add_node(name_i) for j, name_j in enumerate(names): if i < j and sim_matrix[i][j] >= threshold: G.add_edge(name_i, name_j, weight=float(sim_matrix[i][j]))

The threshold controls density. Too low and everything connects to everything. Too high and the graph falls apart into islands. With a compressed model like E5 I pick it from the distribution instead of guessing: at 0.87, 17 of the 105 pairs survive, which is readable. At 0.45, the value I used with MiniLM, every single pair would connect.

Visualizing in 2D

For the spatial view, drop the embedding dimensions with UMAP:

import umap import matplotlib.pyplot as plt reducer = umap.UMAP(n_components=2, n_neighbors=5, min_dist=0.3, metric='cosine', random_state=42) coords_2d = reducer.fit_transform(embeddings) # Color by faction. These labels are mine; the model never sees them. faction_colors = { 'Jedi': '#2a78d6', 'Sith': '#e34948', 'Rebellion': '#1baf7a', 'Bounty hunter': '#eda100', 'Unaligned': '#5d6b7a', } factions = { 'Luke Skywalker': 'Jedi', 'Darth Vader': 'Sith', 'Han Solo': 'Rebellion', 'Leia Organa': 'Rebellion', 'Emperor Palpatine': 'Sith', 'Yoda': 'Jedi', 'Obi-Wan Kenobi': 'Jedi', 'Boba Fett': 'Bounty hunter', 'Ahsoka Tano': 'Unaligned', 'Din Djarin': 'Bounty hunter', 'Kylo Ren': 'Unaligned', 'Rey': 'Jedi', 'Mace Windu': 'Jedi', 'Count Dooku': 'Sith', 'Chewbacca': 'Rebellion', } fig, ax = plt.subplots(figsize=(12, 9)) for name, coord in zip(names, coords_2d): color = faction_colors[factions[name]] ax.scatter(coord[0], coord[1], c=color, s=120, edgecolors='white', zorder=2) ax.annotate(name, (coord[0], coord[1]), fontsize=8, ha='center', va='bottom') ax.set_title('Star Wars Characters in Embedding Space') plt.tight_layout() plt.savefig('star_wars_embeddings.png', dpi=150)

The colors follow the descriptions: Leia is a Force sensitive Rebellion leader, Ahsoka left the Order, and Kylo is "conflicted". Those are my labels, not categories the embedding model discovered. Coloring a plot by faction can make a division feel more convincing than the scores justify.

A 2D projection also throws away much of the geometry. PCA on these vectors keeps about 27% of the variance in two axes; that figure describes PCA, not UMAP. I use the picture to find things to check, and I check them in the full 1024 dimensions.

What the clusters reveal

Here is what holds up when you test it against the scores instead of eyeballing the plot.

The Force split is real but modest. Force users average 0.857 with each other, the four characters without the Force average 0.860 among themselves, and the two groups average 0.830 across. On every projection the characters without the Force end up on their own side of the map, with Leia and Chewbacca near them. It's the clearest separation in the data, and it's still only about three hundredths of cosine.

Jedi and Sith don't split cleanly. For these group averages, the Jedi set is Luke, Yoda, Obi-Wan, and Mace; the Sith set is Vader, Palpatine, and Dooku. Rey is colored as Jedi on the plot but wasn't included in this comparison. Jedi to Jedi averages 0.868, Sith to Sith 0.876, and Jedi to Sith 0.865. The biggest of those gaps is about a hundredth, less than half the typical spread between any two characters (0.023). Emperor Palpatine's nearest neighbor is Mace Windu (0.891), the Jedi whose description ends with "confronted Palpatine".

Rank offers a better description of one neighborhood. Yoda, Obi-Wan, Mace Windu, Palpatine, and Dooku form their own neighborhood on every map, and they are exactly the five characters whose descriptions contain the word "Master": Grand Master, Jedi Master, Sith Master, formerly a Jedi Master. Inside that band the Jedi Council is tight. Yoda, Obi-Wan, and Mace each have the other two in their top three, and Palpatine and Dooku are in each other's top two. Those pockets live inside a shared "old masters" neighborhood rather than on opposite sides of a light and dark divide.

The people who left the Jedi Order find each other. Ahsoka's nearest neighbor is Count Dooku (0.876), and he returns the favor with her in second place. One left to become a Sith and the other left to stay independent, but both descriptions carry the "former Jedi" phrasing, a plausible explanation for why they land close despite their different allegiances. Darth Vader, the "fallen Jedi", sits right next to them on the map.

The new generation is its own trio. Luke, Rey, and Kylo Ren sit together in every projection, tied by "trained by Luke" and "strong in the Force". Kylo's scores are flat across the board (his best match is Rey at 0.865), so there is no single standout match. Reading that as evidence of his inner conflict would ask too much of a similarity score.

The gray zone is real, and it includes Leia. Han's nearest neighbors are Din Djarin, Chewbacca, Leia, and Boba Fett, in that order. The bounty hunters are still closer to each other (0.940) than to anyone else, consistent with their shared occupation and Mandalorian identity. Leia, though, is the surprise: she's Force sensitive on paper but averages only 0.824 with the Jedi, and her closest match is Han. Her description is about ruling and fighting a war, and the model reads it that way.

I also tested what happened when I stripped the named relationships out of the descriptions ("son of Anakin", "trained by Yoda", "father of Luke and Leia") and embedded them again. The Sith pull together once the family ties are gone. Sith to Sith rises to 0.898 against 0.874 for Jedi to Sith, and Vader's link to Palpatine becomes the strongest in his row (0.910). Removing those phrases changes the measured grouping. The edit removes several relationships at once, so it supports a role for relationship wording without isolating any single name or phrase as the cause.

What this actually demonstrates

It's a toy example, but the pipeline is real. Embed text descriptions, compute similarities, visualize with UMAP, build a graph. That exact recipe applies to:

  • Product catalogs: embed product descriptions to find similar items and gaps in inventory
  • Research papers: map a field's literature to find clusters of related work and underexplored connections
  • Job postings: embed role descriptions to see how a company's hiring reflects its strategy
  • Customer support tickets: embed tickets to surface recurring issue categories without manual tagging

For a product catalog, that means checking whether a cluster reflects what an item does or merely the wording its vendor likes. Here, shared ranks and relationships cut across the expected Jedi/Sith boundary. Removing relationship phrases strengthened the Sith grouping, and choosing a different embedding model changed which similarities stood out.

Fifteen authored descriptions are enough to expose that sensitivity, not to settle how Star Wars characters should be classified. Use the map to choose a relationship worth investigating, then check the original descriptions and full-dimensional scores. Palpatine next to Mace Windu becomes useful once you can explain what put them there, and distinguish that explanation from what you still need to test.