Superlinked
The Superlinked Inference Engine (SIE) is an open-source server, under the Apache 2.0 license, that runs open embedding models behind one API. One model can return dense, sparse, and multivector (ColBERT) vectors from the same request. The sie-qdrant Python package wraps the SIE client and returns vectors in the format Qdrant expects. TypeScript applications use the SIE SDK, @superlinked/sie-sdk, directly. There are two ways to run SIE:
- Self-host the SIE server with Docker or Kubernetes. The examples on this page connect to
http://localhost:8080. - Call SIE Serverless at
https://api.superlinked.com, which reads your API key from theSIE_API_KEYenvironment variable.
Models
The Model Catalog lists every model SIE serves and the output types each one supports. These cover the examples on this page:
| Model | Output | Dimensions | Max Tokens |
|---|---|---|---|
BAAI/bge-m3 (the default) | Dense, sparse, multivector | 1024 | 8,192 |
Qwen/Qwen3-Embedding-0.6B | Dense | 1024 | 32,768 |
jinaai/jina-colbert-v2 | Multivector | 128 per token | 8,192 |
Check each model’s license before you use it. For example, BAAI/bge-m3 is MIT-licensed, while jinaai/jina-colbert-v2 is licensed for non-commercial use only (CC BY-NC 4.0).
Use the Cosine distance metric for dense vectors. It gives correct results whether or not a model normalizes its output.
Setup
Start an SIE server, here the CPU image:
docker run -p 8080:8080 ghcr.io/superlinked/sie-server:latest-cpu-default
Install the SIE integration and the Qdrant client. In Python, sie-qdrant installs sie-sdk and qdrant-client as dependencies:
- Python:
pip install sie-qdrant - TypeScript:
npm install @superlinked/sie-sdk @qdrant/js-client-rest
Create the SIE vectorizer, the Qdrant client, and a collection sized for the model. To use SIE Serverless instead, set SIE_API_KEY and pass base_url="https://api.superlinked.com" in Python, or call new SIEClient() with no URL in TypeScript.
from qdrant_client import QdrantClient, models
from sie_qdrant import SIEVectorizer
MODEL = "BAAI/bge-m3"
vectorizer = SIEVectorizer(base_url="http://localhost:8080", model=MODEL)
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="{collection_name}",
vectors_config=models.VectorParams(size=1024, distance=models.Distance.COSINE),
)
import { SIEClient, denseEmbedding } from "@superlinked/sie-sdk";
import { QdrantClient } from "@qdrant/js-client-rest";
const MODEL = "BAAI/bge-m3";
const sie = new SIEClient("http://localhost:8080");
const client = new QdrantClient({ url: "http://localhost:6333" });
await client.createCollection("{collection_name}", {
vectors: { size: 1024, distance: "Cosine" },
});
Embed Documents
One call embeds a list of documents and returns the vectors in input order. This example uses the position of each document as the point ID:
documents = [
"Qdrant is a vector search engine.",
"SIE serves open embedding models from one endpoint.",
]
vectors = vectorizer.embed_documents(documents)
client.upsert(
collection_name="{collection_name}",
points=[
models.PointStruct(id=idx, vector=vector, payload={"text": documents[idx]})
for idx, vector in enumerate(vectors)
],
)
const documents = [
"Qdrant is a vector search engine.",
"SIE serves open embedding models from one endpoint.",
];
const results = await sie.encode(
MODEL,
documents.map((text) => ({ text })),
);
await client.upsert("{collection_name}", {
points: results.map((result, idx) => ({
id: idx,
vector: denseEmbedding(result),
payload: { text: documents[idx] },
})),
});
Embed Queries
embed_query marks the text as a query, so SIE applies query-specific encoding for models that need it, such as E5. In TypeScript, pass isQuery: true. For models without a query prompt, such as BAAI/bge-m3, queries and documents are embedded the same way.
query_vector = vectorizer.embed_query("What is Qdrant?")
hits = client.query_points(
collection_name="{collection_name}",
query=query_vector,
limit=3,
).points
const queryResult = await sie.encode(
MODEL,
{ text: "What is Qdrant?" },
{ isQuery: true },
);
const { points: hits } = await client.query("{collection_name}", {
query: denseEmbedding(queryResult),
limit: 3,
});
Hybrid Search with Dense and Sparse Vectors
SIENamedVectorizer asks for several output types in one SIE call and returns a dictionary keyed by output type. With BAAI/bge-m3, one call returns a dense vector and a learned sparse vector. Sparse vectors come back as indices and values, which is the format Qdrant stores natively.
Create a separate collection with a named dense vector and a named sparse vector, and upsert both:
from sie_qdrant import SIENamedVectorizer
hybrid_vectorizer = SIENamedVectorizer(
base_url="http://localhost:8080",
model="BAAI/bge-m3",
output_types=["dense", "sparse"],
)
client.create_collection(
collection_name="{hybrid_collection_name}",
vectors_config={"dense": models.VectorParams(size=1024, distance=models.Distance.COSINE)},
sparse_vectors_config={"sparse": models.SparseVectorParams()},
)
named_vectors = hybrid_vectorizer.embed_documents(documents)
client.upsert(
collection_name="{hybrid_collection_name}",
points=[
models.PointStruct(
id=idx,
vector={
"dense": named["dense"],
"sparse": models.SparseVector(**named["sparse"]),
},
payload={"text": documents[idx]},
)
for idx, named in enumerate(named_vectors)
],
)
import { sparseEmbedding } from "@superlinked/sie-sdk";
await client.createCollection("{hybrid_collection_name}", {
vectors: { dense: { size: 1024, distance: "Cosine" } },
sparse_vectors: { sparse: {} },
});
const hybridResults = await sie.encode(
"BAAI/bge-m3",
documents.map((text) => ({ text })),
{ outputTypes: ["dense", "sparse"] },
);
await client.upsert("{hybrid_collection_name}", {
points: hybridResults.map((result, idx) => ({
id: idx,
vector: {
dense: denseEmbedding(result),
sparse: sparseEmbedding(result),
},
payload: { text: documents[idx] },
})),
});
To search, run a dense and a sparse query as prefetches and merge their results with Reciprocal Rank Fusion (RRF):
query = hybrid_vectorizer.embed_query("What is Qdrant?")
hits = client.query_points(
collection_name="{hybrid_collection_name}",
prefetch=[
models.Prefetch(query=query["dense"], using="dense", limit=20),
models.Prefetch(query=models.SparseVector(**query["sparse"]), using="sparse", limit=20),
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=3,
).points
const hybridQuery = await sie.encode(
"BAAI/bge-m3",
{ text: "What is Qdrant?" },
{ outputTypes: ["dense", "sparse"], isQuery: true },
);
const { points: hybridHits } = await client.query("{hybrid_collection_name}", {
prefetch: [
{ query: denseEmbedding(hybridQuery), using: "dense", limit: 20 },
{ query: sparseEmbedding(hybridQuery), using: "sparse", limit: 20 },
],
query: { fusion: "rrf" },
limit: 3,
});
ColBERT Multivectors
Late-interaction models such as jinaai/jina-colbert-v2 return one vector per token. Qdrant stores them as a multivector and scores them with MaxSim on the server, so you don’t compute late-interaction scores in your application. Store them in a separate collection:
colbert_vectorizer = SIENamedVectorizer(
base_url="http://localhost:8080",
model="jinaai/jina-colbert-v2",
output_types=["multivector"],
)
client.create_collection(
collection_name="{colbert_collection_name}",
vectors_config={
"colbert": models.VectorParams(
size=128,
distance=models.Distance.COSINE,
multivector_config=models.MultiVectorConfig(
comparator=models.MultiVectorComparator.MAX_SIM,
),
),
},
)
client.upsert(
collection_name="{colbert_collection_name}",
points=[
models.PointStruct(
id=idx,
vector={"colbert": named["multivector"]},
payload={"text": documents[idx]},
)
for idx, named in enumerate(colbert_vectorizer.embed_documents(documents))
],
)
query = colbert_vectorizer.embed_query("What is Qdrant?")
hits = client.query_points(
collection_name="{colbert_collection_name}",
query=query["multivector"],
using="colbert",
limit=3,
).points
import { multivectorEmbedding } from "@superlinked/sie-sdk";
await client.createCollection("{colbert_collection_name}", {
vectors: {
colbert: {
size: 128,
distance: "Cosine",
multivector_config: { comparator: "max_sim" },
},
},
});
const colbertResults = await sie.encode(
"jinaai/jina-colbert-v2",
documents.map((text) => ({ text })),
{ outputTypes: ["multivector"] },
);
await client.upsert("{colbert_collection_name}", {
points: colbertResults.map((result, idx) => {
if (!result.multivector) throw new Error("SIE returned no multivector");
return {
id: idx,
vector: { colbert: multivectorEmbedding(result.multivector) },
payload: { text: documents[idx] },
};
}),
});
const colbertQuery = await sie.encode(
"jinaai/jina-colbert-v2",
{ text: "What is Qdrant?" },
{ outputTypes: ["multivector"], isQuery: true },
);
if (!colbertQuery.multivector) throw new Error("SIE returned no query multivector");
const { points: colbertHits } = await client.query("{colbert_collection_name}", {
query: multivectorEmbedding(colbertQuery.multivector),
using: "colbert",
limit: 3,
});
Vectorizer Options
SIEVectorizer and SIENamedVectorizer accept the same options. SIENamedVectorizer also takes output_types, which defaults to ["dense", "sparse"] and must match the named vectors in your collection.
| Parameter | Default | Description |
|---|---|---|
base_url | http://localhost:8080 | SIE server URL |
model | BAAI/bge-m3 | Model to use, from the Model Catalog |
instruction | None | Instruction prefix for instruction-tuned models |
output_dtype | None | Output data type: float32, float16, int8, or binary |
gpu | None | GPU type to route requests to |
options | None | Model-specific options |
timeout_s | 180.0 | Request timeout in seconds |
Store and Search the Vectors
Next Steps
- Learn how hybrid queries work, including other fusion methods.
- Configure multivectors for ColBERT and other late-interaction models.
- Read the SIE Qdrant integration guide and the SIE quickstart.
- Filter search results by payload fields.
- Reduce memory use with quantization.