Herordenen van tekstdocumenten met Ollama en Qwen3 Embedding model - in Go

RAG implementeren? Hier zijn enkele codefragmenten in Golang.

Inhoud

Dit kleine Reranking Go codevoorbeeld roept Ollama aan om embeddings te genereren voor de query en voor elk kandidaatdocument, en vervolgens sorteren in dalende volgorde op cosinus-afstand.

We hebben al een soortgelijke activiteit gedaan - Reranking met embeddingmodellen maar dat was in Python, met een andere LLM en bijna een jaar geleden.

llamas van verschillende hoogtes - reranking met ollama

TL;DR

Het resultaat ziet er zeer goed uit, de snelheid is 0,128s per document. Een vraag wordt als een document geteld. En het sorteren en afdrukken zijn ook opgenomen in deze statistiek.

LLM-geheugengebruik: Hoewel de modelgrootte op de SSD (ollama ls) minder is dan 3 GB

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

Op de GPU VRAM neemt het (niet een beetje) meer in beslag: 5,5 GB. (ollama ps)

NAAM                                 ID              GROOTTE
dengcao/Qwen3-Embedding-4B:Q5_K_M    7e8c9ad6885b    5,5 GB 

Als je een GPU met 8 GB hebt - moet het wel lukken.

Testen van Reranking met Embeddingmodellen op Ollama - Voorbeelduitvoer

In alle drie de testgevallen was het reranking met embedding met behulp van het dengcao/Qwen3-Embedding-4B:Q5_K_M Ollama-model geweldig! Bekijk het zelf.

We hebben 7 bestanden met tekst die beschrijven wat hun bestandsnaam zegt:

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

test runs:

Reranking test: Wat is kunstmatige intelligentie en hoe werkt machine learning?

./rnk example_query.txt example_docs/

Gebruikte embeddingmodel: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama basis-URL: http://localhost:11434
Verwerken van querybestand: example_query.txt, doelmap: example_docs/
Query: Wat is kunstmatige intelligentie en hoe werkt machine learning?
7 documenten gevonden
Extractie van queryembedding...
Verwerken van documenten...

=== RANGSCHIKKEN OP LIJKBAREHEID ===
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 documenten verwerkt in 0,899s (gemiddeld: 0,128s per document)

Reranking test: Hoe verwerkt Ollama parallele aanvragen?

./rnk example_query2.txt example_docs/

Gebruikte embeddingmodel: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama basis-URL: http://localhost:11434
Verwerken van querybestand: example_query2.txt, doelmap: example_docs/
Query: Hoe verwerkt Ollama parallele aanvragen?
7 documenten gevonden
Extractie van queryembedding...
Verwerken van documenten...

=== RANGSCHIKKEN OP LIJKBAREHEID ===
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 documenten verwerkt in 0,858s (gemiddeld: 0,123s per document)

Reranking test: Hoe kunnen we het reranking van het document met Ollama doen?

./rnk example_query3.txt example_docs/

Gebruikte embeddingmodel: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama basis-URL: http://localhost:11434
Verwerken van querybestand: example_query3.txt, doelmap: example_docs/
Query: Hoe kunnen we het reranking van het document met Ollama doen?
7 documenten gevonden
Extractie van queryembedding...
Verwerken van documenten...

=== RANGSCHIKKEN OP LIJKBAREHEID ===
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 documenten verwerkt in 0,882s (gemiddeld: 0,126s per document)

Go Broncode

Zet alles in een map en compileer het zoals

go build -o rnk

Gebruik het vrijelijk voor entertainende of commerciële doeleinden of upload het naar GitHub als je dat leuk vindt. MIT-licentie.

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-systeem met behulp van Ollama-embeddings",
	Long:  "Een eenvoudig RAG-systeem dat embeddings extrahiert en documenten rangschikt met behulp van 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", "Embeddingmodel dat moet worden gebruikt")
	rootCmd.Flags().StringVarP(&ollamaBaseURL, "url", "u", "http://localhost:11434", "Ollama basis-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("Gebruikte embeddingmodel: %s\n", embeddingModel)
	fmt.Printf("Ollama basis-URL: %s\n", ollamaBaseURL)
	fmt.Printf("Verwerken van querybestand: %s, doelmap: %s\n", queryFile, targetDir)

	// Lees query uit bestand
	query, err := readQueryFromFile(queryFile)
	if err != nil {
		log.Fatalf("Fout bij het lezen van het querybestand: %v", err)
	}
	fmt.Printf("Query: %s\n", query)

	// Zoek alle tekstbestanden in doelmap
	documents, err := findTextFiles(targetDir)
	if err != nil {
		log.Fatalf("Fout bij het zoeken naar tekstbestanden: %v", err)
	}
	fmt.Printf("7 documenten gevonden\n", len(documents))

	// Extract embeddings voor query
	fmt.Println("Extractie van queryembedding...")
	queryEmbedding, err := getEmbedding(query, embeddingModel, ollamaBaseURL)
	if err != nil {
		log.Fatalf("Fout bij het verkrijgen van queryembedding: %v", err)
	}

	// Verwerk documenten
	fmt.Println("Verwerken van documenten...")
	validDocs := make([]Document, 0)

	for _, doc := range documents {
		embedding, err := getEmbedding(doc.Content, embeddingModel, ollamaBaseURL)
		if err != nil {
			fmt.Printf("Waarschuwing: Mislukt om embedding te verkrijgen voor %s: %v\n", doc.Path, err)
			continue
		}

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

	if len(validDocs) == 0 {
		log.Fatalf("Geen documenten konden succesvol worden verwerkt")
	}

	// Sorteer op lijkbareheid (dalend)
	sort.Slice(validDocs, func(i, j int) bool {
		return validDocs[i].Score > validDocs[j].Score
	})

	// Toon resultaten
	fmt.Println("\n=== RANGSCHIKKEN OP LIJKBAREHEID ===")
	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("\n7 documenten verwerkt in %.3fs (gemiddeld: %.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("Waarschuwing: Kon bestand %s niet lezen: %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 fout: %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 vertegenwoordigt de aanvraagpayload voor de Ollama embedding API
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse vertegenwoordigt de respons van de Ollama embedding API
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document vertegenwoordigt een document met zijn metadata
type Document struct {
	Path    string
	Content string
	Score   float64
}