← Back to list

How Vector Databases Measure ‘Closeness’: Dot Product, Cosine, and Euclidean

In my previous two posts I covered ‘Vector Databases Explained for Java Developers’ and ‘How do we “see” a vector with 384 dimensions’…

Mala Gupta · 2026-07-08 15:52 · 1 claps · 13.4 min read
#vector-database #vector-search #ai-agent #ai #java
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval AGT · AI Agents AI · AI · General

How Vector Databases Measure ‘Closeness’: Dot Product, Cosine, and Euclidean

In my previous two posts I covered ‘Vector Databases Explained for Java Developers’ and ‘How do we “see” a vector with 384 dimensions’. This post is about the next step, that is, how a vector database decides which stored vectors are closest to your search.

A vector database’s whole job is to answer “which stored vectors are closest to vector X”, where X could be your search word, string, or even paragraph(s). That search text is first converted into a vector before it can be compared against what’s stored in a vector database.

The next obvious question is “What do you mean by the closest vector values?” “Closest” depends on which of three metrics the vector database uses: Dot product, Euclidean distance, or Cosine similarity. I’ll define all three, watch them disagree on a tiny hand-worked example, then run the same search through Qdrant with a real embedding model.

Why do you need to understand Dot product, Euclidean and Cosine

Imagine you own an e-commerce website. It stores the descriptions of two backpacks from different companies:

  1. Bag 1 Description: “This is an amazing backpack with multiple pockets for your needs. It works for school or your office.”
  2. Bag 2 Description: “A multipurpose bag that you can pack for all your needs. It has 5 pockets. It can be used at the office or while travelling. And if you’re carrying your lunch with a new recipe, don’t worry, it has a non-spilling compartment made especially for containers that could leak when you’re trying a new recipe. You can use it as a backpack with the attachments that come with it: put it on and travel with it like a backpack.”

Now a user searches for “I want to buy a backpack today” on your website. Which item should your website show first? The first one that is short and precise, but mentioning “backpack” just once? Or the second, which includes the word “backpack” more than once, but isn’t primarily a backpack?

As a human, I’d say the first matches better. But a vector database doesn’t read the words. It turns each description into a vector and scores how close the query is, using one of three metrics. Which metric it uses can change the answer. Here’s what each one measures, and how it would lean with our two backpacks.

What do Dot product, Euclidean and Cosine mean?

Dot product multiplies the vectors component by component and sums the result:

A·B = a₁b₁ + a₂b₂ + a₃b₃. 

The same number can also be written as:

A·B = |A|·|B|·cos(θ)

where |A| and |B| are the vectors’ lengths and cos(θ) just measures whether they point the same way. To understand cos(θ) picture two arrows from the same point: 1 is the same direction, 0 a right angle, -1 opposite. You never work the angle out yourself. The cosine() method does that for you as follows:

cos(θ) = A·B / (|A|·|B|). 

So dot product grows in two ways: when the vectors line up, and when they’re simply long. That length part is why it has no upper limit, while cosine, which keeps only the direction, always stays between -1 and 1. In our example, the long second description is both longer and repeats “backpack” more often, the two things dot product rewards, so it’s the metric most likely to be pulled toward that rambling listing.

Euclidean distance

It treats each vector as a point and measures the straight line between them:

√((a₁-b₁)² + (a₂-b₂)² + (a₃-b₃)²). 

Here smaller means closer, and because it works on raw coordinates it’s sensitive to length. In our example, the long description is padded with extra words about recipes and travel, which drags its point away from a short “backpack” query, So, Euclidean sees it as the more distant of the two.

Cosine similarity

It divides the dot product by both lengths:

(A·B) / (|A|·|B|)

It leaves only the angle between the vectors. It ranges from -1 (opposite) through 0 (unrelated) to 1 (same direction), and it ignores length entirely and this is it’s the usual default for text. In our example, the short description points almost straight at “backpack”, so cosine would rate it a near-perfect match however brief it is (similar to a human-friendly answer).

Don’t worry, the code to work with all of these is shown in the next section.

Hand coded values in action

Here’s the code that I used to hand-code values for the search query and both the long and short description. I added comments in the code so that it is self explanatory (executable class that you can copy paste and run). It has NO Frameworks, NO Spring API, No Langchain4J. Just core Java:

package com.gupta.morevectors;

public class HandCodedVectorsDemo {

    public static void main(String[] args) {

        final String SHORT_DESCRIPTION = "This is an amazing backpack with multiple pockets for your needs. It works for school or office.";

        // The long one is.. long :). It also talks about other features such as, lunch, recipes, travel, etc
        // It also says "backpack" more than once. That combination trips up dot product.
        final String LONG_DESCRIPTION = "A multipurpose bag that you can pack for all your needs. It has 5 pockets. It can be used at the office or while travelling.  And if you're carrying your lunch with a new recipe, don't worry, it has a non-spilling compartment made especially for containers that could leak when you're trying a new recipe. You can use it as a backpack with the attachments that come with it: put it on and travel with it like a backpack.";

        final String SHOPPER_SEARCH = "I want to buy a backpack today";

        // We hard-code the final variables SHORT_DESCRIPTION, LONG_DESCRIPTION and SHOPPER_SEARCH
        // as an array of float values for three dimensions, say, [backpack, recipe, travel]
        // and count how many times it mentions each word.
        //
        // Hand-coded vectors: [backpack, recipe, travel] = how many times each word appears.
        float[] shortDesc = { 1, 0, 0 };
        float[] longDesc  = { 2, 2, 2 };
        float[] query     = { 1, 0, 0 };

        // Score the query against each description with all three metrics.
        System.out.println("metric      short   long");
        System.out.printf("dot         %.3f   %.3f%n", dot(query, shortDesc),       dot(query, longDesc));
        System.out.printf("cosine      %.3f   %.3f%n", cosine(query, shortDesc),    cosine(query, longDesc));
        System.out.printf("euclidean   %.3f   %.3f%n", euclidean(query, shortDesc), euclidean(query, longDesc));
        System.out.println();

        System.out.printf("Search String = %s \n", SHOPPER_SEARCH);
        System.out.printf("Short Description = %s \n", SHORT_DESCRIPTION);
        System.out.printf("Long Description = %s \n", LONG_DESCRIPTION);

        double dotShortDesc = dot(query, shortDesc);
        double dotLongDesc = dot(query, longDesc);
        String winnerDesc = (dotShortDesc > dotLongDesc)? "SHORT DESCRIPTION" : "LONG DESCRIPTION";
        double winnerScore = (dotShortDesc > dotLongDesc)? dotShortDesc : dotLongDesc;
        System.out.printf("\nFor metric DOT (larger value is better), Winner is %s, with score %f: ", winnerDesc, winnerScore);

        double cosineShortDesc = cosine(query, shortDesc);
        double cosineLongDesc = cosine(query, longDesc);
        winnerDesc = (cosineShortDesc > cosineLongDesc)? "SHORT DESCRIPTION" : "LONG DESCRIPTION";
        winnerScore = (cosineShortDesc > cosineLongDesc)? cosineShortDesc : cosineLongDesc;
        System.out.printf("\nFor metric COSINE (larger value is better), Winner is %s, with score %f: ", winnerDesc, winnerScore);

        double euclideanShortDesc = euclidean(query, shortDesc);
        double euclideanLongDesc = euclidean(query, longDesc);
        winnerDesc = (euclideanShortDesc < euclideanLongDesc)? "SHORT DESCRIPTION" : "LONG DESCRIPTION";
        winnerScore = (euclideanShortDesc < euclideanLongDesc)? euclideanShortDesc : euclideanLongDesc;
        System.out.printf("\nFor Metric Euclidean (Shorter value is better). Winner is %s, with score %f: ", winnerDesc, winnerScore);

    }

    // Dot product: multiply matching parts, add them up.
    // It rewards both the direction and length.
    static double dot(float[] a, float[] b) {
        double sum = 0;
        for (int i = 0; i < a.length; i++) sum += a[i] * b[i];
        return sum;
    }

    // Euclidean distance: straight-line distance between the two points. Smaller = closer.
    static double euclidean(float[] a, float[] b) {
        double sum = 0;
        for (int i = 0; i < a.length; i++) sum += (a[i] - b[i]) * (a[i] - b[i]);
        return Math.sqrt(sum);
    }

    // Length (magnitude) of a vector.
    static double magnitude(float[] a) {
        double sum = 0;
        for (float x : a) sum += x * x;
        return Math.sqrt(sum);
    }

    // Cosine similarity: dot product divided by both lengths.
    // Only direction matters (-1..1).
    static double cosine(float[] a, float[] b) {
        return dot(a, b) / (magnitude(a) * magnitude(b));
    }
}

Here’s the output of the preceding code:

metric      short   long
dot         1.000   2.000
cosine      1.000   0.577
euclidean   0.000   3.000

Search String = I want to buy a backpack today 
Short Description = This is an amazing backpack with multiple pockets for your needs. It works for school or office. 
Long Description = A multipurpose bag that you can pack for all your needs. It has 5 pockets. It can be used at the office or while travelling.  And if you're carrying your lunch with a new recipe, don't worry, it has a non-spilling compartment made especially for containers that could leak when you're trying a new recipe. You can use it as a backpack with the attachments that come with it: put it on and travel with it like a backpack. 

For metric DOT (larger value is better), Winner is LONG DESCRIPTION, with score 2.000000: 
For metric COSINE (larger value is better), Winner is SHORT DESCRIPTION, with score 1.000000: 
For Metric Euclidean (Shorter value is better). Winner is SHORT DESCRIPTION, with score 0.000000: 
Process finished with exit code 0

Before I redo this example with real embeddings, a quick word on normalization.

A quick word on normalized embeddings

The vectors I hand-coded above are un-normalized, so they can have different lengths. The vectors an embedding model produces are normalized. Every one is scaled to the same length (1). That difference matters, because once all the vectors are the same length there’s no length for dot product to be fooled by. Dot product and cosine become equivalent, and Euclidean distance ranks things the same way too, so all three metrics agree.

This is the mirror image of the hand-coded example, where the vectors had different lengths and the metrics disagreed. So don’t be surprised when the code below picks the same top bag under all three metrics. The takeaway is a reassuring one: with a real embedding model, the metric you pick usually won’t change your top results. The “choose carefully” warning mostly matters when you build vectors yourself, un-normalized.

From hand-coded vectors to real embeddings in Qdrant

Let’s run the same idea with a real embedding model and store the results in Qdrant, using three collections so we can compare all three metrics. Watch what happens to DOT and COSINE once the vectors are normalized.

(If you are new to Qdrant, check out my previous post on how to setup a free Qdrant account here: https://medium.com/@mala.gupta/vector-databases-explained-for-java-developers-d07721b8c8ab).

Here’s the code (I’ve added comments to understand the code and deliberately repeated lines of code instead of using loops, to keep it simple):

package com.gupta.morevectors;

import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections;
import io.qdrant.client.grpc.Points;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;
import static io.qdrant.client.WithPayloadSelectorFactory.enable;

public class QdrantMetricsDemo {
    static final String QDRANT_CLUSTER_ENDPOINT = System.getenv().getOrDefault("QDRANT_CLUSTER_ENDPOINT", "localhost");
    static final String QDRANT_API_KEY = System.getenv().getOrDefault("QDRANT_API_KEY", "");
    static final int DIMENSIONS = 384;

    // 20 backpack descriptions
    static final String[] CATALOG = {
            "This is an amazing backpack with multiple pockets for your needs. It works for school or your office.",
            "A multipurpose bag that you can pack for all your needs. It has 5 pockets. It can be used at the office or while travelling.  And if you're carrying your lunch with a new recipe, don't worry, it has a non-spilling compartment made especially for containers that could leak when you're trying a new recipe. You can use it as a backpack with the attachments that come with it: put it on and travel with it like a backpack.",
            "Durable backpack with padded straps for everyday school use.",
            "Gym backpack with a separate shoe compartment and a wet pocket for sweaty clothes.",
            "Minimalist canvas backpack with a single main compartment and leather trims for a smart casual look.",
            "A sleek laptop backpack with a padded sleeve and a USB charging port for daily commuters.",
            "Leather laptop bag with a professional finish for office meetings and client visits.",
            "Kids backpack with cartoon prints, a name tag, and a small front pocket for snacks.",
            "A toddler harness backpack shaped like a friendly bear, with a detachable safety leash for parents.",
            "Rolltop cycling backpack with reflective strips and a fully waterproof main compartment.",
            "Foldable packable backpack that squeezes into its own pocket, perfect as a spare bag for travel.",
            "Waterproof hiking backpack with 40-litre capacity, a chest strap, and side bottle pockets for long trails.",
            "Camera backpack with padded dividers, a tripod strap, and quick side access for photographers on the move.",
            "Insulated cooler backpack that keeps food and drinks cold for picnics, hikes, and beach days.",
            "Heavy-duty tactical backpack with MOLLE webbing, several utility pouches, and a built-in hydration sleeve.",
            "A stylish mini backpack purse in vegan leather, sized for a phone, cards, and keys for evenings out.",
            "This travel backpack opens flat like a suitcase, has a dedicated laptop compartment, compression straps, and a hidden anti-theft pocket, making it ideal for weekend trips and carry-on flights.",
            "A multipurpose bag with five pockets; use it at the office or while travelling, and it has a non-spilling compartment for leak-prone lunch containers, plus straps so you can wear it like a backpack.",
            "An ergonomic hiking backpack with a ventilated mesh back panel, adjustable torso length, load-lifter straps, an integrated rain cover, and multiple compartments designed to distribute weight on multi-day treks.",
            "A student backpack with a laptop sleeve, water-resistant fabric, multiple organiser pockets, a padded back panel, and a luggage pass-through strap for weekend travel between campus and home."
    };

    static final String SHOPPER_SEARCH = "I want to buy a backpack today";

    static final String BACKPACKS_DOT = "backpacks_dot";
    static final String BACKPACKS_COSINE = "backpacks_cosine";
    static final String BACKPACKS_EUCLIDEAN = "backpacks_euclidean";

    static void main() throws Exception {

        // create QdrantClient that will be used in subsequent steps
        QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder(QDRANT_CLUSTER_ENDPOINT,
                                                                           6334,
                                                                           true)
                                                               .withApiKey(QDRANT_API_KEY)
                                                               .build());

        createCollection(client, BACKPACKS_DOT, Collections.Distance.Dot);
        createCollection(client, BACKPACKS_COSINE, Collections.Distance.Cosine);
        createCollection(client, BACKPACKS_EUCLIDEAN, Collections.Distance.Euclid);

        System.out.println(client.listCollectionsAsync().get());

        // Embedding model
        var embeddingModel = new AllMiniLmL6V2EmbeddingModel();

        // Create corresponding vector embeddings for CATALOG items
        List<Points.PointStruct> points = new ArrayList<>();
        for (int i = 0; i < CATALOG.length; i++) {
            points.add(point(i + 1, CATALOG[i], embed(embeddingModel, CATALOG[i])));
        }

        // Update or insert values in three collections (DOT, COSINE, and EUCLIDEAN)
        client.upsertAsync(BACKPACKS_DOT, points).get();
        client.upsertAsync(BACKPACKS_COSINE, points).get();
        client.upsertAsync(BACKPACKS_EUCLIDEAN, points).get();

        // Create vector embeddings for SHOPPER_QUERY
        List<Float> query = embed(embeddingModel, SHOPPER_SEARCH);

        // Search and store reults for each collection (DOT, COSINE, and EUCLIDEAN)
        List<Points.ScoredPoint> dotResults = search(client, BACKPACKS_DOT, query, 5);
        List<Points.ScoredPoint> cosineResults = search(client, BACKPACKS_COSINE, query, 5);
        List<Points.ScoredPoint> euclideanResults = search(client, BACKPACKS_EUCLIDEAN, query, 5);

        // Print search results
        System.out.println("\n(DOT metric): Searching " + CATALOG.length + " backpacks for: \"" + SHOPPER_SEARCH + "\"\n");
        for (int rank = 0; rank < dotResults.size(); rank++) {
            Points.ScoredPoint p = dotResults.get(rank);
            System.out.printf("   %d. score=%.4f  %s%n",
                              rank + 1, p.getScore(), p.getPayloadMap().get("description").getStringValue());
        }

        System.out.println("\n(COSINE metric): Searching " + CATALOG.length + " backpacks for: \"" + SHOPPER_SEARCH + "\"\n");
        for (int rank = 0; rank < cosineResults.size(); rank++) {
            Points.ScoredPoint p = cosineResults.get(rank);
            System.out.printf("   %d. score=%.4f  %s%n",
                              rank + 1, p.getScore(), p.getPayloadMap().get("description").getStringValue());
        }

        System.out.println("\n(EUCLIDEAN metric): Searching " + CATALOG.length + " backpacks for: \"" + SHOPPER_SEARCH + "\"\n");
        for (int rank = 0; rank < euclideanResults.size(); rank++) {
            Points.ScoredPoint p = euclideanResults.get(rank);
            System.out.printf("   %d. score=%.4f  %s%n",
                              rank + 1, p.getScore(), p.getPayloadMap().get("description").getStringValue());
        }

    }

    private static void createCollection(QdrantClient client, String collectionName, Collections.Distance distance) throws InterruptedException, ExecutionException {
        if (client.collectionExistsAsync(collectionName).get()) {
            client.deleteCollectionAsync(collectionName).get();
        }

        client.createCollectionAsync(collectionName,
                                     Collections.VectorParams.newBuilder()
                                                             .setSize(DIMENSIONS)
                                                             .setDistance(distance)
                                                             .build()
        ).get();
    }

    static Points.PointStruct point(int id, String description, List<Float> vector) {
        return Points.PointStruct.newBuilder()
                                 .setId(id(id))
                                 .setVectors(vectors(vector))
                                 .putAllPayload(Map.of("description", value(description)))
                                 .build();
    }

    // Convert text into a normalized 384-dim embedding
    static List<Float> embed(AllMiniLmL6V2EmbeddingModel model, String text) {
        Embedding embedding = model.embed(text).content();
        List<Float> vector = new ArrayList<>();
        for (float f : embedding.vector()) vector.add(f);
        return vector;
    }

    static List<Points.ScoredPoint> search(QdrantClient client, String collection, List<Float> query, int limit)
            throws Exception {
        return client.queryAsync(Points.QueryPoints.newBuilder()
                                                   .setCollectionName(collection)
                                                   .setQuery(nearest(query))
                                                   .setLimit(limit)
                                                   .setWithPayload(enable(true))
                                                   .build()).get();
    }
}

Here’s the output of the preceding code:

(DOT metric): Searching 20 backpacks for: "I want to buy a backpack today"

   1. score=0.5600  Durable backpack with padded straps for everyday school use.
   2. score=0.5369  This is an amazing backpack with multiple pockets for your needs. It works for school or your office.
   3. score=0.5294  Heavy-duty tactical backpack with MOLLE webbing, several utility pouches, and a built-in hydration sleeve.
   4. score=0.5242  A sleek laptop backpack with a padded sleeve and a USB charging port for daily commuters.
   5. score=0.5204  Waterproof hiking backpack with 40-litre capacity, a chest strap, and side bottle pockets for long trails.

(COSINE metric): Searching 20 backpacks for: "I want to buy a backpack today"

   1. score=0.5600  Durable backpack with padded straps for everyday school use.
   2. score=0.5369  This is an amazing backpack with multiple pockets for your needs. It works for school or your office.
   3. score=0.5294  Heavy-duty tactical backpack with MOLLE webbing, several utility pouches, and a built-in hydration sleeve.
   4. score=0.5242  A sleek laptop backpack with a padded sleeve and a USB charging port for daily commuters.
   5. score=0.5204  Waterproof hiking backpack with 40-litre capacity, a chest strap, and side bottle pockets for long trails.

(EUCLIDEAN metric): Searching 20 backpacks for: "I want to buy a backpack today"

   1. score=0.9381  Durable backpack with padded straps for everyday school use.
   2. score=0.9624  This is an amazing backpack with multiple pockets for your needs. It works for school or your office.
   3. score=0.9701  Heavy-duty tactical backpack with MOLLE webbing, several utility pouches, and a built-in hydration sleeve.
   4. score=0.9755  A sleek laptop backpack with a padded sleeve and a USB charging port for daily commuters.
   5. score=0.9794  Waterproof hiking backpack with 40-litre capacity, a chest strap, and side bottle pockets for long trails.

Why DOT and COSINE gave identical results

Look at the result in the preceding section and you’ll notice that DOT and COSINE didn’t just rank the backpacks in the same order, they also printed the same scores. EUCLIDEAN shows different numbers (it’s a distance, so smaller is closer), but it ranks the backpacks in that exact same order too. That’s not a bug. It’s normalization at work: all-MiniLM returns vectors of length 1, and for unit-length vectors cosine and dot are the same calculation (cosine = dot / (1 x 1) = dot), while Euclidean distance moves in lockstep with them. When every vector has length 1, all three metrics agree.

This is the flip side of the hand-coded example. There, we wrote the vectors ourselves and they had different lengths that were un-normalized and DOT was fooled by the longer vector while COSINE was not, so their results differed. Feed un-normalized vectors and you’ll see DOT and COSINE disagree; feed normalized ones (which is what any real embedding model produces) and they line up. So the “choose your metric carefully” warning really only matters when you build vectors by hand. With a real embedding model, the metric you pick rarely changes what comes back on top.

So does the metric matter?

If you are worried about picking the wrong similarity metric, here’s some good news. With a real embedding model, you usually can’t. Those models normalize their vectors, dot product and cosine are literally the same calculation, and Euclidean ranks results the same way.

But this doesn’t mean that it is irrelevant. It matters. The leverage lives somewhere other than the metric dropdown. The metric only visibly changed our results when we fed it un-normalized, hand-built vectors, which is the one case a real pipeline avoids.

The biggest lever by far is the embedding model itself. A different model understands your text differently and reorders everything. Close behind is what you actually embed, since a word, a sentence, and a paragraph produce very different vectors, which makes how you chunk long documents often the single most important decision in the whole system. The shape of your query relative to your data matters too, because short queries against long passages need models built for that, and mismatches bury good results. And finally, filtering, pre-processing, and index tuning frequently decide the outcome before or around the similarity step.

So the real takeaway isn’t “metrics don’t matter.” It is to pick a good embedding model, think about how to chunk your text, and then let cosine be your sensible default.

What’s next

You now know how a vector database compares vectors. The next question is what you put into it and feed a model a whole document. Watch out for the next blog post in this series in which I’ll talk about chunking and how to split long text so search actually works.


메타데이터
post_id
9749c4dbbbc3
slug
how-vector-databases-measure-closeness-dot-product-cosine-and-euclidean-9749c4dbbbc3
url
https://medium.com/@mala.gupta/how-vector-databases-measure-closeness-dot-product-cosine-and-euclidean-9749c4dbbbc3
canonical_url
https://medium.com/@mala.gupta/how-vector-databases-measure-closeness-dot-product-cosine-and-euclidean-9749c4dbbbc3
author_url
https://medium.com/@mala.gupta
status
ok
fetched_at
2026-07-09 22:34:41