Reclassement de textes avec Ollama et l'LLM d'embedding Qwen3 - en Go

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

Sommaire

Cet 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 les trie de manière décroissante selon la similarité cosinus.

Nous avons déjà effectué une activité similaire - Reranking avec des modèles d’embedding, mais cela était en Python, avec un LLM différent et il y a presque un an.

Pour un guide complet sur la construction de systèmes de génération augmentée par récupération, consultez le Tutoriel de Génération Augmentée par Récupération (RAG) : Architecture, Mise en œuvre et Guide de Production.

Un autre code similaire, mais utilisant Qwen3 Reranker :

llamas of different heights - reranking with ollama

TL;DR

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

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

dengcao/Qwen3-Embedding-4B:Q5_K_M           7e8c9ad6885b    2.9 GB

Dans la VRAM du GPU, cela prend (pas mal) plus : 5,5 Go. (ollama ps)

NAME                                 ID              SIZE
dengcao/Qwen3-Embedding-4B:Q5_K_M    7e8c9ad6885b    5.5 GB 

Si vous avez un GPU de 8 Go, cela devrait suffire.

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

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

Nous avons 7 fichiers contenant des 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

Exécutions de test :

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

./rnk example_query.txt example_docs/

Using embedding model: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama base URL: http://localhost:11434
Processing query file: example_query.txt, target directory: example_docs/
Query: What is artificial intelligence and how does machine learning work?
Found 7 documents
Extracting query embedding...
Processing documents...

=== RANKING BY SIMILARITY ===
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)

Processed 7 documents in 0.899s (avg: 0.128s per document)

Test de reranking : Comment Ollama gère-t-il les requêtes parallèles ?

./rnk example_query2.txt example_docs/

Using embedding model: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama base URL: http://localhost:11434
Processing query file: example_query2.txt, target directory: example_docs/
Query: How ollama handles parallel requests?
Found 7 documents
Extracting query embedding...
Processing documents...

=== RANKING BY SIMILARITY ===
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)

Processed 7 documents in 0.858s (avg: 0.123s per document)

Test de reranking : Comment pouvons-nous effectuer le reranking des documents avec Ollama ?

./rnk example_query3.txt example_docs/

Using embedding model: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama base URL: http://localhost:11434
Processing query file: example_query3.txt, target directory: example_docs/
Query: How can we do the reranking of the document with ollama?
Found 7 documents
Extracting query embedding...
Processing documents...

=== RANKING BY SIMILARITY ===
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)

Processed 7 documents in 0.882s (avg: 0.126s per document)

Code source Go

Mettez tout cela dans un dossier et compilez-le comme ceci :

go build -o rnk

N’hésitez pas à l’utiliser à des fins de divertissement ou commerciales, ou à le télécharger 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: "RAG system using Ollama embeddings",
	Long:  "A simple RAG system that extracts embeddings and ranks documents using 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", "Embedding model to use")
	rootCmd.Flags().StringVarP(&ollamaBaseURL, "url", "u", "http://localhost:11434", "Ollama base URL")
}

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("Using embedding model: %s\n", embeddingModel)
	fmt.Printf("Ollama base URL: %s\n", ollamaBaseURL)
	fmt.Printf("Processing query file: %s, target directory: %s\n", queryFile, targetDir)

	// Read query from file
	query, err := readQueryFromFile(queryFile)
	if err != nil {
		log.Fatalf("Error reading query file: %v", err)
	}
	fmt.Printf("Query: %s\n", query)

	// Find all text files in target directory
	documents, err := findTextFiles(targetDir)
	if err != nil {
		log.Fatalf("Error finding text files: %v", err)
	}
	fmt.Printf("Found %d documents\n", len(documents))

	// Extract embeddings for query
	fmt.Println("Extracting query embedding...")
	queryEmbedding, err := getEmbedding(query, embeddingModel, ollamaBaseURL)
	if err != nil {
		log.Fatalf("Error getting query embedding: %v", err)
	}

	// Process documents
	fmt.Println("Processing documents...")
	validDocs := make([]Document, 0)

	for _, doc := range documents {
		embedding, err := getEmbedding(doc.Content, embeddingModel, ollamaBaseURL)
		if err != nil {
			fmt.Printf("Warning: Failed to get embedding for %s: %v\n", doc.Path, err)
			continue
		}

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

	if len(validDocs) == 0 {
		log.Fatalf("No documents could be processed successfully")
	}

	// Sort by similarity score (descending)
	sort.Slice(validDocs, func(i, j int) bool {
		return validDocs[i].Score > validDocs[j].Score
	})

	// Display results
	fmt.Println("\n=== RANKING BY SIMILARITY ===")
	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("\nProcessed %d documents in %.3fs (avg: %.3fs per 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("Warning: Could not read 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("ollama API error: %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 represents the request payload for Ollama embedding API
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse represents the response from Ollama embedding API
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document represents a document with its metadata
type Document struct {
	Path    string
	Content string
	Score   float64
}

Liens utiles