Reranking des documents textuels avec Ollama et le modèle d'embedding Qwen3 - en Go

Mise en œuvre de RAG ? Voici quelques extraits de code en Golang.

Sommaire

Ce petit exemple de code Go pour le reranking appelle Ollama pour générer des embeddings pour la requête et pour chaque document candidat, puis trie en ordre descendant selon la similarité cosinus.

Nous avons déjà fait une activité similaire - Reranking avec des modèles d’embeddings mais cela était en python, avec un autre LLM et presque un an plus tôt.

llamas de différentes tailles - reranking avec ollama

TL;DR

Le résultat semble très bon, la vitesse est de 0,128s par document. La question est comptée comme un document. Et le tri et l’impression sont également inclus dans cette statistique.

Consommation de mémoire LLM : Même si la taille du modèle sur le disque dur (ollama ls) est inférieure à 3 Go

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

Sur la VRAM de la GPU, cela prend (pas un peu) plus : 5,5 Go. (ollama ps)

NAME                                 ID              SIZE
dengcao/Qwen3-Embedding-4B:Q5_K_M    7e8c9ad6885b    5,5 Go 

Si vous avez une GPU de 8 Go - devrait être OK.

Test du reranking avec des embeddings sur Ollama - Exemple de sortie

Dans les trois cas de test, le reranking avec embeddings en utilisant le modèle dengcao/Qwen3-Embedding-4B:Q5_K_M d’Ollama était incroyable ! Voyez-le par vous-mêmes.

Nous avons 7 fichiers contenant certains textes décrivant ce que leur nom de fichier indique :

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

tests :

Test de reranking : Qu’est-ce que l’intelligence artificielle et comment fonctionne l’apprentissage automatique ?

./rnk example_query.txt example_docs/

Utilisation du modèle d'embedding : dengcao/Qwen3-Embedding-4B:Q5_K_M
URL de base d'Ollama : http://localhost:11434
Traitement du fichier de requête : example_query.txt, répertoire cible : example_docs/
Requête : Qu'est-ce que l'intelligence artificielle et comment fonctionne l'apprentissage automatique ?
7 documents trouvés
Extraction de l'embedding de la requête...
Traitement des documents...

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

7 documents traités en 0,899s (moyenne : 0,128s par document)

Test de reranking : Comment ollama gère les requêtes parallèles ?

./rnk example_query2.txt example_docs/

Utilisation du modèle d'embedding : dengcao/Qwen3-Embedding-4B:Q5_K_M
URL de base d'Ollama : http://localhost:11434
Traitement du fichier de requête : example_query2.txt, répertoire cible : example_docs/
Requête : Comment ollama gère les requêtes parallèles ?
7 documents trouvés
Extraction de l'embedding de la requête...
Traitement des documents...

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

7 documents traités en 0,858s (moyenne : 0,123s par document)

Test de reranking : Comment faire le reranking d’un document avec ollama ?

./rnk example_query3.txt example_docs/

Utilisation du modèle d'embedding : dengcao/Qwen3-Embedding-4B:Q5_K_M
URL de base d'Ollama : http://localhost:11434
Traitement du fichier de requête : example_query3.txt, répertoire cible : example_docs/
Requête : Comment faire le reranking d'un document avec ollama ?
7 documents trouvés
Extraction de l'embedding de la requête...
Traitement des documents...

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

7 documents traités en 0,882s (moyenne : 0,126s par document)

Code source Go

Mettez-le tout dans un dossier et compilez-le comme suit

go build -o rnk

N’hésitez pas à l’utiliser à des fins amusantes ou commerciales ou à l’uploader sur GitHub si vous le souhaitez. Licence 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: "Système RAG utilisant des embeddings Ollama",
	Long:  "Un simple système RAG qui extrait des embeddings et classe les documents à l'aide d'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", "Modèle d'embedding à utiliser")
	rootCmd.Flags().StringVarP(&ollamaBaseURL, "url", "u", "http://localhost:11434", "URL de base d'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("Utilisation du modèle d'embedding : %s\n", embeddingModel)
	fmt.Printf("URL de base d'Ollama : %s\n", ollamaBaseURL)
	fmt.Printf("Traitement du fichier de requête : %s, répertoire cible : %s\n", queryFile, targetDir)

	// Lire la requête à partir du fichier
	query, err := readQueryFromFile(queryFile)
	if err != nil {
		log.Fatalf("Erreur lors de la lecture du fichier de requête : %v", err)
	}
	fmt.Printf("Requête : %s\n", query)

	// Trouver tous les fichiers texte dans le répertoire cible
	documents, err := findTextFiles(targetDir)
	if err != nil {
		log.Fatalf("Erreur lors de la recherche des fichiers texte : %v", err)
	}
	fmt.Printf("Trouvé %d documents\n", len(documents))

	// Extraire les embeddings pour la requête
	fmt.Println("Extraction de l'embedding de la requête...")
	queryEmbedding, err := getEmbedding(query, embeddingModel, ollamaBaseURL)
	if err != nil {
		log.Fatalf("Erreur lors de l'obtention de l'embedding de la requête : %v", err)
	}

	// Traiter les documents
	fmt.Println("Traitement des documents...")
	validDocs := make([]Document, 0)

	for _, doc := range documents {
		embedding, err := getEmbedding(doc.Content, embeddingModel, ollamaBaseURL)
		if err != nil {
			fmt.Printf("Avertissement : Échec de l'obtention de l'embedding pour %s : %v\n", doc.Path, err)
			continue
		}

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

	if len(validDocs) == 0 {
		log.Fatalf("Aucun document n'a pu être traité avec succès")
	}

	// Trier par score de similarité (descendant)
	sort.Slice(validDocs, func(i, j int) bool {
		return validDocs[i].Score > validDocs[j].Score
	})

	// Afficher les résultats
	fmt.Println("\n=== RANGEMENT PAR SIMILARITÉ ===")
	for i, doc := range validDocs {
		fmt.Printf("%d. %s (Score : %.3f)\n", i+1, doc.Path, doc.Score)
	}

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

	fmt.Printf("\nTraité %d documents en %.3fs (moyenne : %.3fs par document)\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("Avertissement : Impossible de lire le fichier %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("erreur de l'API 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 représente la charge utile de la requête pour l'API d'embedding d'Ollama
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse représente la réponse de l'API d'embedding d'Ollama
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document représente un document avec ses métadonnées
type Document struct {
	Path    string
	Content string
	Score   float64
}

Liens utiles