
Polish Embedding Models - bge-m3, multilingual-e5 or PolDense? A Test on 900 Questions
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
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.
| Situation | What's enough |
|---|---|
| Search by numbers, product codes, proper names | BM25 or a database filter |
| Natural-language questions to documentation, FAQs, procedures | An embedding model |
| Both kinds of questions in one system | Hybrid 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).
| Model | NDCG@10 | Correct at rank 1 | Correct passage in top 10 | Parameters | Vector dimension | Passages per second (CPU) | Licence |
|---|---|---|---|---|---|---|---|
| OPI-PIB/PolDense-400M | 82.7 | 67.4% | 96.9% | 395M | 1024 | 2.4 | Gemma |
| OPI-PIB/PolDense-150M | 81.3 | 65.0% | 96.5% | 149M | 768 | 6.7 | Gemma |
| intfloat/multilingual-e5-large | 80.7 | 65.9% | 94.9% | 560M | 1024 | 4.8 | MIT |
| BAAI/bge-m3 | 79.7 | 64.8% | 94.1% | 568M | 1024 | 4.6 | MIT |
| OPI-PIB/PolDense-68M | 79.1 | 62.3% | 94.8% | 68M | 512 | 15.6 | Gemma |
| sdadas/mmlw-retrieval-roberta-large | 76.9 | 60.9% | 92.3% | 435M | 1024 | 6.2 | Apache 2.0 |
| intfloat/multilingual-e5-base | 76.0 | 59.8% | 91.5% | 278M | 768 | 16.2 | MIT |
| sdadas/mmlw-retrieval-roberta-base | 74.6 | 58.1% | 90.7% | 124M | 768 | 18.4 | Apache 2.0 |
| Qwen/Qwen3-Embedding-0.6B | 72.6 | 56.2% | 89.2% | 596M | 1024 | 8.2 | Apache 2.0 |
| intfloat/multilingual-e5-small | 72.2 | 54.8% | 90.8% | 118M | 384 | 45.6 | MIT |
| BM25 (keywords) | 47.7 | 32.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:
| Model | Correct questions | Without diacritics | With a typo |
|---|---|---|---|
| PolDense-400M | 82.7 | 81.4 (-1.3) | 78.6 (-4.1) |
| PolDense-150M | 81.3 | 77.8 (-3.5) | 75.5 (-5.8) |
| multilingual-e5-large | 80.7 | 76.8 (-3.9) | 76.0 (-4.7) |
| bge-m3 | 79.7 | 78.5 (-1.2) | 77.2 (-2.5) |
| PolDense-68M | 79.1 | 76.3 (-2.8) | 72.8 (-6.3) |
| mmlw-retrieval-roberta-large | 76.9 | 76.0 (-0.9) | 70.9 (-6.0) |
| multilingual-e5-base | 76.0 | 71.4 (-4.6) | 69.8 (-6.2) |
| multilingual-e5-small | 72.2 | 66.2 (-6.0) | 67.4 (-4.8) |
| BM25 | 47.7 | 38.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:
| Model | With prefix | Without prefix | Difference |
|---|---|---|---|
| PolDense-150M | 81.3 | 51.4 | -29.9 |
| PolDense-68M | 79.1 | 51.0 | -28.1 |
| PolDense-400M | 82.7 | 56.0 | -26.7 |
| mmlw-retrieval-roberta-base | 74.6 | 67.7 | -6.9 |
| mmlw-retrieval-roberta-large | 76.9 | 71.7 | -5.2 |
| multilingual-e5-large | 80.7 | 78.1 | -2.6 |
| bge-m3 | 79.7 | not 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: ").
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):
| Model | PIRB NDCG@10 | Parameters | Licence |
|---|---|---|---|
| PolDense-1B | 64.11 | 1.0B | Gemma |
| llama-embed-nemotron-8b | 63.73 | 7.5B | non-commercial only |
| bge-multilingual-gemma2 | 63.26 | 9.2B | Gemma |
| PolDense-400M | 63.21 | 395M | Gemma |
| PolDense-150M | 61.07 | 149M | Gemma |
| Qwen3-Embedding-8B | 60.93 | 7.6B | Apache 2.0 |
| PolDense-68M | 59.01 | 68M | Gemma |
| mmlw-retrieval-roberta-large | 58.46 | 435M | Apache 2.0 |
| multilingual-e5-large | 57.29 | 560M | MIT |
| bge-m3 | 55.98 | 568M | MIT |
| multilingual-e5-base | 53.12 | 278M | MIT |
| Qwen3-Embedding-0.6B | 50.71 | 596M | Apache 2.0 |
| BM25 | 41.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):
| Model | Price per million tokens | Notes |
|---|---|---|
| OpenAI text-embedding-3-small | $0.02 | 1536 dimensions, can be shortened |
| OpenAI text-embedding-3-large | $0.13 | 3072 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.02 | 200M tokens free |
| Voyage voyage-4 | $0.06 | 200M tokens free |
| Voyage voyage-4-large | $0.12 | 200M 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.
| Scale | API (text-embedding-3-small) | API (Gemini Embedding 2) | Local on CPU (PolDense-68M) | Vector storage |
|---|---|---|---|---|
| 50k passages, 30k questions a month | about $0.30 one-off + pennies a month | about $3 + about $0.12 a month | about 1 hour of compute on a server you already have | about 0.1-0.3 GB |
| 10M passages, 3M questions a month | about $60 + about $1.20 a month | about $300-600 + about $12 a month | about 7 days on 4 CPU cores - a GPU is needed | 20-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
| Licence | Models | Commercial use |
|---|---|---|
| MIT | multilingual-e5, bge-m3 | Yes, no conditions beyond keeping the licence notice |
| Apache 2.0 | mmlw-retrieval, Qwen3-Embedding | Yes, keeping the licence notice and change notices |
| Gemma Terms of Use | PolDense, bge-multilingual-gemma2 | Yes, but Google's prohibited use policy applies, and when distributing the model you must pass the terms on |
| Non-commercial | llama-embed-nemotron-8b | No |
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:
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:
/// RELATED_SERVICES
Need these concepts implemented? Explore the services related to this topic.
AI App Development
Custom AI software and AI-powered web applications. MVP development, full stack engineering, and AI systems programming from scratch to production.
View serviceServiceAI & Automation
AI agents, process automation with n8n and Make, chatbots, RAG on your own documents and custom LLMs. I build AI systems that cut manual work and run 24/7.
View service/// SOURCES
- 01My test data: PolEval 2022 Allegro FAQ, 900 questions
- 02PIRB - Polish Information Retrieval Benchmark (leaderboard)
- 03Dadas et al. (2024) - PIRB: A Comprehensive Benchmark of Polish Dense and Hybrid Text Retrieval Methods
- 04PL-MTEB: Polish Massive Text Embedding Benchmark (ACL 2026 Findings)
- 05OPI-PIB - PolDense-150M (model card)
- 06sdadas - mmlw-retrieval-roberta-base (model card)
- 07BAAI - bge-m3 (model card)
- 08Wang et al. (2024) - Multilingual E5 Text Embeddings: A Technical Report
- 09OpenAI - API pricing (embeddings)
- 10Google - Gemini API pricing (Gemini Embedding 2)
- 11Voyage AI - Pricing
- 12Google - Gemma Terms of Use
/// RELATED_RECORDS
AI Adoption in Your Team - Why Licences Sit Unused and How to Get People Using AI
According to Gartner, only 5% of companies that finished a Copilot pilot planned a larger rollout, and just 8% of employees use AI daily. Five kinds of resistance and what works, the role of managers, AI champions, measuring adoption at three levels and a 90-day plan.
How to Choose an AI Implementation Partner - 15 Questions, Warning Signs and Contract Clauses
According to MIT NANDA, 95% of AI pilots show no financial impact, yet deployments with partners succeed in 67% of cases. In-house, partner or hybrid, 15 questions to ask, warning signs, contract clauses and a pilot with success criteria.
Human-in-the-Loop in Practice - How to Design Human Oversight of AI Agents
Too little oversight ends in incidents like Replit's deleted database; too much ends in clicking without reading - Claude Code users approve 93% of agent prompts. Four levels of autonomy, a risk and reversibility matrix, the approval card, escalations and AI Act Article 14 in architecture.
Signal received?
Terminate
Silence
Initiate protocol. Establish connection. Let's build something loud.
