Sentence Similarity
Safetensors
sentence-transformers
Italian
pylate
modernbert
colbert
late-interaction
italian
retrieval
information-retrieval
rag
multi-vector
text-embeddings-inference
Instructions to use enricollen/ItColBERT with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- sentence-transformers
How to use enricollen/ItColBERT with sentence-transformers:
from sentence_transformers import SentenceTransformer model = SentenceTransformer("enricollen/ItColBERT") sentences = [ "Questa è una persona felice", "Questo è un cane felice", "Questa è una persona molto felice", "Oggi è una giornata di sole" ] embeddings = model.encode(sentences) similarities = model.similarity(embeddings, embeddings) print(similarities.shape) # [4, 4] - Notebooks
- Google Colab
- Kaggle
| language: | |
| - it | |
| library_name: pylate | |
| tags: | |
| - colbert | |
| - late-interaction | |
| - sentence-transformers | |
| - italian | |
| - retrieval | |
| - information-retrieval | |
| - rag | |
| - multi-vector | |
| pipeline_tag: sentence-similarity | |
| license: apache-2.0 | |
| base_model: nickprock/Italian-ModernBERT-base-embed-mmarco-mnrl | |
| # ItColBERT | |
| <div align="center"> | |
| <img src="assets/banner.png" alt="ItColBERT banner" width="550"> | |
| </div> | |
| **A monolingual Italian late-interaction retriever.** Built with | |
| [PyLate](https://github.com/lightonai/pylate) on top of an Italian ModernBERT | |
| backbone, for semantic search and RAG over Italian text. | |
| *Built and maintained by Enrico Nello. Training code, evaluation harness, and | |
| full development history: [github.com/enricollen/it-colbert](https://github.com/enricollen/it-colbert).* | |
| ## TL;DR | |
| - **What:** ColBERT-style multi-vector retriever, specialized on Italian. | |
| - **Why it exists:** as far as I could find, there was no *Italian-only* | |
| late-interaction retriever — see "Why I built this" below. | |
| - **Size:** ModernBERT-base backbone (~135M parameters), 128-dim token vectors — | |
| the smallest model in the comparison table below, by a wide margin. | |
| - **Best at:** short-to-medium Italian passages (search, RAG chunks, FAQ | |
| retrieval). Weakest at long documents unless you use the chunking recipe | |
| below. | |
| - **Not:** multilingual, and not the strongest late-interaction model overall | |
| — see [How it compares](#how-it-compares). | |
| ## Why I built this | |
| Before starting, I looked for an Italian late-interaction model and mostly | |
| found two things: multilingual late-interaction models that *include* Italian | |
| among many languages ([`jina-colbert-v2`](https://huggingface.co/jinaai/jina-colbert-v2), | |
| [`mLateOn`](https://huggingface.co/lightonai/mLateOn), [`ColBERT-XM`](https://huggingface.co/antoinelouis/colbertxm), | |
| [`SauerkrautLM-Multi-ModernColBERT`](https://huggingface.co/VAGOsolutions/SauerkrautLM-Multi-ModernColBERT)), | |
| and strong Italian dense embedding models that give up ColBERT's token-level | |
| matching for a single vector per passage. Nothing I found combined the two: | |
| an Italian-specialized model that keeps late interaction. That gap is what | |
| made me want to try building one — not "the first Italian ColBERT" (it | |
| isn't, the models above already cover Italian), but the first one that's | |
| *specialized* on it rather than one language among many. | |
| ## What is late interaction, in plain terms? | |
| Most retrieval embedding models compress a whole passage into a single | |
| vector, so a search is one comparison per document. ColBERT-style models | |
| instead keep one small vector **per token**, and score a document by finding | |
| the best-matching document token for every query token (MaxSim), summing the | |
| result. That keeps fine-grained lexical detail — rare names, specific | |
| numbers, exact phrasing — that gets blurred away when everything is squeezed | |
| into one vector. It costs more storage (many vectors instead of one) in | |
| exchange for that precision. | |
| ## What this is for | |
| - Building a RAG retrieval stage over Italian documents. | |
| - Semantic search over Italian text where exact wording/entities matter, not | |
| just topic similarity. | |
| - Reranking a first-stage retriever's candidates. | |
| **Not** intended for: cross-lingual retrieval (query in one language, | |
| documents in another — this model wasn't trained for it), or as a drop-in | |
| replacement for large multilingual dense embedders when Italian isn't the | |
| only language in your corpus. | |
| ## Quickstart | |
| ```bash | |
| pip install -U pylate | |
| ``` | |
| ### Reranking a short candidate list | |
| The simplest usage — no index needed, good for reranking a first-stage | |
| retriever's top results: | |
| ```python | |
| from pylate import rank, models | |
| model = models.ColBERT(model_name_or_path="enricollen/ItColBERT") | |
| queries = ["Qual è la capitale d'Italia?"] | |
| documents = [[ | |
| "Roma è la capitale d'Italia.", | |
| "Milano è la capitale economica del Paese.", | |
| "Napoli è una città del sud Italia.", | |
| ]] | |
| documents_ids = [[1, 2, 3]] | |
| queries_embeddings = model.encode(queries, is_query=True) | |
| documents_embeddings = model.encode(documents, is_query=False) | |
| reranked = rank.rerank( | |
| documents_ids=documents_ids, | |
| queries_embeddings=queries_embeddings, | |
| documents_embeddings=documents_embeddings, | |
| ) | |
| print(reranked) | |
| # [[{'id': 1, 'score': 31.682}, {'id': 2, 'score': 31.552}, {'id': 3, 'score': 31.454}]] | |
| # one list per query, sorted highest score first — "Roma" wins, as expected. | |
| ``` | |
| ### Indexing a larger corpus | |
| For anything beyond a handful of documents per query, build a persistent | |
| index instead of reranking in memory every time: | |
| ```python | |
| from pylate import indexes, models, retrieve | |
| model = models.ColBERT(model_name_or_path="enricollen/ItColBERT") | |
| index = indexes.PLAID( | |
| index_folder="pylate-index", | |
| index_name="index", | |
| override=True, | |
| ) | |
| documents_ids = ["1", "2", "3"] | |
| documents = ["document 1 text", "document 2 text", "document 3 text"] | |
| documents_embeddings = model.encode( | |
| documents, batch_size=32, is_query=False, show_progress_bar=True, | |
| ) | |
| index.add_documents(documents_ids=documents_ids, documents_embeddings=documents_embeddings) | |
| retriever = retrieve.ColBERT(index=index) | |
| queries_embeddings = model.encode( | |
| ["a query"], batch_size=32, is_query=True, show_progress_bar=True, | |
| ) | |
| results = retriever.retrieve(queries_embeddings=queries_embeddings, k=10) | |
| print(results) | |
| # [[{'id': '1', 'score': 30.81}, {'id': '2', 'score': 30.80}, {'id': '3', 'score': 30.78}]] | |
| # same shape as reranking above — list per query, sorted by score — but drawn | |
| # from a persistent index instead of the in-memory documents you pass in. | |
| ``` | |
| Reload an existing index later without re-encoding anything: | |
| ```python | |
| index = indexes.PLAID(index_folder="pylate-index", index_name="index") | |
| ``` | |
| **Documents longer than 512 tokens are truncated at index time.** If your | |
| corpus has long documents (articles, reports, legal text), split them into | |
| ~2,000-character overlapping chunks, index each chunk separately, and take | |
| the max score per source document — see [Evaluation](#evaluation) for why | |
| this matters and how much it recovers. | |
| ## Model details | |
| | | | | |
| |---|---| | |
| | Base model | [`nickprock/Italian-ModernBERT-base-embed-mmarco-mnrl`](https://huggingface.co/nickprock/Italian-ModernBERT-base-embed-mmarco-mnrl) | | |
| | Architecture | ModernBERT backbone (~135M params) → dense projection → 128-dim token vectors | | |
| | Similarity | MaxSim (late interaction) | | |
| | Query length | 32 tokens | | |
| | Document length | 512 tokens (see the chunking note above for longer documents) | | |
| | Language | Italian only | | |
| | License | Apache 2.0 | | |
| | Trained on | One RTX 3090 (24GB), Intel Core i7-14700K, 32GB RAM (27GB usable under WSL2) — no cluster | | |
| ``` | |
| ColBERT( | |
| (0): Transformer({'max_seq_length': 31, 'architecture': 'ModernBertModel'}) | |
| (1): Dense({'in_features': 768, 'out_features': 128, 'bias': False}) | |
| ) | |
| ``` | |
| ## Training recipe | |
| Two stages, following the [ColBERT-Zero](https://huggingface.co/blog/lightonai/colbert-zero) | |
| result that starting from a retrieval-capable checkpoint and running | |
| supervised contrastive + distillation reaches ~99% of full multi-vector | |
| pretraining at roughly a tenth of the cost — which is why this doesn't start | |
| from a raw language model. | |
| **1. Supervised contrastive** (`CachedContrastive`, temperature 0.02), on: | |
| - [`unicamp-dl/mmarco`](https://huggingface.co/datasets/unicamp-dl/mmarco) — Italian triples | |
| - [`hotchpotch/mmarco-hard-negatives-reranker-filtered`](https://huggingface.co/datasets/hotchpotch/mmarco-hard-negatives-reranker-filtered) — reranker-mined hard negatives | |
| - [`nickprock/it-wiki-retrieval-synthetic-hn`](https://huggingface.co/datasets/nickprock/it-wiki-retrieval-synthetic-hn) | |
| - [`yuri-no/miracl-ita-argos`](https://huggingface.co/datasets/yuri-no/miracl-ita-argos) and [`yuri-no/squad-ita`](https://huggingface.co/datasets/yuri-no/squad-ita) — community machine translations, added to widen the mix past machine-translated mMARCO | |
| **2. Knowledge distillation** (`Distillation`, KL) from a single cross-encoder | |
| teacher — [`lightonai/embeddings-fine-tuning-filtered-it`](https://huggingface.co/datasets/lightonai/embeddings-fine-tuning-filtered-it) | |
| (`mxbai-rerank-large-v2` scores) — sample budget spread proportionally across | |
| all 8 splits. | |
| Checkpoints were selected on pooled MLDR-it nDCG@10 and mMARCO-it MRR@10, not | |
| on hold-out KD KL divergence — that metric measures how closely the student | |
| copies the teacher's opinion on the teacher's own data, not whether retrieval | |
| actually improved, and in an earlier run it kept climbing while real | |
| retrieval quality fell. | |
| Everything here — training, benchmarking, significance testing — ran on a | |
| single consumer GPU, no cluster. That budget shaped some choices directly: | |
| batch/mini-batch sizes, and the fact that chunked long-document evaluation | |
| (~26GB host RAM) runs right at this machine's ceiling. | |
| ## How I got here | |
| The short version of the road to this checkpoint, including the parts that | |
| didn't work — because a model card that only shows the winning run isn't | |
| telling the whole story: | |
| 1. **Start from a model that already retrieves**, rather than a raw language | |
| model — the ColBERT-Zero efficiency result above. | |
| 2. **Broaden the training data, then distil.** The starting checkpoint only | |
| knew machine-translated mMARCO. I added more varied Italian data and | |
| applied a second distillation stage. This produced the model in this | |
| repository — the strongest Italian-specialized late-interaction model I | |
| could benchmark it against, except on long documents, which was its | |
| weakest result. | |
| 3. **The long-document weakness turned out to be mostly mechanical.** My | |
| hardest benchmark's documents run a few thousand words each, and were | |
| being cut off at 512 tokens during indexing — discarding roughly 80% of | |
| the average document before the model ever saw it. Splitting documents | |
| into overlapping chunks at query time, with **no retraining at all**, | |
| recovered most of that gap (see the two MLDR-it rows below). | |
| 4. **Two follow-up training attempts, both tested, neither beat that free | |
| fix.** I mined harder training examples from the model's own predictions | |
| — no improvement, and a small real drop in generalization. I then trained | |
| a second version to read twice as much text per document instead of | |
| relying on chunking — statistically no better than chunking a normally | |
| -trained model, once compared fairly on held-out data. | |
| 5. **What shipped.** Since neither training attempt beat "train normally, | |
| then chunk long documents at query time," that's what's in this repo: | |
| this model, plus the chunking recipe for anything longer than 512 tokens. | |
| ## Evaluation | |
| Four Italian retrieval benchmarks. 95% confidence intervals in brackets — | |
| treat any gap smaller than the interval width as noise, not a real | |
| difference. | |
| | Benchmark | Metric | Score (95% CI) | In-domain? | | |
| |---|---|---|---| | |
| | MLDR-it (test, ~10k docs) | nDCG@10 | 0.4008 [0.3404, 0.4589] | No — the clean out-of-domain test | | |
| | MLDR-it, **chunked at query time** | nDCG@10 | 0.4610 [0.4002, 0.5212] | No | | |
| | mMARCO-it (dev, pooled 100k) | MRR@10 (rank only) | 0.7196 [0.7104, 0.7291] | Partially — mMARCO is in the training mix | | |
| | MIRACL-ita (dev, pooled) | nDCG@10 | 0.7194 [0.6984, 0.7375] | Partially — different split of a training source | | |
| | SQuAD-ita (test, pooled) | nDCG@10 | 0.9026 [0.8974, 0.9075] | Partially — different split of a training source | | |
| **MLDR-it is the only clean out-of-domain benchmark, so weigh it most.** Its | |
| documents run a median ~2,700 tokens against this model's 512-token index — | |
| almost all of it is truncated by default. Splitting each document into | |
| 2,000-character overlapping chunks and max-pooling scores at query time (no | |
| retraining, ~7× the indexing cost) recovers most of the gap. Use the | |
| truncated number if index size/latency is the constraint, the chunked number | |
| if document coverage matters more. | |
| ### How it compares | |
| Same protocol, real numbers, paired-bootstrap significance tested against | |
| this model. A **†** marks a score that is *not* statistically distinguishable | |
| from ItColBERT (p > .05) — read those as ties, not losses or wins, regardless | |
| of which number is higher. | |
| | Model | MLDR-it (nDCG@10) | mMARCO-it (MRR@10) | MIRACL-ita (nDCG@10) | SQuAD-ita (nDCG@10) | | |
| |---|---|---|---|---| | |
| | **ItColBERT (this model)** | **0.4008** (0.4610 chunked) | **0.7196** | **0.7194** | **0.9026** | | |
| | mLateOn | 0.4623 | 0.8207 | 0.7880 | 0.9480 | | |
| | jina-colbert-v2 | 0.3858 † | 0.8389 | 0.7755 | 0.8849 | | |
| | bge-m3 (dense) | 0.4531 | 0.7812 | 0.7566 | 0.8247 | | |
| | multilingual-e5-large (dense) | 0.4310 † | 0.8239 | 0.7653 | 0.8513 | | |
| | SauerkrautLM-Multi-ModernColBERT | 0.3122 | 0.5342 | 0.5996 | 0.8338 | | |
| | ColBERT-XM | 0.2734 | 0.6654 | 0.6260 | 0.8558 | | |
| | BM25 | 0.4850 (vs. ItColBERT's 0.4610 chunked: † ) | 0.5715 | 0.5516 | 0.8262 | | |
| Reading the MLDR-it column: against BM25's 0.4850, ItColBERT's plain | |
| 512-token number (0.4008) loses significantly — but that's comparing unequal | |
| document access, since BM25 reads the whole document and ItColBERT reads the | |
| first 512 tokens of it. Once ItColBERT is allowed to read the same amount of | |
| each document (the 0.4610 chunked number), the two are a statistical tie. | |
| In plain terms: this is the strongest **Italian-specialized** | |
| late-interaction model I could find and benchmark against, and it beats most | |
| general-purpose late-interaction alternatives outright. It doesn't beat the | |
| single strongest multilingual late-interaction model I tested (mLateOn), and | |
| it doesn't beat large multilingual dense embedders on most benchmarks — | |
| matching those was never the goal; they're a different, much larger model | |
| class. | |
| ### Quality per parameter | |
| Worth stating plainly: this is also the smallest model in the whole | |
| comparison, by a wide margin. | |
| | Model | Parameters | MLDR-it (nDCG@10) | | |
| |---|---|---| | |
| | **ItColBERT** | **~135M** | **0.4008** (0.4610 chunked) | | |
| | SauerkrautLM-Multi-ModernColBERT | 149M | 0.3122 | | |
| | ColBERT-XM | 277M | 0.2734 | | |
| | mLateOn | 307M | 0.4623 | | |
| | multilingual-e5-large (dense) | 560M | 0.4310 † | | |
| | bge-m3 (dense) | 568M | 0.4531 | | |
| | jina-colbert-v2 | ~0.6B | 0.3858 † | | |
| At roughly a quarter to a sixth the size of the \~560M-parameter multilingual | |
| giants, ItColBERT beats `SauerkrautLM-Multi-ModernColBERT` (the same size | |
| class) and `ColBERT-XM` (2× the parameters) outright, and statistically ties | |
| `jina-colbert-v2` (\~4.4× the parameters) on the primary out-of-domain | |
| benchmark. `mLateOn` is the one model that beats it outright while also being | |
| smaller than the dense giants — included here rather than left out, since | |
| citing only the flattering comparisons would defeat the point of this | |
| section. Fewer parameters also means a smaller index and cheaper inference, | |
| which is part of why training and evaluating this entirely on one consumer | |
| GPU was practical in the first place. | |
| **Protocol notes that matter for these numbers:** | |
| - All late-interaction models above are indexed at the same document length | |
| unless a row is marked "chunked". | |
| - Pooled corpora inflate absolute scores; use them for relative ranking, not | |
| as numbers comparable to published full-corpus results. | |
| - MLDR-it has 200 queries — differences under ~0.03 nDCG@10 are inside the | |
| noise. Don't read a ranking claim as established without a significance | |
| test behind it. | |
| - MIRACL-ita and SQuAD-ita are **community machine translations**, not | |
| official resources, and both overlap this model's training data source | |
| (different splits, checked for direct query leakage — none found). | |
| ## Limitations | |
| - Italian only. Cross-lingual retrieval isn't the goal here. | |
| - Much of the training data derives from machine-translated mMARCO, and it | |
| shows: short-passage retrieval is the strongest result, long-document | |
| retrieval the weakest, even after the chunking fix. | |
| - Documents longer than 512 tokens truncate at index time unless you chunk | |
| and max-pool (see Quickstart and Evaluation above). | |
| - Multi-vector indexes are larger than single-vector dense indexes, and | |
| chunking multiplies that further — budget accordingly for large corpora. | |
| - Behind the strongest multilingual late-interaction model I tested | |
| (mLateOn) on every benchmark, and behind large multilingual dense | |
| embedders on most — see "How it compares". | |
| - Not evaluated for bias or harmful content. Usual caveats apply for a model | |
| trained substantially on machine-translated web and QA data. | |
| ## What's next | |
| Not done yet, in rough priority order: | |
| - Fusing this model's rankings with BM25 (rank fusion) — cheap, and the | |
| chunked MLDR-it numbers above suggest it should help, since the two | |
| disagree on individual queries while scoring about the same overall. | |
| - Re-running the comparison table above with every model chunked, not just | |
| this one, for a fully apples-to-apples long-document comparison. | |
| - A smaller 64-dimension variant, for when index size matters more than the | |
| last points of quality — planned, not trained yet. | |
| - An MTEB-Italian-style community benchmark check, if/when suitable | |
| multi-vector support exists in that harness. | |
| ## Citation | |
| If you use this model, please cite it, plus ColBERT/ColBERTv2, PyLate, | |
| mMARCO, and MLDR below. | |
| ```bibtex | |
| @misc{nello2026itcolbert, | |
| author = {Nello, Enrico}, | |
| title = {ItColBERT: A Monolingual Italian Late-Interaction Retriever}, | |
| year = {2026}, | |
| publisher = {Hugging Face}, | |
| howpublished = {\url{https://huggingface.co/enricollen/ItColBERT}}, | |
| } | |
| ``` | |
| ```bibtex | |
| @inproceedings{DBLP:conf/cikm/ChaffinS25, | |
| author = {Antoine Chaffin and Rapha{\"{e}}l Sourty}, | |
| title = {PyLate: Flexible Training and Retrieval for Late Interaction Models}, | |
| booktitle = {Proceedings of the 34th {ACM} International Conference on Information | |
| and Knowledge Management, {CIKM} 2025, Seoul, Republic of Korea, November | |
| 10-14, 2025}, | |
| pages = {6334--6339}, | |
| publisher = {{ACM}}, | |
| year = {2025}, | |
| url = {https://github.com/lightonai/pylate}, | |
| doi = {10.1145/3746252.3761608}, | |
| } | |
| ``` | |
| Further reading: [ColBERTv2](https://arxiv.org/abs/2112.01488), | |
| [mMARCO](https://arxiv.org/abs/2108.13897), | |
| [MLDR / BGE-M3](https://arxiv.org/abs/2402.03216). | |
| ## Acknowledgments | |
| Built on [`nickprock/Italian-ModernBERT-base-embed-mmarco-mnrl`](https://huggingface.co/nickprock/Italian-ModernBERT-base-embed-mmarco-mnrl) | |
| and [PyLate](https://github.com/lightonai/pylate) (LightOn). Benchmarked | |
| against [`SauerkrautLM-Multi-ModernColBERT`](https://huggingface.co/VAGOsolutions/SauerkrautLM-Multi-ModernColBERT), | |
| [`jina-colbert-v2`](https://huggingface.co/jinaai/jina-colbert-v2), | |
| [`mLateOn`](https://huggingface.co/lightonai/mLateOn), and | |
| [`ColBERT-XM`](https://huggingface.co/antoinelouis/colbertxm) — thank you to | |
| everyone building and sharing these, Italian NLP is a small enough space that | |
| every open checkpoint helps. | |
| ### Framework versions | |
| Python 3.11.15 · Sentence Transformers 5.3.0 · PyLate 1.5.0 · Transformers | |
| 5.3.0 · PyTorch 2.6.0+cu124 · Accelerate 1.14.0 · Datasets 5.0.1 · Tokenizers | |
| 0.22.2 | |