Ricerca semantica con Ollama

Ti è mai capitato di cercare su un e-commerce la parola “piede” e trovare delle scarpe? Noi, esseri umani, conosciamo la relazione tra questi due termini, ma come possiamo rendere disponibile questa conoscenza al nostro software?
Spesso questa ricerca viene chiamata “Ricerca Vettoriale” o “Ricerca Semantica”. Questo tipo di ricerca è molto più potente rispetto alla ricerca testuale (Full-Text), perché non si limita a cercare corrispondenze esatte tra le parole, ma cerca di capire il significato dietro le parole stesse.
TOC
Cos’è la ricerca semantica
La ricerca semantica (o a vettori) utilizza modelli di machine learning e rappresentazioni numeriche (vettori) per catturare il significato di testi, immagini o altri contenuti, anziché limitarsi alle parole chiave testuali. Invece di cercare corrispondenze letterali, confronta le “vicinanze” tra vettori che rappresentano concetti: in questo modo trova risultati più pertinenti sul piano semantico, anche se i termini non combaciano esattamente.
Come si fa?
Di seguito troverai un breve tutorial su come iniziare a sviluppare un’applicazione che esegua vector embedding e vector search locale utilizzando Ollama per gestire i modelli, Orama per la ricerca vettoriale e Node.js come base applicativa. L’obiettivo è mostrarti un percorso semplice per effettuare l’embedding di testi con un modello locale (ad esempio nomic-embed-text) e salvare/ricercare questi embedding su un indice vettoriale. Il tutto in pochissimi minuti!
Al momento della stesura di questo articolo, il team di Orama sta lavorando alla versione 1.0.0 di Orama Core, un nuovo progetto che include al suo interno sia la ricerca vettoriale - obiettivo di questo articolo - sia i modelli LLM che si occupano dell’embedding. Mettere in piedi una ricerca vettoriale e un sistema di embedding è un bellissimo esperimento, ma strumenti come Orama Core sono fatti proprio per evitare di ricostruire la ruota quotidianamente, e suggeriamo di farlo solo a scopi didattici, o se veramente si ha la necessità di scendere a un livello inferiore di astrazione.
1. Introduzione e prerequisiti
Node.js: assicurati di aver installato Node.js (consigliata una versione LTS).
Per installarlo è sufficiente andare sul sito https://nodejs.org/ e scaricare l’ultima versione per il proprio sistema operativo.Ollama: è un prodotto che semplifica l’esecuzione di modelli LLM in locale. Fornisce un’interfaccia a riga di comando (CLI) e permette di integrare i modelli in un’app Node.js tramite un semplice SDK.
Anche in questo caso, per installarlo è sufficiente andare su https://ollama.com/ e scaricare l’ultima versione per il proprio sistema operativo.
2. Ollama: installazione e comandi principali
Una volta installato Ollama, puoi utilizzare alcuni comandi chiave:
ollama list
: mostra i modelli installati localmente.ollama pull <nome-modello>
: scarica un modello, ad esempio:ollama pull nomic-embed-text
ollama run <nome-modello>
: esegue il modello con un prompt testuale.ollama serve
: avvia il server di Ollama, utile se vuoi servire i modelli tramite API (in alcuni casi potresti preferire la modalità integrata direttamente con l’SDK Node).
Nel nostro esempio useremo il modello nomic-embed-text per generare embedding delle stringhe.
Come mai nomic-embed-text?
Al momento della stesura di questo articolo, risulta il più popolare nella libreria di Ollama, ha una finestra di contesto molto ampia ed è molto veloce nella fase di embedding. Se preferisci, puoi usare qualsiasi altro modello di embedding.
3. Orama: indice vettoriale e schema dei dati
Per utilizzare la ricerca vettoriale di Orama, dobbiamo:
Creare un database definendo lo schema e il tipo “vector” sui campi embedding.
Inserire i documenti (con l’embedding calcolato).
Effettuare la ricerca vettoriale su tali documenti.
Nell’esempio qui sotto, useremo Orama per indicizzare una lista di film (contenuti in un file locale chiamato movies.json
, che andremo a scaricare dopo).
4. Esempio pratico con Node.js
Prepara un nuovo progetto Node.js e installa i pacchetti necessari:
npm init -y
npm install ollama @orama/orama
Assicurati di aver già lanciato il comando:
ollama pull nomic-embed-text
Così da scaricare il modello richiesto.
a) Funzione per creare embedding (ollama.js)
Crea un file ollama.js
con il contenuto seguente. Questa funzione riceve un array di stringhe (documents
) e restituisce un array di vettori di embedding (uno per ogni stringa):
import ollama from 'content/posts/2025/03/2025-03-04-ollama'
// documents come array di stringhe
export async function getEmbeddings(documents) {
const results = [];
let counter = 0;
for (let document of documents) {
counter++;
console.log(`Embedding ${counter} di ${documents.length}`);
const embeddings = await ollama.embed({
model: 'nomic-embed-text',
input: document,
keep_alive: "60s"
})
results.push(embeddings.embeddings[0]);
}
return results;
}
P.S. Abbiamo aggiunto un console.log
per non lasciarti in attesa del risultato!
b) Creazione dell’indice e ricerca (index.js)
Nel file index.js
, utilizziamo Orama per:
Creare un indice con proprietà
title
,description
eembedding
(di tipo vector).Leggere il file
movies.json
e preparare i documenti.
Per questo step abbiamo utilizzato un semplice file JSON trovato su Github: https://github.com/toedter/movies-demo/blob/master/backend/src/main/resources/static/movie-data/movies-250.json.
Con i dovuti aggiustamenti, potrai sostituire questo file con una query verso un DB, una chiamata a Redis o qualsiasi altra fonte di dato ti possa essere utile!Calcolare gli embedding con la funzione
getEmbeddings
.Inserire i dati in Orama.
Eseguire una ricerca vettoriale passandogli l’embedding di una query.
Ecco un esempio completo (anche in questo caso, abbiamo aggiunto qualche console.log
per dare un senso di progressione):
import {create, insertMultiple, search} from "@orama/orama";
import {getEmbeddings} from "./2025-03-04-ollama.md";
import fs from "fs";
console.log('Starting...');
const movies = JSON.parse(fs.readFileSync("./movies.json", "utf-8"));
console.log('Movies file read');
// Creazione del database Orama
const db = create({
schema: {
title: "string",
description: "string",
embedding: "vector[768]", // Attenzione, questo valore deve essere allineato con
},
});
// Mappiamo i documenti
const documents = movies.map(m => ({
title: m.Title,
embeddable: `${m.Title} ${m.Plot}`,
description: `${m.Plot} ${m.Genre} ${m.Director} ${m.Writer} ${m.Actors}`
}));
console.log('Mapped documents');
// Creiamo gli embedding
console.time("embedding");
const embeddings = await getEmbeddings(documents.map(d => d.embeddable));
console.timeEnd("embedding");
// Inseriamo dati con embedding nel database
console.time("insert");
await insertMultiple(db, documents.map((d, i) => ({
title: d.title,
description: d.description,
embedding: embeddings[i],
})));
console.timeEnd("insert");
// Recuperiamo la query da riga di comando
const query = process.argv.slice(2).join(" ");
const embeddingQuery = (await getEmbeddings([query]))[0];
// Eseguiamo la ricerca vettoriale
console.time("search");
const results = await search(db, {
mode: "vector",
vector: {
value: embeddingQuery,
property: "embedding",
},
similarity: 0.5, // Punteggio minimo di similarità da ricercare
limit: 3, // Limite dei risultati mostrati
});
console.timeEnd("search");
// Mostriamo i risultati (titolo e punteggio di similarità)
console.log(
results.hits.map(h => ({document: h.document, score: h.score}))
);
Come eseguirlo?
node index.js "testo della tua query"
Nel nostro caso, usando come chiave di ricerca “planet”, i risultati che ci vengono restituiti sono, in ordine: Interstellar e Avatar. In un altro test, un altro modello ci ha restituito nell’ordine Avatar, Interstellar e The Avengers.
La cosa incredibile è che nessuno di questi film contiene, nelle informazioni di embedding passate, ossia titolo e plot, la parola “planet”, ed è questo il bello della ricerca semantica!
5. Conclusioni & Prossimi Step
In pochi passi hai configurato:
Ollama per scaricare ed eseguire in locale un modello LLM (in questo caso, per embedding).
Orama per creare un indice con campi testuali e vettoriali, dove salvare i dati e cercarli tramite similarità.
Node.js per unire il tutto in un’unica applicazione: lettura dati, generazione embedding, indicizzazione e ricerca.
E ora? Beh, la fantasia è il tuo unico limite (oltre che la dimensione del modello LLM scelto 😂).
Potresti implementare la Data Persistence (vedi qui per maggiori dettagli) per evitare di dover addestrare il tuo modello a ogni avvio, oppure potresti usare modelli differenti e confrontare i risultati!
Questa base può essere estesa per creare chatbot, motori di ricerca intelligenti, o integrazioni con dati testuali molto più ampi.
Buon sviluppo con la tua AI locale!