Ollama와 Qwen3 Embedding LLM을 사용한 텍스트 재정렬 - Go로

RAG을 구현 중이시다면, 여기 Golang에서 사용할 수 있는 코드 스니펫 몇 가지가 있습니다.

Page content

이 작은
Reranking Go 코드 예제는 Ollama를 호출하여 쿼리와 각 후보 문서에 대한 임베딩을 생성
그런 다음 코사인 유사도에 따라 내림차순으로 정렬합니다.

우리는 이미 유사한 활동을 수행했습니다 - 임베딩 모델을 사용한 Reranking 하지만 이전에는 파이썬을 사용했고, 다른 LLM과 거의 1년 전에 수행했습니다.

또 다른 유사한 코드지만 Qwen3 Reranker를 사용한 예시입니다:

다른 높이의	llamas - Ollama와 함께 Reranking

TL;DR

결과는 매우 좋습니다, 문서당 속도는 0.128초입니다. 질문은 문서로 간주됩니다. 정렬 및 출력도 이 통계에 포함됩니다.

LLM 메모리 소비: 모델 크기가 SSD(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에서 임베딩을 사용한 Reranking 테스트 - 샘플 출력

3가지 테스트 사례 모두에서 dengcao/Qwen3-Embedding-4B:Q5_K_M Ollama 모델을 사용한 임베딩 기반 Reranking은 매우 훌륭했습니다! 자신의 눈으로 확인해 보세요.

우리는 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
Ollama 기본 URL: 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
Ollama 기본 URL: 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 테스트: Ollama를 사용하여 문서의 Reranking을 어떻게 수행할 수 있는가?

./rnk example_query3.txt example_docs/

임베딩 모델 사용: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama 기본 URL: http://localhost:11434
쿼리 파일 처리: example_query3.txt, 대상 디렉토리: example_docs/
쿼리: Ollama를 사용하여 문서의 Reranking을 어떻게 수행할 수 있는가?
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: "Ollama 임베딩을 사용한 RAG 시스템",
	Long:  "Ollama를 사용하여 임베딩을 추출하고 문서를 순위 매기는 간단한 RAG 시스템",
	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", "Ollama 기본 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("임베딩 모델 사용: %s\n", embeddingModel)
	fmt.Printf("Ollama 기본 URL: %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("ollama API 오류: %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는 Ollama 임베딩 API 요청 페이로드를 나타냅니다
type OllamaEmbeddingRequest struct {
	Model  string `json:"model"`
	Prompt string `json:"prompt"`
}

// OllamaEmbeddingResponse는 Ollama 임베딩 API 응답을 나타냅니다
type OllamaEmbeddingResponse struct {
	Embedding []float64 `json:"embedding"`
}

// Document는 문서의 메타데이터를 나타냅니다
type Document struct {
	Path    string
	Content string
	Score   float64
}

유용한 링크