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

Реализуете RAG? Вот несколько фрагментов кода на языке Golang.

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

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

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

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

llamas разной высоты - reranking с ollama

TL;DR

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

Потребление памяти LLM: Даже если размер модели на SSD (ollama ls) меньше 3ГБ

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

На GPU VRAM это занимает (немного) больше: 5.5ГБ. (ollama ps)

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

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

Тестирование reranking с вложениями на Ollama - Пример вывода

Во всех трех тестовых случаях reranking с вложениями с использованием модели 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

тестовые запуски:

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

./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с на документ)

Тест reranking: Как 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с на документ)

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

./rnk example_query3.txt example_docs/

Используется модель вложений: dengcao/Qwen3-Embedding-4B:Q5_K_M
Базовый URL Ollama: http://localhost:11434
Обработка файла запроса: example_query3.txt, целевая директория: example_docs/
Запрос: Как мы можем выполнить reranking документа с помощью 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 система, использующая вложения Ollama",
	Long:  "Простая RAG система, которая извлекает вложения и ранжирует документы с помощью 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", "Модель вложений для использования")
	rootCmd.Flags().StringVarP(&ollamaBaseURL, "url", "u", "http://localhost:11434", "Базовый 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("Используется модель вложений: %s\n", embeddingModel)
	fmt.Printf("Базовый URL Ollama: %s\n", ollamaBaseURL)
	fmt.Printf("Обработка файла запроса: %s, целевая директория: %s\n", queryFile, targetDir)

	// Чтение запроса из файла
	query, err := readQueryFromFile(queryFile)
	if err != nil {
		log.Fatalf("Ошибка чтения файла запроса: %v", err)
	}
	fmt.Printf("Запрос: %s\n", query)

	// Поиск всех текстовых файлов в целевой директории
	documents, err := findTextFiles(targetDir)
	if err != nil {
		log.Fatalf("Ошибка поиска текстовых файлов: %v", err)
	}
	fmt.Printf("Найдено %d документов\n", len(documents))

	// Извлечение вложений для запроса
	fmt.Println("Извлечение вложения запроса...")
	queryEmbedding, err := getEmbedding(query, embeddingModel, ollamaBaseURL)
	if err != nil {
		log.Fatalf("Ошибка получения вложения запроса: %v", err)
	}

	// Обработка документов
	fmt.Println("Обработка документов...")
	validDocs := make([]Document, 0)

	for _, doc := range documents {
		embedding, err := getEmbedding(doc.Content, embeddingModel, ollamaBaseURL)
		if err != nil {
			fmt.Printf("Предупреждение: Не удалось получить вложение для %s: %v\n", doc.Path, err)
			continue
		}

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

	if len(validDocs) == 0 {
		log.Fatalf("Ни один документ не был успешно обработан")
	}

	// Сортировка по оценке схожести (по убыванию)
	sort.Slice(validDocs, func(i, j int) bool {
		return validDocs[i].Score > validDocs[j].Score
	})

	// Отображение результатов
	fmt.Println("\n=== РАНЖИРОВАНИЕ ПО СХОЖЕСТИ ===")
	for i, doc := range validDocs {
		fmt.Printf("%d. %s (Оценка: %.3f)\n", i+1, doc.Path, doc.Score)
	}

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

	fmt.Printf("\nОбработано %d документов за %.3fs (среднее: %.3fs на документ)\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("Предупреждение: Не удалось прочитать файл %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("ошибка 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 представляет собой payload запроса для API вложений Ollama
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse представляет собой ответ от API вложений Ollama
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document представляет собой документ с его метаданными
type Document struct {
	Path    string
	Content string
	Score   float64
}

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