Ollama 와 Qwen3 임베딩 LLM 을 활용한 텍스트 재랭킹 - Go 로 구현
RAG 구현 중이신가요? Golang 코드 스니펫을 소개합니다.
이 작은 Go 코드 예시는 쿼리와 각 후보 문서에 대해 임베딩을 생성하기 위해 Ollama 를 호출합니다 그리고 코사인 유사도 기준으로 내림차순으로 정렬합니다.
우리는 이미 유사한 작업을 수행한 적이 있습니다 - 임베딩 모델을 활용한 재순위화 하지만 그것은 Python 으로 작성되었으며 다른 LLM 을 사용했고 거의 1 년 전의 일입니다.
검색 증강 생성 (RAG) 시스템 구축에 대한 전체 가이드는 검색 증강 생성 (RAG) 튜토리얼: 아키텍처, 구현 및 프로덕션 가이드 를 참조하세요.
또 다른 유사한 코드이지만 Qwen3 Reranker 를 사용하는 예시는 다음과 같습니다:

TL;DR
결과는 매우 훌륭하며, 속도는 문서당 0.128 초입니다. 질문도 하나의 문서로 계산됩니다. 정렬과 출력 시간도 이 통계에 포함되어 있습니다.
LLM 메모리 사용량:
디스크 (ollama ls) 상의 모델 크기는 3GB 미만이더라도
dengcao/Qwen3-Embedding-4B:Q5_K_M 7e8c9ad6885b 2.9 GB
GPU VRAM 에서는 (조금 더) 많은 5.5GB를 차지합니다. (ollama ps)
NAME ID SIZE
dengcao/Qwen3-Embedding-4B:Q5_K_M 7e8c9ad6885b 5.5 GB
8GB GPU 를 사용한다면 문제없을 것입니다.
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/
Using embedding model: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama base URL: http://localhost:11434
Processing query file: example_query.txt, target directory: example_docs/
Query: What is artificial intelligence and how does machine learning work?
Found 7 documents
Extracting query embedding...
Processing documents...
=== RANKING BY SIMILARITY ===
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)
Processed 7 documents in 0.899s (avg: 0.128s per document)
재순위화 테스트: Ollama 는 병렬 요청을 어떻게 처리합니까?
./rnk example_query2.txt example_docs/
Using embedding model: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama base URL: http://localhost:11434
Processing query file: example_query2.txt, target directory: example_docs/
Query: How ollama handles parallel requests?
Found 7 documents
Extracting query embedding...
Processing documents...
=== RANKING BY SIMILARITY ===
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)
Processed 7 documents in 0.858s (avg: 0.123s per document)
재순위화 테스트: Ollama 를 사용하여 문서의 재순위화는 어떻게 수행할 수 있습니까?
./rnk example_query3.txt example_docs/
Using embedding model: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama base URL: http://localhost:11434
Processing query file: example_query3.txt, target directory: example_docs/
Query: How can we do the reranking of the document with ollama?
Found 7 documents
Extracting query embedding...
Processing documents...
=== RANKING BY SIMILARITY ===
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)
Processed 7 documents in 0.882s (avg: 0.126s per document)
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
}
유용한 링크
- Ollama 치트시트
- Ollama 와 Qwen3 Reranker 모델을 활용한 텍스트 문서 재순위화 - Go 로 구현
- Ollama 의 Qwen3 임베딩 및 Reranker 모델: 차세대 성능
- https://en.wikipedia.org/wiki/Retrieval-augmented_generation
- Ollama 모델 위치 설치 및 설정
- LLM 을 위한 효과적인 프롬프트 작성법
- Ollama 에서 LLM 테스트: gemma2, qwen2 및 Mistral Nemo
- LLM 비교: Mistral Small, Gemma 2, Qwen 2.5, Mistral Nemo, LLama3 및 Phi - Ollama 에서
- 테스트: Ollama 가 Intel CPU 성능 및 효율 코어 활용 방식
- Python 으로 Ollama 에서 임베딩 모델을 활용한 재순위화
- LLM 요약 능력 비교
- 클라우드 LLM 제공업체