A giugno Loom ha imparato la ricerca semantica, con l’interruttore spento di default. Con le note brevi Loom funziona benissimo. Con i documenti lunghi, come manuali in PDF o documentazione tecnica di centinaia di pagine, veniva fuori un difetto che con le note non si vedeva.
Oggi esce la 0.7, e quasi tutto ruota attorno a una frase: la risposta era a pagina 40.
Il problema: Loom leggeva solo l’inizio
Quando fai una domanda con loom ask, Loom trova i file pertinenti e li manda all’LLM in una sola chiamata. Per non sfondare il contesto ogni file ha un budget: 8000 caratteri, circa 2000 token.
Con una nota di mezza pagina non cambia niente: il file entra tutto. Con un PDF di 60 pagine invece Loom tagliava i primi 8000 caratteri e buttava il resto. Copertina, indice, introduzione. Se la risposta stava al capitolo 7, il modello non la vedeva mai e rispondeva con quello che aveva: l’indice.
Il bello (si fa per dire) è che la ricerca funzionava benissimo. Il file giusto veniva trovato, stava in cima ai risultati, e poi arrivava all’LLM senza la parte che serviva.
La soluzione: l’inizio più il passaggio giusto
Dalla 0.7, quando un file supera il budget, Loom manda due pezzi:
- l’inizio del file (circa 1200 caratteri): titolo, abstract, intro. Servono quando la domanda è “di cosa parla questo documento?”;
- la finestra che risponde meglio alla domanda, presa da qualunque punto del file.
All’LLM arriva una cosa così:
Manuale di installazione – Server XYZ
1. Introduzione
Questo documento descrive...
[...]
7.3 Connection pooling con PgBouncer
Per limitare le connessioni al database impostare pool_mode = transaction...
[...truncated]
Come sceglie la finestra? Niente embeddings e niente chiamate in più. Fa scorrere una finestra sul testo e dà un punteggio a ogni posizione in base ai termini della domanda, con tre accorgimenti:
- I termini rari pesano di più. In un manuale di database la parola “database” compare ovunque e non aiuta a scegliere; “pgbouncer” compare in tre punti e decide lei. È lo stesso principio dell’IDF di BM25, applicato dentro un singolo file.
- Uno stemming rozzo ma efficace: i termini vengono tagliati a 6 lettere, così “ricetta” trova anche “ricette” e “configurazione” trova “configurare”. Non serve classificare documenti, serve solo atterrare vicino alla risposta.
- Un termine ripetuto 200 volte non vince da solo: ogni termine conta al massimo 3 volte per finestra. Meglio una finestra che nomina tutte le parole della domanda una volta, che una che ripete la stessa all’infinito.
Se nessuna finestra contiene i termini della domanda, Loom torna al vecchio comportamento e manda l’inizio del file. Resta una sola chiamata all’LLM, non serve rifare la scansione e non c’è niente da configurare. Vale per la CLI, la GUI e il tool MCP loom_ask.
Unico avviso: sui file lunghi le risposte possono cambiare rispetto a prima. Di solito in meglio, visto che prima il modello rispondeva leggendo l’indice.
loom_grep: quando ti serve la parola esatta
La ricerca di Loom (BM25 o ibrida) risponde a “quali file parlano di questo argomento?”. Ma spesso la domanda è un’altra: “dove compare esattamente questa cosa?”. Un numero di fattura, il nome di un cliente, una data, un codice di errore. La ricerca per rilevanza qui può sbagliare: il file che nomina la fattura una volta, in una tabella, non sarà mai primo in classifica.
Per questo c’è un nuovo tool MCP, loom_grep: un’espressione regolare sul contenuto estratto di tutti i file indicizzati (compreso il testo estratto dai PDF), oppure di un solo file.
loom_grep(pattern="FT-2026-0[0-9]{3}", context=1)
Per ogni riga trovata restituisce il file, il numero di riga, l’offset in byte e, se li chiedi, qualche riga di contesto. Non chiama l’LLM. Per default ignora maiuscole e minuscole e si ferma a 50 risultati (al massimo 500), così un pattern come e non ti rovescia addosso l’intero archivio.
Usa il motore RE2 di Go, che gira in tempo lineare: un pattern scritto male da un agente non può bloccare il processo con il backtracking catastrofico che colpisce altri motori regex.
Leggere solo il pezzo che serve
loom_grep ha senso insieme alla seconda novità: loom_get_file ora accetta offset e length e restituisce un intervallo del file invece di tutto il contenuto.
Messi insieme, permettono a Claude di esplorare una knowledge base come esplora un codebase:
loom_greptrova, per esempio, la riga che nomina un codice di errore all’offset 48.210 di un PDF da 300 KB;loom_get_filelegge solo i 4000 byte attorno a quell’offset;- Claude risponde senza aver caricato nel contesto gli altri 296 KB.
Chi usa Claude Code riconoscerà lo schema: è quello che fa con grep e la lettura di un file a righe. Ora funziona anche sui tuoi documenti.
excerpt_chars: lo stesso taglio anche via HTTP
Chi usa loom-http come livello di retrieval per un’altra applicazione aveva lo stesso problema, spostato di un passo: /search restituisce il contenuto intero di ogni file, e l’applicazione doveva tagliarlo da sé. Di solito all’inizio.
Ora basta passare excerpt_chars:
curl -s localhost:8080/search \
-d '{"query":"orario check-in","limit":3,"excerpt_chars":3000}'
I file lunghi tornano tagliati come fa ask, cioè inizio più passaggio pertinente, a circa 3000 byte, con un campo content_len che dice quanto era lungo il file intero. I file corti tornano interi. Se non passi il parametro, la risposta è identica a prima.
Ho sistemato anche un dettaglio: con budget piccoli l’inizio del file occupa al massimo un quarto dello spazio. Prima, con 1200 byte di budget, sarebbe stato tutto inizio e niente risposta.
.loomignore: decidi tu cosa non indicizzare
Loom salta da sempre i file e le cartelle nascosti (.git, .obsidian…). Oltre a quello non ignora niente di suo, ed è voluto: cosa sia rumore (build, bozze vecchie, export duplicati, l’archivio del 2019) cambia da persona a persona.
Quindi le regole le scrivi tu, con la sintassi che conosci già da Git:
# ~/loom/.loomignore
# qualunque cartella "build", a qualsiasi profondità
build/
# solo la cartella archivio_v1 nella radice
/archivio_v1/
# tutti i file .bak
*.bak
bozze/**/*.old.md
Se preferisci non mettere file dentro una cartella (per esempio un vault di Obsidian), puoi usare la lista ignore in ~/.loom/config.toml, che vale per tutte le cartelle indicizzate:
notes_dirs = ["/Users/tu/Documents/lavoro", "/Users/tu/Obsidian/vault"]
ignore = ["build/", "*.bak"]
Qualche dettaglio pratico:
- le cartelle ignorate non vengono nemmeno attraversate, quindi la scansione è anche più veloce;
- i file già indicizzati che diventano ignorati vengono tolti dall’indice alla scansione successiva, come se li avessi cancellati. Togli la regola e alla scansione dopo tornano;
- un pattern non valido ferma la scansione con un errore che indica file e riga, senza toccare l’indice;
- la negazione (
!pattern) non è supportata: è l’unico pezzo di.gitignoreche manca; - in modalità multi-corpus ogni corpus usa il suo
notes/.loomignore.
I bug che non vedevi
Due correzioni meritano una riga, perché erano del tipo peggiore: quelle silenziose.
Salvare le impostazioni dalla GUI spegneva la ricerca ibrida. Il codice che scriveva il config.toml emetteva sempre il blocco [embeddings] commentato. Bastava aprire le impostazioni della GUI e salvare, per qualunque motivo, perché il tuo enabled = true sparisse. Loom tornava al solo BM25 senza dire niente: nessun errore, solo risultati un po’ peggiori sulle domande in un’altra lingua. Ora il blocco viene riscritto attivo con i tuoi valori. Della chiave API viene scritto solo il nome della variabile d’ambiente, mai la chiave.
/scan via HTTP non diceva cosa era andato storto (corretto nella 0.6.1). Restituiva solo i contatori: {"failed": 1} e arrangiati. Ora restituisce lo stesso elenco di errori della CLI, con file e motivo, e riporta anche i file spostati (stesso contenuto, percorso nuovo), che prima sembravano “non è successo niente”. CLI, MCP e HTTP ora usano lo stesso codice per il resoconto, così non possono più divergere.
Aggiornare
brew upgrade loom
Per Claude Desktop scarica il nuovo pacchetto .mcpb dalla pagina delle release. Non serve rifare la scansione: gli estratti pertinenti, loom_grep e gli intervalli lavorano sull’indice che hai già. Il .loomignore entra in gioco alla prossima loom scan.
Tirando le somme
Gli aggiornamenti di giugno erano funzioni nuove: embeddings, HTTP, multi-corpus. Questo è diverso. Sistema cose che c’erano già e non funzionavano come dovevano: domande a cui Loom avrebbe dovuto rispondere e non rispondeva.
La regola però resta quella di sempre: niente infrastruttura in più. L’estratto pertinente è un ciclo su una stringa, loom_grep è la libreria standard di Go, il .loomignore è un file di testo. Un binario, un file SQLite, il modello che usi già.
Risorse
- Repository Loom: github.com/Mauto82/loom
- Changelog 0.7.0: CHANGELOG.md
- Homebrew tap: github.com/Mauto82/homebrew-loom
- Sintassi RE2: github.com/google/re2/wiki/Syntax
- I post precedenti su Loom: la presentazione e la ricerca semantica opzionale
P.S. Il bug della GUI è il mio preferito della categoria: nessun errore, nessun log, solo risposte un po’ peggiori. È il tipo di bug per cui dai la colpa al modello di embedding per giorni, prima di pensare ad aprire un file di configurazione.
