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 the SIE_API_KEY environment variable.

Models

The Model Catalog lists every model SIE serves and the output types each one supports. These cover the examples on this page:

ModelOutputDimensionsMax Tokens
BAAI/bge-m3 (the default)Dense, sparse, multivector10248,192
Qwen/Qwen3-Embedding-0.6BDense102432,768
jinaai/jina-colbert-v2Multivector128 per token8,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.

ParameterDefaultDescription
base_urlhttp://localhost:8080SIE server URL
modelBAAI/bge-m3Model to use, from the Model Catalog
instructionNoneInstruction prefix for instruction-tuned models
output_dtypeNoneOutput data type: float32, float16, int8, or binary
gpuNoneGPU type to route requests to
optionsNoneModel-specific options
timeout_s180.0Request timeout in seconds

Store and Search the Vectors

Next Steps

Was this page useful?

Thank you for your feedback! 🙏

We are sorry to hear that. 😔 You can edit this page on GitHub, or create a GitHub issue.