LocalDoc/colbert-az
ColBERT-AZ
A late-interaction retrieval model for Azerbaijani built on top of mmBERT-base-en-az. Trained via cross-encoder distillation from bge-reranker-v2-m3 on a mix of native Azerbaijani and translated retrieval data.
ColBERT-AZ uses late interaction (token-level MaxSim scoring) rather than dense single-vector retrieval, providing higher precision in retrieval compared to bi-encoder models of similar or larger size.
Model Details
Training
Data
ColBERT-AZ was trained on 3 million triplets sampled from a weighted mix of four reranked datasets:
All datasets include reranker scores from bge-reranker-v2-m3, used as teacher signal for knowledge distillation.
Recipe
Loss
Combined KL distillation + InfoNCE:
L = α × KL(softmax(student_scores) || softmax(teacher_scores)) + (1 − α) × InfoNCEwhere α = 0.7 and student scores are computed via MaxSim over [pos, neg1, ..., negK].
Evaluation
Held-out validation
Evaluated on 4,500 held-out triplets (1,500 per native source). Each query is ranked among 1 positive and 8 hard negatives.
AZ-MIRAGE benchmark
Evaluated on the AZ-MIRAGE retrieval benchmark (7,373 queries, 40,448 document pool):
Comparison with bi-encoder models on AZ-MIRAGE:
Usage
This repository contains:
config.json,model.safetensors,tokenizer.*— encoder backbone (mmBERT-base-en-az)projection.pt— ColBERT linear projection layer (768 → 128, no bias)
ColBERT requires both the backbone and the projection layer for correct inference.
Loading the model
import torch
import torch.nn as nn
import torch.nn.functional as F
from transformers import AutoTokenizer, AutoModel
class ColBERT(nn.Module):
def __init__(self, model_name: str, embedding_dim: int = 128):
super().__init__()
self.backbone = AutoModel.from_pretrained(model_name)
self.projection = nn.Linear(self.backbone.config.hidden_size, embedding_dim, bias=False)
@torch.no_grad()
def encode(self, input_ids, attention_mask, keep_mask=None):
out = self.backbone(input_ids=input_ids, attention_mask=attention_mask, return_dict=True)
emb = self.projection(out.last_hidden_state)
emb = F.normalize(emb, p=2, dim=-1)
eff_mask = attention_mask if keep_mask is None else attention_mask * keep_mask
emb = emb * eff_mask.unsqueeze(-1).float()
return emb, eff_mask
# Load
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tokenizer = AutoTokenizer.from_pretrained("LocalDoc/colbert-az")
# Add ColBERT special tokens
tokenizer.add_special_tokens({"additional_special_tokens": ["[Q]", "[D]"]})
model = ColBERT("LocalDoc/colbert-az")
model.backbone.resize_token_embeddings(len(tokenizer))
# Load projection layer
from huggingface_hub import hf_hub_download
proj_path = hf_hub_download(repo_id="LocalDoc/colbert-az", filename="projection.pt")
model.projection.load_state_dict(torch.load(proj_path, map_location="cpu"))
model = model.to(device).eval()Encoding queries and documents
# Tokenization helpers
def tokenize_query(text: str, max_len: int = 32):
text = f"[Q] {text}"
enc = tokenizer(text, padding="max_length", truncation=True,
max_length=max_len, return_tensors="pt")
# ColBERT trick: replace pad with mask for query expansion
pad_mask = enc["input_ids"] == tokenizer.pad_token_id
enc["input_ids"][pad_mask] = tokenizer.mask_token_id
enc["attention_mask"] = torch.ones_like(enc["input_ids"])
return enc
def tokenize_doc(text: str, max_len: int = 256):
text = f"[D] {text}"
return tokenizer(text, padding=True, truncation=True,
max_length=max_len, return_tensors="pt")
# Compute MaxSim score between query and a single document
def maxsim_score(query: str, document: str) -> float:
q_enc = {k: v.to(device) for k, v in tokenize_query(query).items()}
d_enc = {k: v.to(device) for k, v in tokenize_doc(document).items()}
q_emb, _ = model.encode(q_enc["input_ids"], q_enc["attention_mask"])
d_emb, d_mask = model.encode(d_enc["input_ids"], d_enc["attention_mask"])
# MaxSim: for each query token, take max similarity over doc tokens, then sum
sim = torch.einsum("qld,bnd->qlbn", q_emb, d_emb)
sim = sim.masked_fill(~d_mask.unsqueeze(0).unsqueeze(0).bool(), float("-inf"))
max_per_token, _ = sim.max(dim=-1)
score = max_per_token.sum(dim=1).item()
return score
# Example
query = "Azərbaycan mədəniyyətinin tarixi"
doc = "Azərbaycan mədəniyyəti zəngin tarixə malikdir və qədim dövrlərdən başlayaraq inkişaf edib."
print(f"Score: {maxsim_score(query, doc):.4f}")Recommended retrieval pipeline
For production retrieval, use ColBERT-AZ with a proper indexing library that supports late interaction:
These libraries handle efficient indexing, scalable MaxSim retrieval, and quantization for production deployment.
Citation
@misc{colbert-az-2026,
title = {ColBERT-AZ: Late-Interaction Retrieval for Azerbaijani},
author = {LocalDoc},
year = {2026},
url = {https://huggingface.co/LocalDoc/colbert-az}
}License
Apache 2.0
Acknowledgements
- Built on top of mmBERT-base-en-az
- Distilled from bge-reranker-v2-m3
- Original ColBERT architecture: Khattab & Zaharia (2020), ColBERT: Efficient and Effective Passage Search via Contextualized Late Interaction over BERT
- ColBERTv2 distillation methodology: Santhanam et al. (2022), ColBERTv2: Effective and Efficient Retrieval via Lightweight Late Interaction
