File size: 19,156 Bytes
6bd0ed7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72db0c2
 
 
6bd0ed7
 
 
 
 
 
 
 
 
 
 
 
 
11cc2a0
 
6bd0ed7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11cc2a0
6bd0ed7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11cc2a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06576f5
11cc2a0
 
06576f5
11cc2a0
 
 
 
 
 
 
6bd0ed7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
---
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