Robust Sentence Splitting & Snippet Generation con spaCy

Quando si costruisce un motore di ricerca, un sistema di question answering o un pipeline di indicizzazione semantica, due operazioni tornano sempre: suddividere il testo in frasi (sentence splitting) e generare uno snippet rappresentativo attorno a una parola chiave o a un segmento rilevante. Con spaCy entrambe le operazioni sono robuste, veloci e facilmente personalizzabili.
In questo articolo esploreremo come sfruttare la pipeline di spaCy per implementare uno splitter di frasi affidabile e come costruire, a partire da esso, un generatore di snippet contestuali di qualità.
Cosa vedrai
- Sentence splitting con spaCy
- Personalizzare le regole di segmentazione
- Generare snippet contestuali
- Snippet per motori di ricerca interni
- Pipeline completa: da testo grezzo a snippet
- Risorse utili
Sentence splitting con spaCy
La segmentazione delle frasi (sentence boundary detection, SBD) è uno dei passaggi più delicati delle attività di preprocessing in ambito NLP. Un semplice split(".") fallisce su abbreviazioni, numeri decimali e titoli: ad esempio, la frase Dr. Rossi ha ottenuto un punteggio di 9.5. verrebbe spezzata in tre frammenti che perdono di significato.
spaCy risolve il problema grazie al componente senter (o sentencizer nella versione più leggera), addestrato su corpus reali per gestire correttamente questi casi.
Per prima cosa installiamo spaCy con il modello della lingua desiderata:
pip install spacy
# Modello italiano (large, più accurato)
python -m spacy download it_core_news_lg
# Modello inglese
python -m spacy download en_core_web_lg
Segmentare un testo è immediato:
import spacy
nlp = spacy.load("it_core_news_lg")
testo = """
Dr. Rossi ha ricevuto un punteggio di 9.5 su 10. È un ottimo risultato!
L'azienda Acme S.p.A. ha annunciato ricavi per 3.2 mld €. I dettagli seguiranno.
La conferenza si terrà il 15 dic. 2026 a Milano.
"""
doc = nlp(testo.strip())
for i, sent in enumerate(doc.sents, 1):
print(f"[{i}] {sent.text.strip()}")
Output:
[1] Dr. Rossi ha ricevuto un punteggio di 9.5 su 10.
[2] È un ottimo risultato!
[3] L'azienda Acme S.p.A. ha annunciato ricavi per 3.2 mld €.
[4] I dettagli seguiranno.
[5] La conferenza si terrà il 15 dic. 2026 a Milano.
Le abbreviazioni (Dr., S.p.A., dic.) e i numeri decimali (9.5, 3.2) vengono gestiti correttamente senza alcuna configurazione aggiuntiva.
Usare solo il sentencizer (senza NER e POS)
Se hai testi molto lunghi o risorse limitate e non ti servono le annotazioni linguistiche complete, puoi usare la pipeline minimale con solo il componente sentencizer:
import spacy
from spacy.lang.it import Italian
nlp_fast = Italian()
nlp_fast.add_pipe("sentencizer")
doc = nlp_fast(testo.strip())
frasi = [s.text.strip() for s in doc.sents]
print(frasi)
Il sentencizer si basa su regole euristiche (punteggiatura + maiuscola successiva) ed è molto più veloce, ma meno preciso del modello statistico su testi con molte abbreviazioni.
Personalizzare le regole di segmentazione
A volte le frasi nel tuo corpus hanno caratteristiche particolari: ad esempio testi tecnici con elenchi puntati, pseudocodice o intestazioni Markdown, per cui il modello standard può sbagliare. In questi casi puoi iniettare regole personalizzate tramite un componente dedicato.
Nel seguente esempio impediamo che i trattini di apertura elenco (-, •, –) vengano trattati come inizio di nuova frase se seguono immediatamente un token di fine frase:
import spacy
from spacy.language import Language
nlp = spacy.load("it_core_news_lg")
@Language.component("custom_sentence_rules")
def custom_sentence_rules(doc):
"""
Impedisce che i trattini di apertura elenco (- o •)
vengano trattati come inizio di nuova frase se seguono
immediatamente un token di fine frase.
"""
for token in doc[1:]:
if token.text in ("-", "•", "–") and token.i > 0:
# Non iniziare una nuova frase su questi token
token.is_sent_start = False
return doc
# Inserisco il componente prima del senter per avere precedenza
nlp.add_pipe("custom_sentence_rules", before="senter")
testo_lista = """
Requisiti del progetto:
- Python 3.11 o superiore
- spaCy 3.x
- Un modello linguistico installato
"""
doc = nlp(testo_lista.strip())
for sent in doc.sents:
print(repr(sent.text.strip()))
Generare snippet contestuali
Uno snippet contestuale è un breve estratto di testo (tipicamente 1-3 frasi) che circonda un termine di interesse. Per capirci, è lo stesso testo che vedi sotto ogni risultato in Google o in qualsiasi motore di ricerca.
L’approccio con spaCy per la sua generazione è estremamente elegante: siccome il documento è già segmentato in frasi, basta trovare la frase che contiene il termine e restituire quella frase, opzionalmente con le frasi adiacenti per aumentare il contesto.
import spacy
from dataclasses import dataclass
nlp = spacy.load("it_core_news_lg")
@dataclass
class Snippet:
text: str
sentence_index: int
start_char: int
end_char: int
def generate_snippet(
text: str,
query: str,
context_sentences: int = 1,
max_length: int = 300,
) -> Snippet | None:
"""
Restituisce lo snippet più rilevante attorno alla prima
occorrenza di `query` nel testo.
Args:
text: Il testo sorgente.
query: Il termine (o frase) da cercare.
context_sentences: Numero di frasi adiacenti da includere.
max_length: Lunghezza massima dello snippet in caratteri.
Returns:
Un oggetto Snippet, oppure None se il termine non è trovato.
"""
doc = nlp(text)
sentences = list(doc.sents)
query_lower = query.lower()
# Trova la prima frase contenente il termine
match_idx = next(
(i for i, s in enumerate(sentences) if query_lower in s.text.lower()),
None,
)
if match_idx is None:
return None
# Calcola il range di frasi da includere
start_idx = max(0, match_idx - context_sentences)
end_idx = min(len(sentences), match_idx + context_sentences + 1)
snippet_parts = [s.text.strip() for s in sentences[start_idx:end_idx]]
snippet_text = " ".join(snippet_parts)
# Tronca se supera max_length, cercando di non spezzare parole
if len(snippet_text) > max_length:
snippet_text = snippet_text[:max_length].rsplit(" ", 1)[0] + "…"
return Snippet(
text=snippet_text,
sentence_index=match_idx,
start_char=sentences[match_idx].start_char,
end_char=sentences[match_idx].end_char,
)
# ── Demo ──────────────────────────────────────────────────────────────────────
testo = """
spaCy è una libreria open-source per il Natural Language Processing in Python.
Supporta decine di lingue ed è progettata per l'uso in produzione.
La funzione di sentence splitting divide automaticamente un testo in frasi.
Questa funzionalità è fondamentale per costruire pipeline NLP robuste.
Il componente senter, addestrato su corpus reali, gestisce correttamente
abbreviazioni e casi particolari.
"""
snippet = generate_snippet(testo.strip(), query="sentence splitting", context_sentences=1)
if snippet:
print(f"Snippet trovato (frase {snippet.sentence_index}):")
print(snippet.text)
Output:
Snippet trovato (frase 2):
Supporta decine di lingue ed è progettata per l'uso in produzione. La funzione di sentence splitting divide automaticamente un testo in frasi. Questa funzionalità è fondamentale per costruire pipeline NLP robuste.
Snippet per motori di ricerca interni
Nei motori di ricerca interni (intranet, knowledge base, e-commerce) è spesso utile evidenziare il termine di query all’interno dello snippet con un tag HTML. Ecco un’estensione della funzione precedente che produce output HTML:
import html
import re
def highlight_query(snippet_text: str, query: str) -> str:
"""
Avvolge le occorrenze di `query` in un tag <mark> per l'evidenziazione.
Effettua un'escaping dell'HTML per sicurezza.
"""
safe_text = html.escape(snippet_text)
safe_query = re.escape(html.escape(query))
highlighted = re.sub(
f"({safe_query})",
r"<mark>\1</mark>",
safe_text,
flags=re.IGNORECASE,
)
return highlighted
snippet = generate_snippet(testo.strip(), query="sentence splitting")
if snippet:
html_snippet = highlight_query(snippet.text, "sentence splitting")
print(html_snippet)
Output (semplificato):
Supporta decine di lingue ... La funzione di <mark>sentence splitting</mark> divide ...
Pipeline completa: da testo grezzo a snippet
Mettiamo tutto insieme in una classe SnippetEngine riutilizzabile, che accetta più documenti e restituisce gli snippet per qualsiasi query:
import spacy
from dataclasses import dataclass, field
nlp = spacy.load("it_core_news_lg")
@dataclass
class Document:
id: str
title: str
text: str
sentences: list = field(default_factory=list)
def __post_init__(self):
doc = nlp(self.text)
self.sentences = [s.text.strip() for s in doc.sents if s.text.strip()]
class SnippetEngine:
"""
Motore di snippet su una collezione di documenti.
Pre-processa i documenti una sola volta al momento dell'indicizzazione.
"""
def __init__(self):
self._docs: list[Document] = []
def index(self, document: Document) -> None:
"""Indicizza un documento."""
self._docs.append(document)
def search(
self,
query: str,
context_sentences: int = 1,
max_length: int = 300,
) -> list[dict]:
"""
Cerca la query nei documenti indicizzati e restituisce
una lista di risultati con titolo e snippet.
"""
results = []
query_lower = query.lower()
for doc in self._docs:
match_idx = next(
(i for i, s in enumerate(doc.sentences) if query_lower in s.lower()),
None,
)
if match_idx is None:
continue
start = max(0, match_idx - context_sentences)
end = min(len(doc.sentences), match_idx + context_sentences + 1)
snippet = " ".join(doc.sentences[start:end])
if len(snippet) > max_length:
snippet = snippet[:max_length].rsplit(" ", 1)[0] + "…"
results.append(
{
"id": doc.id,
"title": doc.title,
"snippet": snippet,
"sentence_index": match_idx,
}
)
return results
# ── Demo ──────────────────────────────────────────────────────────────────────
engine = SnippetEngine()
engine.index(Document(
id="doc-1",
title="Introduzione a spaCy",
text=(
"spaCy è una libreria NLP open-source scritta in Python e Cython. "
"È progettata per essere veloce e scalabile in ambienti di produzione. "
"Supporta il sentence splitting, la tokenizzazione e il riconoscimento delle entità."
),
))
engine.index(Document(
id="doc-2",
title="Generazione di snippet per la ricerca",
text=(
"Uno snippet è un breve estratto di testo che mostra il contesto attorno a un termine cercato. "
"I motori di ricerca moderni usano tecniche NLP per generare snippet rilevanti. "
"La qualità degli snippet influenza direttamente il CTR dei risultati di ricerca."
),
))
for result in engine.search("snippet", context_sentences=1):
print(f"[{result['id']}] {result['title']}")
print(f" → {result['snippet']}")
print()
Output:
[doc-1] Introduzione a spaCy
→ È progettata per essere veloce e scalabile in ambienti di produzione. Supporta il sentence splitting, la tokenizzazione e il riconoscimento delle entità.
[doc-2] Generazione di snippet per la ricerca
→ Uno snippet è un breve estratto di testo che mostra il contesto attorno a un termine cercato. I motori di ricerca moderni usano tecniche NLP per generare snippet rilevanti. La qualità degli snippet influenza direttamente il CTR dei risultati di ricerca.
Consiglio pratico: se la tua applicazione deve processare migliaia di documenti, usa
nlp.pipe()al posto di chiamate singole anlp(). Questo consente a spaCy di fare batch processing in modo molto più efficiente, riducendo significativamente i tempi di elaborazione.
# Processamento in batch — molto più efficiente su grandi volumi
texts = [doc.text for doc in raw_documents]
for spacy_doc in nlp.pipe(texts, batch_size=64):
sentences = [s.text.strip() for s in spacy_doc.sents]
# ... indicizza le frasi












