使用 Ollama 和 Qwen3 嵌入模型对文本文档进行重排序 - 使用 Go 语言
实现 RAG?这里有一些用 Golang 编写的代码片段。
目录
这个小的 Go代码示例重新排序是调用Ollama生成嵌入 用于查询和每个候选文档, 然后按余弦相似度降序排序。
我们之前做过类似的活动 - 使用嵌入模型重新排序,但那是用Python,使用不同的LLM,而且几乎一年前了。
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上使用嵌入进行重新排序测试 - 示例输出
在所有三个测试用例中,使用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
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秒)
重新排序测试: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秒)
重新排序测试:我们如何使用Ollama对文档进行重新排序?
./rnk example_query3.txt example_docs/
使用嵌入模型: dengcao/Qwen3-Embedding-4B:Q5_K_M
Ollama基础URL: 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: "使用Ollama嵌入的RAG系统",
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", "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
}
有用的链接
- Ollama速查表
- Ollama上的Qwen3嵌入和重新排序模型:最先进的性能
- https://en.wikipedia.org/wiki/Retrieval-augmented_generation
- 安装和配置Ollama模型位置
- 为LLMs编写有效的提示
- 在Ollama上测试LLMs: gemma2, qwen2和Mistral Nemo
- LLMs比较: Mistral Small, Gemma 2, Qwen 2.5, Mistral Nemo, LLama3和Phi - 在Ollama上
- 测试: Ollama如何使用Intel CPU性能和高效核心
- 在Ollama上使用嵌入模型进行重新排序(Python)
- 比较LLM的摘要能力
- 云LLM提供商