Polish Embedding Models - bge-m3, multilingual-e5 or PolDense? A Test on 900 Questions
RETURN_TO_BLOG
AI & Automation 14 min

Polish Embedding Models - bge-m3, multilingual-e5 or PolDense? A Test on 900 Questions

Paweł Wiszniewski
Paweł Wiszniewski
SEO & GEO Specialist · AI Engineer

An embedding model turns text into a vector of numbers, so RAG, a chatbot or a search engine can find passages similar in meaning to a question, not just ones with the same words. Which one should you pick for Polish text? The rule from my test on 900 real customer questions: if the Gemma licence works for you, take PolDense-150M (81.3 NDCG@10, 149M parameters, runs on an ordinary CPU). If you need an MIT or Apache licence, take multilingual-e5-large (80.7) or bge-m3 (79.7, the most robust to questions without Polish diacritics and with typos). Up to about 1M passages a CPU server is enough; above that you need a GPU or an API. Whatever you choose, add the query prefix the model requires - without it PolDense loses almost 30 points.

Which embedding model should you use for Polish RAG? I tested 10 models and BM25 on 900 real customer questions: results, robustness to missing diacritics and typos, the prefix that costs 30 points, the PIRB ranking, API prices, cost at two scales, licences and code.

/// THIS POST IN SHORT

Polish embeddings - results of a test on 900 questions

In this post I show the results of my test of 10 models and BM25 on Polish customer questions, what happens when people type without Polish diacritics or with a typo, how one forgotten prefix ruins results, what the official Polish PIRB benchmark says, what API and local models cost at two scales, which licences allow commercial use and how to test models on your own data - with code.

Do You Need an Embedding Model at All - the First Decision

The simplest alternative is keyword search (BM25), which you get for free in Postgres or Elasticsearch. In my test BM25 scored 47.7 NDCG@10, and the best embedding model 82.7. The correct passage came first in 33% of cases with BM25 and in 67% with the best model. The reason is simple: a customer asks "how do I send the item back", the documentation says "return procedure", and Polish inflection means "zwrot", "zwrotu" and "zwrocie" are three different words to BM25.

SituationWhat's enough
Search by numbers, product codes, proper namesBM25 or a database filter
Natural-language questions to documentation, FAQs, proceduresAn embedding model
Both kinds of questions in one systemHybrid search: embeddings + BM25

My Test: 10 Models on 900 Customer Questions

How I tested: the Allegro FAQ set from the PolEval 2022 competition - 900 real customer questions and 921 answer passages (50 words on average). The correct passages are known for each question. Each model turned all passages and questions into vectors, and I checked whether the right passage made it to the top of the list. Everything ran on a 4-core Intel Xeon CPU, no GPU, with the prefixes recommended by the model authors (October 2026).

ModelNDCG@10Correct at rank 1Correct passage in top 10ParametersVector dimensionPassages per second (CPU)Licence
OPI-PIB/PolDense-400M82.767.4%96.9%395M10242.4Gemma
OPI-PIB/PolDense-150M81.365.0%96.5%149M7686.7Gemma
intfloat/multilingual-e5-large80.765.9%94.9%560M10244.8MIT
BAAI/bge-m379.764.8%94.1%568M10244.6MIT
OPI-PIB/PolDense-68M79.162.3%94.8%68M51215.6Gemma
sdadas/mmlw-retrieval-roberta-large76.960.9%92.3%435M10246.2Apache 2.0
intfloat/multilingual-e5-base76.059.8%91.5%278M76816.2MIT
sdadas/mmlw-retrieval-roberta-base74.658.1%90.7%124M76818.4Apache 2.0
Qwen/Qwen3-Embedding-0.6B72.656.2%89.2%596M10248.2Apache 2.0
intfloat/multilingual-e5-small72.254.8%90.8%118M38445.6MIT
BM25 (keywords)47.732.6%64.7%----

What follows from this:

  • Polish models win in their weight class. PolDense-68M, with 68M parameters, beats multilingual models eight times its size (mmlw-large, Qwen3-0.6B) and almost matches bge-m3.
  • Popular doesn't mean good in Polish. Qwen3-Embedding-0.6B, often recommended in international rankings, came second to last among the models - on par with the smallest e5.
  • Parameter count says nothing about speed. PolDense-68M is smaller than mmlw-base but slower on a CPU, because it handles texts up to 8,192 tokens and without special acceleration libraries (available mainly on GPUs) it computes more slowly.
  • The gap between first and fifth place is 3.6 points. The choice between them should come from licence, speed and robustness to messy questions, not the ranking alone.

Polish Diacritics and Typos - the Test Nobody Runs

Customers often type "jak zwrocic towar" instead of "jak zwrócić towar". So I checked what happens when I strip Polish diacritics from the same questions (92% of them were affected) or add one typo to a longer word:

ModelCorrect questionsWithout diacriticsWith a typo
PolDense-400M82.781.4 (-1.3)78.6 (-4.1)
PolDense-150M81.377.8 (-3.5)75.5 (-5.8)
multilingual-e5-large80.776.8 (-3.9)76.0 (-4.7)
bge-m379.778.5 (-1.2)77.2 (-2.5)
PolDense-68M79.176.3 (-2.8)72.8 (-6.3)
mmlw-retrieval-roberta-large76.976.0 (-0.9)70.9 (-6.0)
multilingual-e5-base76.071.4 (-4.6)69.8 (-6.2)
multilingual-e5-small72.266.2 (-6.0)67.4 (-4.8)
BM2547.738.3 (-9.4)41.8 (-5.9)

Conclusions: bge-m3 is the most robust - with questions lacking diacritics and with typos it loses the least, so for consumer-facing search it can be a safer choice than a model with a higher score on clean data. BM25 and the smallest models suffer most. Don't strip Polish diacritics from documents "just in case" - the models were trained on correct Polish and you'll only make results worse.

The Prefix Everyone Forgets

Many models require a short prefix before the question: "query: " in e5, "zapytanie: " in mmlw, "[query]: " in PolDense. It's not cosmetic. I measured what happens when you leave it out:

ModelWith prefixWithout prefixDifference
PolDense-150M81.351.4-29.9
PolDense-68M79.151.0-28.1
PolDense-400M82.756.0-26.7
mmlw-retrieval-roberta-base74.667.7-6.9
mmlw-retrieval-roberta-large76.971.7-5.2
multilingual-e5-large80.778.1-2.6
bge-m379.7not required-

Without the prefix, the best model in the test drops almost to the level of plain keyword search. This error doesn't crash the application or show a message - results are just worse and nobody knows why. So keep the prefix in one place in the code together with the model name, and add it only to questions, not documents (unless the model also requires a document prefix, like e5 with "passage: ").

embed_polish.py
from sentence_transformers import SentenceTransformerMODEL = "OPI-PIB/PolDense-150M"QUERY_PREFIX = "[query]: "  # changing the model? change the prefix toomodel = SentenceTransformer(MODEL, device="cpu")def embed_documents(texts: list[str]):    return model.encode(texts, batch_size=16, normalize_embeddings=True)def embed_query(text: str):    return model.encode(QUERY_PREFIX + text, normalize_embeddings=True)docs = ["Zwrot towaru możesz zgłosić w ciągu 14 dni w zakładce Moje zakupy.",        "Fakturę VAT pobierzesz po zaksięgowaniu płatności."]D = embed_documents(docs)q = embed_query("jak oddac towar ktory mi nie pasuje")print(docs[int((D @ q).argmax())])

The PIRB Ranking - the Bigger Picture Across 41 Tasks

My test covers one type of data: short FAQ answers. A broader view comes from the Polish Information Retrieval Benchmark (PIRB), maintained by Poland's National Information Processing Institute (OPI): 41 tasks, from law and medicine to questions from Polish web portals. Selected results (average NDCG@10, as of 9 October 2026):

ModelPIRB NDCG@10ParametersLicence
PolDense-1B64.111.0BGemma
llama-embed-nemotron-8b63.737.5Bnon-commercial only
bge-multilingual-gemma263.269.2BGemma
PolDense-400M63.21395MGemma
PolDense-150M61.07149MGemma
Qwen3-Embedding-8B60.937.6BApache 2.0
PolDense-68M59.0168MGemma
mmlw-retrieval-roberta-large58.46435MApache 2.0
multilingual-e5-large57.29560MMIT
bge-m355.98568MMIT
multilingual-e5-base53.12278MMIT
Qwen3-Embedding-0.6B50.71596MApache 2.0
BM2541.85--

The order in PIRB is similar to mine: PolDense models at the top, e5-large ahead of bge-m3, and Qwen3-0.6B far behind. The difference is the scale of the scores - PIRB includes many harder tasks (e.g. law), so averages are lower. A curiosity from PIRB: the hybrid variant of bge-m3 (55.75) scored slightly lower than the dense variant alone (55.98), so "hybrid always helps" is a myth - you have to check it on your own data.

API Models - OpenAI, Gemini, Voyage

API models aren't in PIRB, and without keys I couldn't test them fairly, so I don't give Polish results for them. Prices (October 2026):

ModelPrice per million tokensNotes
OpenAI text-embedding-3-small$0.021536 dimensions, can be shortened
OpenAI text-embedding-3-large$0.133072 dimensions, can be shortened
Google Gemini Embedding 2$0.20 ($0.10 in batch mode)Free on the free tier
Voyage voyage-4-lite$0.02200M tokens free
Voyage voyage-4$0.06200M tokens free
Voyage voyage-4-large$0.12200M tokens free

An API makes sense when you don't want to run a server and the data can go to the provider under business terms. Before choosing, run them through the same test I show below - testing on 1,000 passages costs fractions of a cent.

What It Costs at Two Scales

Assumption: passages of about 300 tokens and questions of about 20 tokens.

ScaleAPI (text-embedding-3-small)API (Gemini Embedding 2)Local on CPU (PolDense-68M)Vector storage
50k passages, 30k questions a monthabout $0.30 one-off + pennies a monthabout $3 + about $0.12 a monthabout 1 hour of compute on a server you already haveabout 0.1-0.3 GB
10M passages, 3M questions a monthabout $60 + about $1.20 a monthabout $300-600 + about $12 a monthabout 7 days on 4 CPU cores - a GPU is needed20-123 GB depending on the dimension

The key conclusion: computing embeddings through an API is cheap. The real costs are elsewhere:

  • Vector database memory. 10M vectors with 3072 dimensions (text-embedding-3-large) take about 123 GB, with 512 dimensions (PolDense-68M) about 20 GB. That's a difference in the database bill every month. More on database costs in the vector database comparison.
  • Switching models. Vectors from different models aren't compatible, so a model switch means recomputing the whole index. Keep the source texts and plan for it up front - I cover this in the post on AI vendor lock-in.
  • Privacy. A local model sends documents nowhere, which simplifies GDPR compliance for personal data.

Licences - What's Allowed in a Company

LicenceModelsCommercial use
MITmultilingual-e5, bge-m3Yes, no conditions beyond keeping the licence notice
Apache 2.0mmlw-retrieval, Qwen3-EmbeddingYes, keeping the licence notice and change notices
Gemma Terms of UsePolDense, bge-multilingual-gemma2Yes, but Google's prohibited use policy applies, and when distributing the model you must pass the terms on
Non-commercialllama-embed-nemotron-8bNo

If your legal team prefers the simplest licence, pick e5-large or bge-m3 - you lose 1-2 points against PolDense-150M, but you get MIT.

How to Test Models on Your Own Data

A ranking is a starting point, not a decision. Collect 50-200 real questions and mark for each the passage that should be found. Then run a script like this for 2-3 candidates:

test_embeddings.py
from sentence_transformers import SentenceTransformerCANDIDATES = {"OPI-PIB/PolDense-150M": "[query]: ", "BAAI/bge-m3": "",              "intfloat/multilingual-e5-large": "query: "}DOC_PREFIX = {"intfloat/multilingual-e5-large": "passage: "}def evaluate(name, qprefix, docs, questions, correct):    m = SentenceTransformer(name, device="cpu")    D = m.encode([DOC_PREFIX.get(name, "") + d for d in docs], normalize_embeddings=True)    Q = m.encode([qprefix + q for q in questions], normalize_embeddings=True)    top10 = (Q @ D.T).argsort(axis=1)[:, ::-1][:, :10]    hit1 = sum(top[0] == c for top, c in zip(top10, correct)) / len(correct)    hit10 = sum(c in top for top, c in zip(top10, correct)) / len(correct)    return round(100 * hit1, 1), round(100 * hit10, 1)# docs: list of passages, questions: list of questions, correct: index of the right passage for each question# for name, prefix in CANDIDATES.items(): print(name, evaluate(name, prefix, docs, questions, correct))

Add questions without Polish diacritics and with typos to the set - the way your users actually type. The "correct passage in top 10" score tells you whether RAG has a chance to answer well, because the language model usually gets several top passages, not just the first.

The Most Common Mistakes

  • No query prefix - up to 30 points lost in my test, with no error message at all.
  • Choosing a model from an English ranking. MTEB and international rankings don't tell you how a model handles Polish.
  • Stripping Polish diacritics from documents. The models were trained on correct text, so you only make results worse.
  • Mixing vectors from two models in one index. After a model switch, recompute everything.
  • Passages too long for 512-token models. e5 and mmlw truncate longer text without warning, so the end of a document never reaches the vector. Split texts into shorter passages or choose a model with a longer limit (bge-m3, PolDense - up to 8,192 tokens).

---

I help companies build search and RAG on Polish documents - from choosing an embedding model and testing it on your own data to the vector database and deployment. I do this as part of AI app development and AI automation. Get in touch - I'll start by testing 2-3 models on a sample of your questions.

Worth reading next:

/// AUTHOR
Paweł Wiszniewski – AI & Web Engineer

Paweł Wiszniewski

SEO & GEO Specialist & AI Engineer

SEO/GEO specialist (10 years) and AI engineer (3 years). I build search visibility, AI systems and automations that reduce costs and improve operational efficiency.

Signal received?

Terminate
Silence

Initiate protocol. Establish connection. Let's build something loud.

> WAITING_FOR_INPUT...