Neuranking von Texten mit Ollama und Qwen3 Embedding-LLM – in Go

Implementieren Sie RAG? Hier sind einige Code-Snippets in Golang..

Inhaltsverzeichnis

Dieses kleine Go-Code-Beispiel für das Neuranking ruft Ollama auf, um Embeddings zu generieren für die Abfrage und für jedes Kandidatendokument, sortiert dann absteigend nach der Kosinus-Ähnlichkeit.

Wir haben bereits ähnliche Aktivitäten durchgeführt – Neuranking mit Embedding-Modellen, aber das war in Python, mit einem anderen LLM und vor fast einem Jahr.

Für eine vollständige Anleitung zur Erstellung von Retrieval-Augmented-Generation-Systemen (RAG) sehen Sie den RAG-Tutorial: Architektur, Implementierung und Produktionsleitfaden.

Ein weiterer ähnlicher Code, jedoch mit dem Qwen3 Reranker:

Lamas unterschiedlicher Größe – Neuranking mit Ollama

TL;DR

Das Ergebnis sieht sehr gut aus, die Geschwindigkeit beträgt 0,128 Sekunden pro Dokument. Die Frage wird als ein Dokument gezählt. Das Sortieren und Drucken ist ebenfalls in dieser Statistik enthalten.

LLM-Speichernutzung: Auch wenn die Modellgröße auf der Festplatte (ollama ls) weniger als 3 GB beträgt

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

nimmt es im GPU-VRAM (nicht ganz so wenig) mehr ein: 5,5 GB. (ollama ps)

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

Wenn Sie eine GPU mit 8 GB VRAM haben, sollte das in Ordnung sein.

Test des Neurankings mit Embeddings auf Ollama – Beispieloutput

In allen drei Testfällen war das Neuranking mit Embeddings unter Verwendung des Ollama-Modells dengcao/Qwen3-Embedding-4B:Q5_K_M fantastisch! Überzeugen Sie sich selbst.

Wir haben 7 Dateien, die Texte enthalten, die beschreiben, was ihr Dateiname aussagt:

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

Testläufe:

Neuranking-Test: Was ist künstliche Intelligenz und wie funktioniert maschinelles Lernen?

./rnk example_query.txt example_docs/

Verwendetes Embedding-Modell: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama-Basis-URL: http://localhost:11434
Verarbeite Abfrage-Datei: example_query.txt, Zielverzeichnis: example_docs/
Abfrage: Was ist künstliche Intelligenz und wie funktioniert maschinelles Lernen?
7 Dokumente gefunden
Extrahiere Abfrage-Embedding...
Verarbeite Dokumente...

=== RANGIERUNG NACH ÄHNLICHKEIT ===
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 Dokumente in 0,899s verarbeitet (Durchschnitt: 0,128s pro Dokument)

Neuranking-Test: Wie behandelt Ollama parallele Anfragen?

./rnk example_query2.txt example_docs/

Verwendetes Embedding-Modell: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama-Basis-URL: http://localhost:11434
Verarbeite Abfrage-Datei: example_query2.txt, Zielverzeichnis: example_docs/
Abfrage: Wie behandelt Ollama parallele Anfragen?
7 Dokumente gefunden
Extrahiere Abfrage-Embedding...
Verarbeite Dokumente...

=== RANGIERUNG NACH ÄHNLICHKEIT ===
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 Dokumente in 0,858s verarbeitet (Durchschnitt: 0,123s pro Dokument)

Neuranking-Test: Wie können wir das Neuranking von Dokumenten mit Ollama durchführen?

./rnk example_query3.txt example_docs/

Verwendetes Embedding-Modell: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama-Basis-URL: http://localhost:11434
Verarbeite Abfrage-Datei: example_query3.txt, Zielverzeichnis: example_docs/
Abfrage: Wie können wir das Neuranking von Dokumenten mit Ollama durchführen?
7 Dokumente gefunden
Extrahiere Abfrage-Embedding...
Verarbeite Dokumente...

=== RANGIERUNG NACH ÄHNLICHKEIT ===
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 Dokumente in 0,882s verarbeitet (Durchschnitt: 0,126s pro Dokument)

Go-Quellcode

Legen Sie alles in einen Ordner und kompilieren Sie es wie folgt:

go build -o rnk

Fühlen Sie sich frei, es für jeden unterhaltsamen oder kommerziellen Zweck zu verwenden oder es zu GitHub hochzuladen, wenn Sie möchten. MIT-Lizenz.

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 unter Verwendung von Ollama-Embeddings",
	Long:  "Ein einfaches RAG-System, das Embeddings extrahiert und Dokumente unter Verwendung von Ollama rangiert",
	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", "Zu verwendendes Embedding-Modell")
	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("Verwendetes Embedding-Modell: %s\n", embeddingModel)
	fmt.Printf("Ollama-Basis-URL: %s\n", ollamaBaseURL)
	fmt.Printf("Verarbeite Abfrage-Datei: %s, Zielverzeichnis: %s\n", queryFile, targetDir)

	// Abfrage aus Datei lesen
	query, err := readQueryFromFile(queryFile)
	if err != nil {
		log.Fatalf("Fehler beim Lesen der Abfrage-Datei: %v", err)
	}
	fmt.Printf("Abfrage: %s\n", query)

	// Alle Textdateien im Zielverzeichnis finden
	documents, err := findTextFiles(targetDir)
	if err != nil {
		log.Fatalf("Fehler beim Finden von Textdateien: %v", err)
	}
	fmt.Printf("Gefunden %d Dokumente\n", len(documents))

	// Embeddings für die Abfrage extrahieren
	fmt.Println("Extrahiere Abfrage-Embedding...")
	queryEmbedding, err := getEmbedding(query, embeddingModel, ollamaBaseURL)
	if err != nil {
		log.Fatalf("Fehler beim Abrufen des Abfrage-Embeddings: %v", err)
	}

	// Dokumente verarbeiten
	fmt.Println("Verarbeite Dokumente...")
	validDocs := make([]Document, 0)

	for _, doc := range documents {
		embedding, err := getEmbedding(doc.Content, embeddingModel, ollamaBaseURL)
		if err != nil {
			fmt.Printf("Warnung: Embedding für %s konnte nicht abgerufen werden: %v\n", doc.Path, err)
			continue
		}

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

	if len(validDocs) == 0 {
		log.Fatalf("Keine Dokumente konnten erfolgreich verarbeitet werden")
	}

	// Nach Ähnlichkeitswert sortieren (absteigend)
	sort.Slice(validDocs, func(i, j int) bool {
		return validDocs[i].Score > validDocs[j].Score
	})

	// Ergebnisse anzeigen
	fmt.Println("\n=== RANGIERUNG NACH ÄHNLICHKEIT ===")
	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("\nVerarbeitet %d Dokumente in %.3fs (Durchschnitt: %.3fs pro Dokument)\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("Warnung: Datei %s konnte nicht gelesen werden: %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-Fehler: %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 stellt die Anfrage-Payload für die Ollama-Embedding-API dar
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse stellt die Antwort der Ollama-Embedding-API dar
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document repräsentiert ein Dokument mit seinen Metadaten
type Document struct {
	Path    string
	Content string
	Score   float64
}