Riordinare documenti testuali con Ollama e modello di embedding Qwen3 - in Go

L'implementazione di RAG? Ecco alcuni snippet di codice in Golang.

Indice

Questo piccolo Esempio di codice Go per il rirango è che chiama Ollama per generare embedding per la query e per ogni documento candidato, poi ordinando in ordine discendente in base alla similarità del coseno.

Abbiamo già fatto un’attività simile - Rirango con modelli di embedding ma era in Python, con un LLM diverso e quasi un anno fa.

llamas di altezze diverse - rirango con ollama

TL;DR

Il risultato sembra molto buono, la velocità è di 0,128s per documento. La domanda è considerata come un documento. E l’ordinamento e la stampa sono anche inclusi in questo dato statistico.

Consumo di memoria dell’LLM: Anche se la dimensione del modello su sdd (ollama ls) è inferiore a 3 GB

dengcao/Qwen3-Embedding-4B:Q5_K_M           7e8c9ad6885b    2,9 GB

In VRAM della GPU occupa (non un po’) di più: 5,5 GB. (ollama ps)

NOME                                 ID              DIMENSIONE
dengcao/Qwen3-Embedding-4B:Q5_K_M    7e8c9ad6885b    5,5 GB 

Se hai una GPU da 8 GB - dovrebbe essere OK.

Test del rirango con embedding su Ollama - Output di esempio

In tutti e tre i casi di test il rirango con embedding utilizzando il modello dengcao/Qwen3-Embedding-4B:Q5_K_M di Ollama è stato fantastico! Vedilo da te stesso.

Abbiamo 7 file che contengono alcuni testi che descrivono ciò che il loro nome del file dice:

  • ai_introduction.txt
  • machine_learning.md
  • qwen3-reranking-models.md
  • ollama-parallelism.md
  • ollama-reranking-models.md
  • programming_basics.txt
  • setup.log

esecuzione dei test:

Test di rirango: Cosa è l’intelligenza artificiale e come funziona l’apprendimento automatico?

./rnk example_query.txt example_docs/

Utilizzando il modello di embedding: dengcao/Qwen3-Embedding-4B:Q5_K_M
URL base di Ollama: http://localhost:11434
Elaborazione del file della query: example_query.txt, cartella di destinazione: example_docs/
Query: Cosa è l'intelligenza artificiale e come funziona l'apprendimento automatico?
Trovati 7 documenti
Estrazione dell'embedding della query...
Elaborazione dei documenti...

=== RANKING PER SIMILARITÀ ===
1. example_docs/ai_introduction.txt (Punteggio: 0,451)
2. example_docs/machine_learning.md (Punteggio: 0,388)
3. example_docs/qwen3-reranking-models.md (Punteggio: 0,354)
4. example_docs/ollama-parallelism.md (Punteggio: 0,338)
5. example_docs/ollama-reranking-models.md (Punteggio: 0,318)
6. example_docs/programming_basics.txt (Punteggio: 0,296)
7. example_docs/setup.log (Punteggio: 0,282)

Elaborati 7 documenti in 0,899s (media: 0,128s per documento)

Test di rirango: Come Ollama gestisce le richieste parallele?

./rnk example_query2.txt example_docs/

Utilizzando il modello di embedding: dengcao/Qwen3-Embedding-4B:Q5_K_M
URL base di Ollama: http://localhost:11434
Elaborazione del file della query: example_query2.txt, cartella di destinazione: example_docs/
Query: Come Ollama gestisce le richieste parallele?
Trovati 7 documenti
Estrazione dell'embedding della query...
Elaborazione dei documenti...

=== RANKING PER SIMILARITÀ ===
1. example_docs/ollama-parallelism.md (Punteggio: 0,557)
2. example_docs/qwen3-reranking-models.md (Punteggio: 0,532)
3. example_docs/ollama-reranking-models.md (Punteggio: 0,498)
4. example_docs/ai_introduction.txt (Punteggio: 0,366)
5. example_docs/machine_learning.md (Punteggio: 0,332)
6. example_docs/programming_basics.txt (Punteggio: 0,307)
7. example_docs/setup.log (Punteggio: 0,257)

Elaborati 7 documenti in 0,858s (media: 0,123s per documento)

Test di rirango: Come possiamo eseguire il rirango del documento con Ollama?

./rnk example_query3.txt example_docs/

Utilizzando il modello di embedding: dengcao/Qwen3-Embedding-4B:Q5_K_M
URL base di Ollama: http://localhost:11434
Elaborazione del file della query: example_query3.txt, cartella di destinazione: example_docs/
Query: Come possiamo eseguire il rirango del documento con Ollama?
Trovati 7 documenti
Estrazione dell'embedding della query...
Elaborazione dei documenti...

=== RANKING PER SIMILARITÀ ===
1. example_docs/ollama-reranking-models.md (Punteggio: 0,552)
2. example_docs/ollama-parallelism.md (Punteggio: 0,525)
3. example_docs/qwen3-reranking-models.md (Punteggio: 0,524)
4. example_docs/ai_introduction.txt (Punteggio: 0,369)
5. example_docs/machine_learning.md (Punteggio: 0,346)
6. example_docs/programming_basics.txt (Punteggio: 0,316)
7. example_docs/setup.log (Punteggio: 0,279)

Elaborati 7 documenti in 0,882s (media: 0,126s per documento)

Codice sorgente Go

Metti tutto in una cartella e compila come

go build -o rnk

Sentiti libero di usarlo per qualsiasi scopo divertente o commerciale o caricarlo su GitHub se ti piace. Licenza MIT.

main.go

package main

import (
	"fmt"
	"log"
	"os"
	"sort"
	"time"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "rnk [query-file] [target-directory]",
	Short: "Sistema RAG utilizzando embedding di Ollama",
	Long:  "Un semplice sistema RAG che estrae gli embedding e classifica i documenti utilizzando Ollama",
	Args:  cobra.ExactArgs(2),
	Run:   runRnk,
}

var (
	embeddingModel string
	ollamaBaseURL  string
)

func init() {
	rootCmd.Flags().StringVarP(&embeddingModel, "model", "m", "dengcao/Qwen3-Embedding-4B:Q5_K_M", "Modello di embedding da utilizzare")
	rootCmd.Flags().StringVarP(&ollamaBaseURL, "url", "u", "http://localhost:11434", "URL base di Ollama")
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func runRnk(cmd *cobra.Command, args []string) {
	queryFile := args[0]
	targetDir := args[1]

	startTime := time.Now()

	fmt.Printf("Utilizzando il modello di embedding: %s\n", embeddingModel)
	fmt.Printf("URL base di Ollama: %s\n", ollamaBaseURL)
	fmt.Printf("Elaborazione del file della query: %s, cartella di destinazione: %s\n", queryFile, targetDir)

	// Leggi la query dal file
	query, err := readQueryFromFile(queryFile)
	if err != nil {
		log.Fatalf("Errore nella lettura del file della query: %v", err)
	}
	fmt.Printf("Query: %s\n", query)

	// Trova tutti i file di testo nella cartella di destinazione
	documents, err := findTextFiles(targetDir)
	if err != nil {
		log.Fatalf("Errore nel trovare i file di testo: %v", err)
	}
	fmt.Printf("Trovati %d documenti\n", len(documents))

	// Estrai gli embedding per la query
	fmt.Println("Estrazione dell'embedding della query...")
	queryEmbedding, err := getEmbedding(query, embeddingModel, ollamaBaseURL)
	if err != nil {
		log.Fatalf("Errore nell'ottenere l'embedding della query: %v", err)
	}

	// Elabora i documenti
	fmt.Println("Elaborazione dei documenti...")
	validDocs := make([]Document, 0)

	for _, doc := range documents {
		embedding, err := getEmbedding(doc.Content, embeddingModel, ollamaBaseURL)
		if err != nil {
			fmt.Printf("Avviso: Impossibile ottenere l'embedding per %s: %v\n", doc.Path, err)
			continue
		}

		similarity := cosineSimilarity(queryEmbedding, embedding)
		doc.Score = similarity
		validDocs = append(validDocs, doc)
	}

	if len(validDocs) == 0 {
		log.Fatalf("Nessun documento è stato elaborato correttamente")
	}

	// Ordina per punteggio di similarità (discendente)
	sort.Slice(validDocs, func(i, j int) bool {
		return validDocs[i].Score > validDocs[j].Score
	})

	// Mostra i risultati
	fmt.Println("\n=== RANKING PER SIMILARITÀ ===")
	for i, doc := range validDocs {
		fmt.Printf("%d. %s (Punteggio: %.3f)\n", i+1, doc.Path, doc.Score)
	}

	totalTime := time.Since(startTime)
	avgTimePerDoc := totalTime / time.Duration(len(validDocs))

	fmt.Printf("\nElaborati %d documenti in %.3fs (media: %.3fs per documento)\n",
		len(validDocs), totalTime.Seconds(), avgTimePerDoc.Seconds())
}

documents.go

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

func readQueryFromFile(filename string) (string, error) {
	content, err := os.ReadFile(filename)
	if err != nil {
		return "", err
	}
	return strings.TrimSpace(string(content)), nil
}

func findTextFiles(dir string) ([]Document, error) {
	var documents []Document

	err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if !info.IsDir() && isTextFile(path) {
			content, err := os.ReadFile(path)
			if err != nil {
				fmt.Printf("Avviso: Impossibile leggere il file %s: %v\n", path, err)
				return nil
			}

			documents = append(documents, Document{
				Path:    path,
				Content: string(content),
			})
		}

		return nil
	})

	return documents, err
}

func isTextFile(filename string) bool {
	ext := strings.ToLower(filepath.Ext(filename))
	textExts := []string{".txt", ".md", ".rst", ".csv", ".json", ".xml", ".html", ".htm", ".log"}
	for _, textExt := range textExts {
		if ext == textExt {
			return true
		}
	}
	return false
}

embeddings.go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

func getEmbedding(text string, model string, ollamaBaseURL string) ([]float64, error) {
	req := OllamaEmbeddingRequest{
		Model:  model,
		Prompt: text,
	}

	jsonData, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}

	resp, err := http.Post(ollamaBaseURL+"/api/embeddings", "application/json", bytes.NewBuffer(jsonData))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("errore API di Ollama: %s", string(body))
	}

	var embeddingResp OllamaEmbeddingResponse
	if err := json.NewDecoder(resp.Body).Decode(&embeddingResp); err != nil {
		return nil, err
	}

	return embeddingResp.Embedding, nil
}

similarity.go

package main

func cosineSimilarity(a, b []float64) float64 {
	if len(a) != len(b) {
		return 0
	}

	var dotProduct, normA, normB float64

	for i := range a {
		dotProduct += a[i] * b[i]
		normA += a[i] * a[i]
		normB += b[i] * b[i]
	}

	if normA == 0 || normB == 0 {
		return 0
	}

	return dotProduct / (sqrt(normA) * sqrt(normB))
}

func sqrt(x float64) float64 {
	if x == 0 {
		return 0
	}
	z := x
	for i := 0; i < 10; i++ {
		z = (z + x/z) / 2
	}
	return z
}

types.go

package main

// OllamaEmbeddingRequest rappresenta il payload della richiesta per l'API di embedding di Ollama
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse rappresenta la risposta dall'API di embedding di Ollama
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document rappresenta un documento con i suoi metadati
type Document struct {
	Path    string
	Content string
	Score   float64
}