Ponowne rankowanie tekstów przy użyciu Ollama i Qwen3 Embedding LLM – w Go

Wdrażasz RAG? Oto kilka fragmentów kodu w języku Golang.

Page content

Ten niewielki przykład kodu Go do rerankingu wywołuje Ollamę do generowania wektorów dla zapytania oraz dla każdego dokumentu kandydackiego, następnie sortuje wyniki malejąco według podobieństwa kosinusowego.

Już wcześniej wykonaliśmy podobną aktywność – Reranking z modelami embeddingowymi, ale wtedy w Pythonie, z innym LLM i prawie rok temu.

Dla kompletnego przewodnika o budowaniu systemów generacji z wzmocnieniem odzyskiwaniem (RAG), zobacz Tutorial Retrieval-Augmented Generation (RAG): Architektura, Implementacja i Przewodnik Produkcyjny.

Kolejny podobny kod, ale używający modelu Qwen3 Reranker:

llamy o różnych wysokościach - reranking z ollamą

TL;DR

Wynik wygląda bardzo dobrze, szybkość wynosi 0,128 s na dokument. Zapytanie jest liczone jako dokument. Do tego statystyki uwzględnione są również sortowanie i wyświetlanie.

Zużycie pamięci LLM: Mimo że rozmiar modelu na dysku (ollama ls) jest mniejszy niż 3 GB

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

W pamięci GPU VRAM zajmuje (nieco) więcej: 5,5 GB. (ollama ps)

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

Jeśli masz GPU z 8 GB, powinno być OK.

Testowanie rerankingu z embeddingami na Ollamie - Przykładowy wynik

We wszystkich trzech przypadkach testowych reranking z embeddingami przy użyciu modelu ollama dengcao/Qwen3-Embedding-4B:Q5_K_M był świetny! Sprawdźcie to sami.

Mamy 7 plików zawierających teksty opisujące, co sugeruje ich nazwa:

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

wykonywane testy:

Test rerankingu: Co to jest sztuczna inteligencja i jak działa uczenie maszynowe?

./rnk example_query.txt example_docs/

Używanie modelu embeddingowego: dengcao/Qwen3-Embedding-4B:Q5_K_M
Bazowy URL Ollama: http://localhost:11434
Przetwarzanie pliku zapytania: example_query.txt, katalog docelowy: example_docs/
Zapytanie: Co to jest sztuczna inteligencja i jak działa uczenie maszynowe?
Znaleziono 7 dokumentów
Pobieranie wektora zapytania...
Przetwarzanie dokumentów...

=== RANKING WEDŁUG PODOBIENSTWA ===
1. example_docs/ai_introduction.txt (Wynik: 0.451)
2. example_docs/machine_learning.md (Wynik: 0.388)
3. example_docs/qwen3-reranking-models.md (Wynik: 0.354)
4. example_docs/ollama-parallelism.md (Wynik: 0.338)
5. example_docs/ollama-reranking-models.md (Wynik: 0.318)
6. example_docs/programming_basics.txt (Wynik: 0.296)
7. example_docs/setup.log (Wynik: 0.282)

Przetworzono 7 dokumentów w 0.899s (średnio: 0.128s na dokument)

Test rerankingu: Jak Ollama obsługuje równoległe żądania?

./rnk example_query2.txt example_docs/

Używanie modelu embeddingowego: dengcao/Qwen3-Embedding-4B:Q5_K_M
Bazowy URL Ollama: http://localhost:11434
Przetwarzanie pliku zapytania: example_query2.txt, katalog docelowy: example_docs/
Zapytanie: Jak Ollama obsługuje równoległe żądania?
Znaleziono 7 dokumentów
Pobieranie wektora zapytania...
Przetwarzanie dokumentów...

=== RANKING WEDŁUG PODOBIENSTWA ===
1. example_docs/ollama-parallelism.md (Wynik: 0.557)
2. example_docs/qwen3-reranking-models.md (Wynik: 0.532)
3. example_docs/ollama-reranking-models.md (Wynik: 0.498)
4. example_docs/ai_introduction.txt (Wynik: 0.366)
5. example_docs/machine_learning.md (Wynik: 0.332)
6. example_docs/programming_basics.txt (Wynik: 0.307)
7. example_docs/setup.log (Wynik: 0.257)

Przetworzono 7 dokumentów w 0.858s (średnio: 0.123s na dokument)

Test rerankingu: Jak wykonać reranking dokumentów z Ollamą?

./rnk example_query3.txt example_docs/

Używanie modelu embeddingowego: dengcao/Qwen3-Embedding-4B:Q5_K_M
Bazowy URL Ollama: http://localhost:11434
Przetwarzanie pliku zapytania: example_query3.txt, katalog docelowy: example_docs/
Zapytanie: Jak wykonać reranking dokumentów z Ollamą?
Znaleziono 7 dokumentów
Pobieranie wektora zapytania...
Przetwarzanie dokumentów...

=== RANKING WEDŁUG PODOBIENSTWA ===
1. example_docs/ollama-reranking-models.md (Wynik: 0.552)
2. example_docs/ollama-parallelism.md (Wynik: 0.525)
3. example_docs/qwen3-reranking-models.md (Wynik: 0.524)
4. example_docs/ai_introduction.txt (Wynik: 0.369)
5. example_docs/machine_learning.md (Wynik: 0.346)
6. example_docs/programming_basics.txt (Wynik: 0.316)
7. example_docs/setup.log (Wynik: 0.279)

Przetworzono 7 dokumentów w 0.882s (średnio: 0.126s na dokument)

Kod źródłowy Go

Umieść wszystko w folderze i skompiluj tak:

go build -o rnk

Zachęcam do użycia tego w celach rozrywkowych lub komercyjnych lub wrzucenia na GitHub, jeśli chcesz. Licencja 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: "System RAG używający embeddingów Ollama",
	Long:  "Prosty system RAG, który ekstrahuje embeddingi i rankuje dokumenty używając 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", "Model embeddingowy do użycia")
	rootCmd.Flags().StringVarP(&ollamaBaseURL, "url", "u", "http://localhost:11434", "Bazowy URL 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("Używanie modelu embeddingowego: %s\n", embeddingModel)
	fmt.Printf("Bazowy URL Ollama: %s\n", ollamaBaseURL)
	fmt.Printf("Przetwarzanie pliku zapytania: %s, katalog docelowy: %s\n", queryFile, targetDir)

	// Odczytanie zapytania z pliku
	query, err := readQueryFromFile(queryFile)
	if err != nil {
		log.Fatalf("Błąd odczytu pliku zapytania: %v", err)
	}
	fmt.Printf("Zapytanie: %s\n", query)

	// Znalezienie wszystkich plików tekstowych w katalogu docelowym
	documents, err := findTextFiles(targetDir)
	if err != nil {
		log.Fatalf("Błąd wyszukiwania plików tekstowych: %v", err)
	}
	fmt.Printf("Znaleziono %d dokumentów\n", len(documents))

	// Ekstrakcja embeddingów dla zapytania
	fmt.Println("Pobieranie wektora zapytania...")
	queryEmbedding, err := getEmbedding(query, embeddingModel, ollamaBaseURL)
	if err != nil {
		log.Fatalf("Błąd pobierania wektora zapytania: %v", err)
	}

	// Przetwarzanie dokumentów
	fmt.Println("Przetwarzanie dokumentów...")
	validDocs := make([]Document, 0)

	for _, doc := range documents {
		embedding, err := getEmbedding(doc.Content, embeddingModel, ollamaBaseURL)
		if err != nil {
			fmt.Printf("Ostrzeżenie: Nie udało się pobrać wektora dla %s: %v\n", doc.Path, err)
			continue
		}

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

	if len(validDocs) == 0 {
		log.Fatalf("Żaden dokument nie mógł zostać pomyślnie przetworzony")
	}

	// Sortowanie według wyniku podobieństwa (malejąco)
	sort.Slice(validDocs, func(i, j int) bool {
		return validDocs[i].Score > validDocs[j].Score
	})

	// Wyświetlenie wyników
	fmt.Println("\n=== RANKING WEDŁUG PODOBIENSTWA ===")
	for i, doc := range validDocs {
		fmt.Printf("%d. %s (Wynik: %.3f)\n", i+1, doc.Path, doc.Score)
	}

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

	fmt.Printf("\nPrzetworzono %d dokumentów w %.3fs (średnio: %.3fs na 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("Ostrzeżenie: Nie można odczytać pliku %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("błąd 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 reprezentuje obciążenie żądania dla API embeddingowego Ollama
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse reprezentuje odpowiedź od API embeddingowego Ollama
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document reprezentuje dokument z jego metadanymi
type Document struct {
	Path    string
	Content string
	Score   float64
}

Przydatne linki