{"id":84,"date":"2026-06-23T21:45:00","date_gmt":"2026-06-23T21:45:00","guid":{"rendered":"https:\/\/rpi.temporiti.net\/wordpress\/?p=84"},"modified":"2026-06-24T07:20:27","modified_gmt":"2026-06-24T05:20:27","slug":"minilm-semantic-search","status":"publish","type":"post","link":"https:\/\/rpi.temporiti.net\/wordpress\/?p=84","title":{"rendered":"Ricerca semantica con sentence-transformers all-MiniLM-L6-v2"},"content":{"rendered":"<p>Sul portatile mi sono ritrovato negli anni una cartella <code>~\/Note\/<\/code> che \u00e8 cresciuta a forza di Markdown buttati l\u00ec in fretta. Appunti di letture, snippet di configurazione, riassunti di conferenze, runbook che ho scritto per me stesso, post-mortem personali su problemi che ho risolto e voglio ricordare. <code>grep<\/code> funziona finch\u00e9 ricordo le parole esatte; per il resto, perdo dieci minuti a navigare nelle sottocartelle ogni volta che cerco &#8220;quella cosa che avevo scritto su X&#8221;. Da qualche mese ho aggiunto un piccolo strato di ricerca semantica costruito con sentence-transformers, e fa esattamente quello che mi serviva.<\/p>\n<p>Il modello che uso \u00e8 <code>all-MiniLM-L6-v2<\/code>, un classico del settore: 22 milioni di parametri, dimensione embedding 384, peso intorno ai 90 MB. Sul portatile a CPU gira senza farsi notare e produce risultati onesti.<\/p>\n<h2>Setup con sentence-transformers<\/h2>\n<p>L&#8217;installazione \u00e8 in un virtualenv dedicato. Su Debian 13 e su macOS la procedura \u00e8 identica, cambia solo il modo in cui attivo l&#8217;ambiente.<\/p>\n<pre><code class=\"language-bash\">\nsudo apt install -y python3-venv\npython3 -m venv ~\/.venvs\/embed\nsource ~\/.venvs\/embed\/bin\/activate\npip install --upgrade pip\npip install sentence-transformers numpy\n<\/code><\/pre>\n<p>La prima volta che istanzio il modello, la libreria lo scarica in <code>~\/.cache\/huggingface\/hub\/<\/code>. Sono 90 MB scarsi, scaricati una volta sola, da l\u00ec in poi tutto offline.<\/p>\n<p>Il flusso che ho messo in piedi \u00e8 semplice. Uno script Python attraversa l&#8217;archivio Markdown, calcola un embedding per ogni file (o per ogni paragrafo se il file \u00e8 lungo), salva il tutto in un file Numpy <code>.npy<\/code> insieme a un indice. Quando voglio cercare, lo stesso script prende la query, calcola il suo embedding, fa il prodotto scalare con tutti i vettori salvati e mi restituisce i primi cinque risultati per similarit\u00e0 coseno.<\/p>\n<pre><code class=\"language-python\">\nfrom pathlib import Path\nimport numpy as np\nfrom sentence_transformers import SentenceTransformer\n\nmodel = SentenceTransformer(\"all-MiniLM-L6-v2\")\nnotes_root = Path.home() \/ \"Note\"\n\ndocs = []\npaths = []\nfor md in notes_root.rglob(\"*.md\"):\n    text = md.read_text(encoding=\"utf-8\")\n    docs.append(text)\n    paths.append(str(md))\n\nembeddings = model.encode(\n    docs,\n    batch_size=16,\n    show_progress_bar=True,\n    normalize_embeddings=True,\n)\n\nnp.save(\"notes.npy\", embeddings)\nPath(\"notes.idx\").write_text(\"\\n\".join(paths))\n<\/code><\/pre>\n<p>Per la ricerca, le poche righe che mi servono:<\/p>\n<pre><code class=\"language-python\">\nimport numpy as np\nfrom pathlib import Path\nfrom sentence_transformers import SentenceTransformer\n\nmodel = SentenceTransformer(\"all-MiniLM-L6-v2\")\nembeddings = np.load(\"notes.npy\")\npaths = Path(\"notes.idx\").read_text().splitlines()\n\nquery = \"configurazione rsyslog per inviare a un collector remoto\"\nq = model.encode([query], normalize_embeddings=True)[0]\nscores = embeddings @ q\n\ntop = np.argsort(-scores)[:5]\nfor i in top:\n    print(f\"{scores[i]:.3f}  {paths[i]}\")\n<\/code><\/pre>\n<p>Reindicizzo una volta a settimana con un alias fish che lancia lo script di build. Sui circa 1200 file Markdown che ho oggi il calcolo finisce in due minuti scarsi sul portatile.<\/p>\n<h2>Un esempio reale<\/h2>\n<p>Un pomeriggio della settimana scorsa stavo configurando un container con Caddy come reverse proxy davanti a un&#8217;API interna, e ricordavo vagamente di aver scritto mesi fa qualcosa su come gestire gli upstream timeout di Caddy quando l&#8217;API risponde lenta sulla prima richiesta. Niente da fare con <code>grep<\/code>: avevo provato <code>caddy<\/code>, <code>upstream<\/code>, <code>timeout<\/code>, <code>reverse_proxy<\/code>, e mi tornavano fuori venti file con questi termini sparsi.<\/p>\n<p>Ho lanciato lo script di ricerca semantica con la query &#8220;Caddy reverse proxy timeout quando upstream lento&#8221;, e nei primi tre risultati c&#8217;era esattamente la nota che cercavo: un appunto di marzo dove avevo annotato la sintassi <code>transport http { dial_timeout, read_timeout, write_timeout }<\/code> per uno specifico problema che avevo risolto su un microservizio FastAPI. Non c&#8217;era la parola &#8220;lento&#8221; da nessuna parte nella nota, c&#8217;era &#8220;latenza&#8221;, e MiniLM aveva colto la corrispondenza semantica.<\/p>\n<p>Cinque minuti di lavoro risparmiati, e soprattutto la sensazione che la mia knowledge base personale fosse finalmente di nuovo interrogabile. Quel singolo episodio \u00e8 quello che mi ha convinto a stabilizzare lo script e integrarlo nelle abitudini.<\/p>\n<h2>Cosa fa bene<\/h2>\n<p>Query in linguaggio naturale dove ricordo il concetto ma non le parole esatte. Sinonimi tecnici (latenza\/lento, autenticazione\/auth, certificato\/cert). Riformulazioni dello stesso problema. Funziona ragionevolmente sia in italiano sia in inglese, perch\u00e9 il modello \u00e8 multilingue di fatto pur essendo addestrato principalmente su inglese. Per ricerche tipo &#8220;come avevo configurato il backup di Postgres in quel laboratorio&#8221; mi tira fuori la nota giusta anche se non ci sono parole letterali in comune.<\/p>\n<h2>Cosa fa meno bene<\/h2>\n<p>Per ricerche esatte di stringhe (nomi di flag, opzioni di comando, IP, sigle specifiche) il vecchio <code>grep<\/code> resta pi\u00f9 veloce e preciso. Sui paragrafi molto brevi (una riga di appunto) la qualit\u00e0 peggiora perch\u00e9 c&#8217;\u00e8 poco contesto da embeddare. Le sigle ambigue (CA come Certificate Authority vs CA come California) non le disambigua bene.<\/p>\n<h2>Privacy &#8211; vantaggio del modello locale<\/h2>\n<p>I miei appunti contengono di tutto: configurazioni di ambienti di esempio che preferisco tenere riservate, password offuscate, riferimenti a setup di laboratorio personali, idee per articoli che non ho ancora pubblicato. Una ricerca semantica via API esterna mi obbligherebbe a uploadare query e contesto a un fornitore terzo, e nel migliore dei casi a pagare con il tempo di elaborazione, nel peggiore con la possibilit\u00e0 che query e snippet finiscano nei log o nel training del fornitore. Con MiniLM in locale tutto sta sull&#8217;host: il modello, gli embeddings, le query.<\/p>\n<p>Il modello scaricato sta in <code>~\/.cache\/huggingface\/hub\/models--sentence-transformers--all-MiniLM-L6-v2\/<\/code>. Si elimina con <code>rm -rf<\/code> quando voglio liberare spazio (e si riscarica al prossimo bisogno). Confronto con servizi cloud equivalenti: API di embedding come quelle di OpenAI o Cohere richiedono upload di ogni nota e di ogni query, con policy di retention e residency da leggere; qui non c&#8217;\u00e8 nulla di tutto questo.<\/p>\n<p>Licenza: sentence-transformers come libreria \u00e8 Apache 2.0, sviluppata da UKP Lab e dalla community. Il modello <code>all-MiniLM-L6-v2<\/code> \u00e8 rilasciato sotto Apache 2.0. Stack interamente permissivo, riutilizzabile anche dentro tooling commerciale senza vincoli copyleft.<\/p>\n<h2>In pratica<\/h2>\n<p>Lo script di build sta in un alias fish (<code>embed-notes<\/code>) che lancio il sabato mattina mentre faccio colazione. Lo script di ricerca \u00e8 un altro alias (<code>note <query><\/code>) che richiama il modello con la query passata sulla riga di comando. Tempo medio di una ricerca dopo il caricamento del modello (che richiede un paio di secondi): meno di mezzo secondo per risposta su 1200 file. Per la mia knowledge base personale \u00e8 il livello di velocit\u00e0 e qualit\u00e0 che cercavo.<\/p>\n<hr>\n<blockquote>\n<p>Immagine generata con ComfyUI Mac M1 \/ RealVisXL V5 Lightning.<\/p>\n<\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>Sul portatile mi sono ritrovato negli anni una cartella ~\/Note\/ che \u00e8 cresciuta a forza di Markdown buttati l\u00ec in fretta. Appunti di letture, snippet di configurazione, riassunti di conferenze, runbook che ho scritto per me stesso, post-mortem personali su problemi che ho risolto e voglio ricordare. grep funziona finch\u00e9 ricordo le parole esatte; per [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":85,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6],"tags":[],"class_list":["post-84","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai-locale"],"_links":{"self":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/84","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=84"}],"version-history":[{"count":6,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/84\/revisions"}],"predecessor-version":[{"id":398,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/posts\/84\/revisions\/398"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=\/wp\/v2\/media\/85"}],"wp:attachment":[{"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=84"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=84"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rpi.temporiti.net\/wordpress\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=84"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}