Jina AI
Jina AI, now part of Elastic, provides text, code, and multimodal embedding models through the Jina Embedding API. There are two ways to use them with Qdrant:
- Call the Jina API from your application and send the vectors to Qdrant. This works with any Qdrant deployment, and it’s what most of this page covers.
- On Qdrant Cloud, pass text or images to Qdrant and let Qdrant Cloud Inference call Jina AI for you.
Models
| Model | Dimensions | Max Tokens | Input |
|---|---|---|---|
jina-embeddings-v5-text-small | 1024 | 32,768 | Text |
jina-embeddings-v5-text-nano | 768 | 8,192 | Text |
jina-embeddings-v5-omni-small | 1024 | 32,768 | Text, image, video, audio, PDF |
jina-embeddings-v5-omni-nano | 768 | 8,192 | Text, image, video, audio, PDF |
jina-code-embeddings-1.5b | 1536 | 32,768 | Code, text |
jina-code-embeddings-0.5b | 896 | 32,768 | Code, text |
Jina AI recommends jina-embeddings-v5-text-small for text retrieval, and the nano models when latency or cost matters more than the last point of accuracy. An omni model returns the same vector for text as the text model of the same size, so you can add images or video to an existing text collection without re-embedding it. For source code, use a jina-code-embeddings model with tasks such as nl2code.passage and nl2code.query. Every model in the Models table accepts a dimensions parameter that shortens its vectors (see Reduce Vector Size).
jina-embeddings-v4, jina-embeddings-v3, jina-clip-v2, and the jina-embeddings-v2 models are previous generations. Use a v5 model instead. jina-embeddings-v4 is free on the API but throttled, and Jina AI says it is not suitable for production.
The API L2-normalizes vectors by default, so the Cosine and Dot distance metrics rank results identically. This page uses Cosine.
Setup
Jina AI has no official Python or TypeScript SDK, so these examples call its REST API with requests and fetch. Install the HTTP client and the Qdrant client:
- Python:
pip install requests qdrant-client - TypeScript:
npm install @qdrant/js-client-rest
Create a helper for the Embedding API, the Qdrant client, and a collection sized for the model. The helper reads your API key from the JINA_API_KEY environment variable.
import os
from typing import Any
import requests
from qdrant_client import QdrantClient, models
JINA_URL = "https://api.jina.ai/v1/embeddings"
MODEL = "jina-embeddings-v5-text-small"
def jina_embed(**body: Any) -> list[dict[str, Any]]:
response = requests.post(
JINA_URL,
headers={"Authorization": f"Bearer {os.environ['JINA_API_KEY']}"},
json=body,
)
response.raise_for_status()
return response.json()["data"]
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="{collection_name}",
vectors_config=models.VectorParams(size=1024, distance=models.Distance.COSINE),
)
import { QdrantClient } from "@qdrant/js-client-rest";
const JINA_URL = "https://api.jina.ai/v1/embeddings";
const MODEL = "jina-embeddings-v5-text-small";
async function jinaEmbed<T = { index: number; embedding: number[] }>(body: object): Promise<T[]> {
const response = await fetch(JINA_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.JINA_API_KEY}`,
},
body: JSON.stringify(body),
});
if (!response.ok) throw new Error(await response.text());
return ((await response.json()) as { data: T[] }).data;
}
const client = new QdrantClient({ url: "http://localhost:6333" });
await client.createCollection("{collection_name}", {
vectors: { size: 1024, distance: "Cosine" },
});
Embed Documents
One request embeds a list of inputs. Set task to retrieval.passage for documents. Each item in the response carries the index of its input, which this example uses as the point ID:
documents = [
"Qdrant is a vector search engine.",
"Jina AI embedding models turn text, images, and video into dense vectors.",
]
data = jina_embed(model=MODEL, task="retrieval.passage", input=documents)
client.upsert(
collection_name="{collection_name}",
points=[
models.PointStruct(
id=item["index"],
vector=item["embedding"],
payload={"text": documents[item["index"]]},
)
for item in data
],
)
const documents = [
"Qdrant is a vector search engine.",
"Jina AI embedding models turn text, images, and video into dense vectors.",
];
const data = await jinaEmbed({ model: MODEL, task: "retrieval.passage", input: documents });
await client.upsert("{collection_name}", {
points: data.map((item) => ({
id: item.index,
vector: item.embedding,
payload: { text: documents[item.index] },
})),
});
The API sets no limit on the number of inputs per request, but each input must fit the model’s context window. A longer input returns an error unless you set truncate to true. Requests also count against per-key token-per-minute rate limits, so split large datasets into several requests, and see Upload Points for batching on the Qdrant side.
Embed Queries
v5 models have task-specific adapters. Embed queries with retrieval.query and documents with retrieval.passage, so the model encodes a short question and a longer passage that answers it close together. The default task is text-matching, which is for symmetric similarity, so always set the task for retrieval:
query_vector = jina_embed(model=MODEL, task="retrieval.query", input=["What is Qdrant?"])[0][
"embedding"
]
hits = client.query_points(
collection_name="{collection_name}",
query=query_vector,
limit=3,
).points
const [queryItem] = await jinaEmbed({ model: MODEL, task: "retrieval.query", input: ["What is Qdrant?"] });
const { points: hits } = await client.query("{collection_name}", {
query: queryItem.embedding,
limit: 3,
});
The other v5 tasks are text-matching, classification, and clustering.
Reduce Vector Size
v5 models use Matryoshka Representation Learning, so the dimensions parameter shortens the vector with little loss in quality, down to 32 dimensions. Set the collection’s size to the same value, and pass the same dimensions when you embed queries:
reduced = jina_embed(model=MODEL, task="retrieval.passage", input=documents, dimensions=256)
const reduced = await jinaEmbed({
model: MODEL,
task: "retrieval.passage",
input: documents,
dimensions: 256,
});
To keep the full vectors but store them in less memory, use Binary Quantization.
Embed Images, Video, Audio, and PDFs
The omni models embed images, video, audio, and PDFs into the same vector space as text. Because jina-embeddings-v5-omni-small returns the same text vectors as jina-embeddings-v5-text-small, this example adds images to the collection from Embed Documents, and the query from Embed Queries searches the text and the images together:
images = [
"https://i.ibb.co/nQNGqL0/beach1.jpg",
"https://i.ibb.co/r5w8hG8/beach2.jpg",
]
image_data = jina_embed(
model="jina-embeddings-v5-omni-small",
task="retrieval.passage",
input=[{"image": url} for url in images],
)
client.upsert(
collection_name="{collection_name}",
points=[
models.PointStruct(
id=100 + item["index"],
vector=item["embedding"],
payload={"image": images[item["index"]]},
)
for item in image_data
],
)
const images = ["https://i.ibb.co/nQNGqL0/beach1.jpg", "https://i.ibb.co/r5w8hG8/beach2.jpg"];
const imageData = await jinaEmbed({
model: "jina-embeddings-v5-omni-small",
task: "retrieval.passage",
input: images.map((url) => ({ image: url })),
});
await client.upsert("{collection_name}", {
points: imageData.map((item) => ({
id: 100 + item.index,
vector: item.embedding,
payload: { image: images[item.index] },
})),
});
Each list item is one input, written as {"text": ...}, {"image": ...}, {"video": ...}, or {"audio": ...}, with a URL or a base64 string as the value. Some inputs follow different rules:
- PDF: send
{"pdf": ...}as the wholeinput, one PDF per request, up to 8 MB. - Images: up to 5 MB each.
- Video: the model samples 32 frames.
- Combined inputs: a
{"content": [...]}item fuses several parts, for example a caption and an image, into one vector. The order of the parts affects the result.
Multivector Output
jina-colbert-v2 is a late-interaction model that returns one 128-dimensional vector per token, which Qdrant stores as a multivector and scores with MaxSim. It takes input_type instead of task, and the response carries embeddings instead of embedding. Store the vectors in a separate collection:
client.create_collection(
collection_name="{colbert_collection_name}",
vectors_config=models.VectorParams(
size=128,
distance=models.Distance.COSINE,
multivector_config=models.MultiVectorConfig(
comparator=models.MultiVectorComparator.MAX_SIM
),
),
)
colbert_data = jina_embed(model="jina-colbert-v2", input_type="document", input=documents)
client.upsert(
collection_name="{colbert_collection_name}",
points=[
models.PointStruct(
id=item["index"],
vector=item["embeddings"],
payload={"text": documents[item["index"]]},
)
for item in colbert_data
],
)
colbert_query = jina_embed(model="jina-colbert-v2", input_type="query", input=["What is Qdrant?"])
colbert_hits = client.query_points(
collection_name="{colbert_collection_name}",
query=colbert_query[0]["embeddings"],
limit=3,
).points
type MultiItem = { index: number; embeddings: number[][] };
await client.createCollection("{colbert_collection_name}", {
vectors: { size: 128, distance: "Cosine", multivector_config: { comparator: "max_sim" } },
});
const colbertData = await jinaEmbed<MultiItem>({
model: "jina-colbert-v2",
input_type: "document",
input: documents,
});
await client.upsert("{colbert_collection_name}", {
points: colbertData.map((item) => ({
id: item.index,
vector: item.embeddings,
payload: { text: documents[item.index] },
})),
});
const [colbertQuery] = await jinaEmbed<MultiItem>({
model: "jina-colbert-v2",
input_type: "query",
input: ["What is Qdrant?"],
});
const { points: colbertHits } = await client.query("{colbert_collection_name}", {
query: colbertQuery.embeddings,
limit: 3,
});
jina-embeddings-v4 also returns multivectors when you set return_multivector to true. Jina AI notes that for most retrieval pipelines, a dense v5 model followed by a reranker gives better accuracy for the cost.
Qdrant Cloud Inference
On Qdrant Cloud, Qdrant can call Jina AI for you. Instead of a vector, send a Document or an Image with the model name prefixed with jinaai/. Pass your Jina AI API key in the jina-api-key request header or in the options object. Qdrant does not store the key, so send it with every request. Jina AI parameters such as dimensions also go in options, and the collection’s vector size must match them, which is 512 in these examples. The examples use jina-clip-v2 to embed an image and search it with text.
To ingest an image:
PUT /collections/{collection_name}/points?wait=true
{
"points": [
{
"id": 1,
"vector": {
"image": "https://qdrant.tech/example.png",
"model": "jinaai/jina-clip-v2",
"options": {
"jina-api-key": "<YOUR_JINAAI_API_KEY>",
"dimensions": 512
}
}
}
]
}
from qdrant_client import QdrantClient, models
from qdrant_client.context_headers import headers
client = QdrantClient(
url="https://xyz-example.qdrant.io:6333",
api_key="<your-qdrant-api-key>",
cloud_inference=True
)
with headers({"jina-api-key": "<YOUR_JINAAI_API_KEY>"}):
client.upsert(
collection_name="{collection_name}",
points=[
models.PointStruct(
id=1,
vector=models.Image(
image="https://qdrant.tech/example.png",
model="jinaai/jina-clip-v2",
options={
"dimensions": 512
}
)
)
]
)
import { QdrantClient, withHeaders } from "@qdrant/js-client-rest";
await withHeaders({ 'jina-api-key': '<YOUR_JINAAI_API_KEY>' }, () =>
client.upsert("{collection_name}", {
points: [
{
id: 1,
vector: {
image: 'https://qdrant.tech/example.png',
model: 'jinaai/jina-clip-v2',
options: {
dimensions: 512,
},
},
},
],
})
);
use qdrant_client::{
Payload, Qdrant,
qdrant::{Image, PointStruct, UpsertPointsBuilder},
};
use std::collections::HashMap;
let mut options = HashMap::new();
options.insert("dimensions".to_string(), 512.into());
client
.with_header("jina-api-key", "<YOUR_JINAAI_API_KEY>")
.upsert_points(UpsertPointsBuilder::new("{collection_name}",
vec![
PointStruct::new(1,
Image {
image: Some("https://qdrant.tech/example.png".into()),
model: "jinaai/jina-clip-v2".into(),
options,
},
Payload::default())
]).wait(true))
.await?;
import static io.qdrant.client.PointIdFactory.id;
import static io.qdrant.client.ValueFactory.value;
import static io.qdrant.client.VectorsFactory.vectors;
import io.grpc.Context;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.RequestHeaders;
import io.qdrant.client.grpc.Points.Image;
import io.qdrant.client.grpc.Points.PointStruct;
import java.util.List;
import java.util.Map;
Context ctx = RequestHeaders.withHeader(
Context.current(), "jina-api-key", "<YOUR_JINAAI_API_KEY>");
ctx.call(() -> client
.upsertAsync(
"{collection_name}",
List.of(
PointStruct.newBuilder()
.setId(id(1))
.setVectors(
vectors(
Image.newBuilder()
.setModel("jinaai/jina-clip-v2")
.setImage(value("https://qdrant.tech/example.png"))
.putAllOptions(
Map.of(
"dimensions",
value(512)))
.build()))
.build()))
.get());
using Qdrant.Client;
using Qdrant.Client.Grpc;
using (RequestHeaders.Use("jina-api-key", "<YOUR_JINAAI_API_KEY>"))
await client.UpsertAsync(
collectionName: "{collection_name}",
points: new List<PointStruct>
{
new()
{
Id = 1,
Vectors = new Image()
{
Model = "jinaai/jina-clip-v2",
Image_ = "https://qdrant.tech/example.png",
Options = { ["dimensions"] = 512 },
},
},
}
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
ctx := qdrant.WithHeader(context.Background(), "jina-api-key", "<YOUR_JINAAI_API_KEY>")
client.Upsert(ctx, &qdrant.UpsertPoints{
CollectionName: "{collection_name}",
Points: []*qdrant.PointStruct{
{
Id: qdrant.NewIDNum(uint64(1)),
Vectors: qdrant.NewVectorsImage(&qdrant.Image{
Model: "jinaai/jina-clip-v2",
Image: qdrant.NewValueString("https://qdrant.tech/example.png"),
Options: qdrant.NewValueMap(map[string]any{
"dimensions": 512,
}),
}),
},
},
})
To search with text:
POST /collections/{collection_name}/points/query
{
"query": {
"text": "Mission to Mars",
"model": "jinaai/jina-clip-v2",
"options": {
"jina-api-key": "<YOUR_JINAAI_API_KEY>",
"dimensions": 512
}
}
}
from qdrant_client import QdrantClient, models
from qdrant_client.context_headers import headers
client = QdrantClient(
url="https://xyz-example.qdrant.io:6333",
api_key="<your-qdrant-api-key>",
cloud_inference=True
)
with headers({"jina-api-key": "<YOUR_JINAAI_API_KEY>"}):
client.query_points(
collection_name="{collection_name}",
query=models.Document(
text="Mission to Mars",
model="jinaai/jina-clip-v2",
options={
"dimensions": 512
}
)
)
import { QdrantClient, withHeaders } from "@qdrant/js-client-rest";
await withHeaders({ 'jina-api-key': '<YOUR_JINAAI_API_KEY>' }, () =>
client.query("{collection_name}", {
query: {
text: 'Mission to Mars',
model: 'jinaai/jina-clip-v2',
options: {
dimensions: 512,
},
},
})
);
use qdrant_client::{
Qdrant,
qdrant::{Document, Query, QueryPointsBuilder, Value},
};
use std::collections::HashMap;
let mut options = HashMap::<String, Value>::new();
options.insert("dimensions".to_string(), 512.into());
client
.with_header("jina-api-key", "<YOUR_JINAAI_API_KEY>")
.query(
QueryPointsBuilder::new("{collection_name}")
.query(Query::new_nearest(Document {
text: "Mission to Mars".into(),
model: "jinaai/jina-clip-v2".into(),
options,
}))
.build(),
)
.await?;
import static io.qdrant.client.QueryFactory.nearest;
import static io.qdrant.client.ValueFactory.value;
import io.grpc.Context;
import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.RequestHeaders;
import io.qdrant.client.grpc.Points.Document;
import io.qdrant.client.grpc.Points.QueryPoints;
import java.util.Map;
Context ctx = RequestHeaders.withHeader(
Context.current(), "jina-api-key", "<YOUR_JINAAI_API_KEY>");
ctx.call(() -> client
.queryAsync(
QueryPoints.newBuilder()
.setCollectionName("{collection_name}")
.setQuery(
nearest(
Document.newBuilder()
.setModel("jinaai/jina-clip-v2")
.setText("Mission to Mars")
.putAllOptions(
Map.of(
"dimensions",
value(512)))
.build()))
.build())
.get());
using Qdrant.Client;
using Qdrant.Client.Grpc;
using (RequestHeaders.Use("jina-api-key", "<YOUR_JINAAI_API_KEY>"))
await client.QueryAsync(
collectionName: "{collection_name}",
query: new Document()
{
Model = "jinaai/jina-clip-v2",
Text = "Mission to Mars",
Options = { ["dimensions"] = 512 },
}
);
import (
"context"
"github.com/qdrant/go-client/qdrant"
)
ctx := qdrant.WithHeader(context.Background(), "jina-api-key", "<YOUR_JINAAI_API_KEY>")
client.Query(ctx, &qdrant.QueryPoints{
CollectionName: "{collection_name}",
Query: qdrant.NewQueryDocument(&qdrant.Document{
Text: "Mission to Mars",
Model: "jinaai/jina-clip-v2",
Options: qdrant.NewValueMap(map[string]any{
"dimensions": 512,
}),
}),
})
For other ways to pass the key, see External Providers.
Store and Search the Vectors
Next Steps
- Filter search results by payload fields, for example to search only images.
- Jina AI Embedding API documentation, which lists every model, input type, and parameter.
- Combine Jina AI vectors with sparse vectors for hybrid search.
- Reduce memory use with quantization.