Переранжирование текстов с помощью Ollama и модели встраивания Qwen3 LLM на языке Go

Внедряете RAG? Вот несколько сниппетов на Golang..

Содержимое страницы

Этот небольшой пример кода на Go для пересортировки вызывает Ollama для генерации эмбеддингов для запроса и для каждого кандидата-документа, затем сортирует их по убыванию косинусного сходства.

Мы уже проводили подобную деятельность — Пересортировка с использованием моделей эмбеддингов, но это было на Python, с другой моделью LLM и почти год назад.

Для полного руководства по созданию систем генерации с дополнением извлечения (Retrieval-Augmented Generation) см. Учебник по Retrieval-Augmented Generation (RAG): Архитектура, реализация и руководство по внедрению.

Еще один похожий код, но с использованием Qwen3 Reranker:

llamas of different heights - reranking with ollama

TL;DR

Результат выглядит очень хорошо, скорость составляет 0.128с на документ. Вопрос считается как документ. И сортировка, и вывод также включены в эту статистику.

Потребление памяти LLM: Несмотря на то, что размер модели на диске (ollama ls) составляет менее 3 ГБ

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

В видеопамяти GPU (VRAM) она занимает (немного) больше: 5.5 ГБ. (ollama ps)

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

Если у вас GPU с 8 ГБ памяти — должно быть нормально.

Тестирование пересортировки с эмбеддингами на Ollama — Пример вывода

Во всех трех случаях тестирования пересортировка с использованием эмбеддингов модели dengcao/Qwen3-Embedding-4B:Q5_K_M в Ollama была великолепна! Убедитесь в этом сами.

У нас есть 7 файлов, содержащих текст, описывающий, что говорит их имя файла:

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

Запуски тестов:

Тест пересортировки: Что такое искусственный интеллект и как работает машинное обучение?

./rnk example_query.txt example_docs/

Используется модель эмбеддингов: dengcao/Qwen3-Embedding-4B:Q5_K_M
Базовый URL Ollama: http://localhost:11434
Обработка файла запроса: example_query.txt, целевая директория: example_docs/
Запрос: Что такое искусственный интеллект и как работает машинное обучение?
Найдено 7 документов
Извлечение эмбеддинга запроса...
Обработка документов...

=== РАНЖИРОВАНИЕ ПО СХОДСТВУ ===
1. example_docs/ai_introduction.txt (Оценка: 0.451)
2. example_docs/machine_learning.md (Оценка: 0.388)
3. example_docs/qwen3-reranking-models.md (Оценка: 0.354)
4. example_docs/ollama-parallelism.md (Оценка: 0.338)
5. example_docs/ollama-reranking-models.md (Оценка: 0.318)
6. example_docs/programming_basics.txt (Оценка: 0.296)
7. example_docs/setup.log (Оценка: 0.282)

Обработано 7 документов за 0.899с (в среднем: 0.128с на документ)

Тест пересортировки: Как Ollama обрабатывает параллельные запросы?

./rnk example_query2.txt example_docs/

Используется модель эмбеддингов: dengcao/Qwen3-Embedding-4B:Q5_K_M
Базовый URL Ollama: http://localhost:11434
Обработка файла запроса: example_query2.txt, целевая директория: example_docs/
Запрос: Как Ollama обрабатывает параллельные запросы?
Найдено 7 документов
Извлечение эмбеддинга запроса...
Обработка документов...

=== РАНЖИРОВАНИЕ ПО СХОДСТВУ ===
1. example_docs/ollama-parallelism.md (Оценка: 0.557)
2. example_docs/qwen3-reranking-models.md (Оценка: 0.532)
3. example_docs/ollama-reranking-models.md (Оценка: 0.498)
4. example_docs/ai_introduction.txt (Оценка: 0.366)
5. example_docs/machine_learning.md (Оценка: 0.332)
6. example_docs/programming_basics.txt (Оценка: 0.307)
7. example_docs/setup.log (Оценка: 0.257)

Обработано 7 документов за 0.858с (в среднем: 0.123с на документ)

Тест пересортировки: Как выполнить пересортировку документа с помощью Ollama?

./rnk example_query3.txt example_docs/

Используется модель эмбеддингов: dengcao/Qwen3-Embedding-4B:Q5_K_M
Базовый URL Ollama: http://localhost:11434
Обработка файла запроса: example_query3.txt, целевая директория: example_docs/
Запрос: Как выполнить пересортировку документа с помощью Ollama?
Найдено 7 документов
Извлечение эмбеддинга запроса...
Обработка документов...

=== РАНЖИРОВАНИЕ ПО СХОДСТВУ ===
1. example_docs/ollama-reranking-models.md (Оценка: 0.552)
2. example_docs/ollama-parallelism.md (Оценка: 0.525)
3. example_docs/qwen3-reranking-models.md (Оценка: 0.524)
4. example_docs/ai_introduction.txt (Оценка: 0.369)
5. example_docs/machine_learning.md (Оценка: 0.346)
6. example_docs/programming_basics.txt (Оценка: 0.316)
7. example_docs/setup.log (Оценка: 0.279)

Обработано 7 документов за 0.882с (в среднем: 0.126с на документ)

Исходный код на Go

Поместите все это в папку и скомпилируйте так:

go build -o rnk

Вы можете использовать его в любых развлекательных или коммерческих целях или загрузить на GitHub, если хотите. Лицензия 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
}

Полезные ссылки