إعادة ترتيب النصوص باستخدام Ollama و Qwen3 Embedding LLM - بلغة Go

هل تخطط لتطبيق RAG؟ إليك بعض أكواد Golang...

Page content

هذا المثال الصغير كود Go لترتيب المعاودة يدعو Ollama لإنشاء تضمينات للمستفسر ولكل وثيقة مرشحة، ثم ترتيبها تنازليًا حسب التشابه الكسبي.

لقد قمنا بالفعل بنشاط مشابه - ترتيب المعاودة باستخدام نماذج التضمين ولكن كان ذلك في لغة Python، مع نموذج LLM مختلف تقريبًا، وقبل عام تقريبًا.

كود آخر مشابه، لكن باستخدام Qwen3 Reranker:

اللاماس بارتفاعات مختلفة - ترتيب المعاودة مع ollama

TL;DR

النتيجة تبدو جيدة جدًا، السرعة هي 0.128 ثانية لكل وثيقة. السؤال يُعد وثيقة. والتقسيم والطباعة تُحسب أيضًا ضمن هذه الإحصائيات.

استهلاك ذاكرة LLM: رغم أن حجم النموذج على sdd (ollama ls) أقل من 3 جيجابايت

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

يأخذ في ذاكرة VRAM للوحدة المُسرّعة (ليس قليلاً) أكثر: 5.5 جيجابايت. (ollama ps)

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

إذا كانت لديك وحدة مُسرّعة بسعة 8 جيجابايت - يجب أن تكون في порядке جيد.

اختبار ترتيب المعاودة مع التضمينات على Ollama - مخرجات عينة

في جميع حالات الاختبار الثلاثة، كان ترتيب المعاودة مع التضمينات باستخدام نموذج ollama dengcao/Qwen3-Embedding-4B:Q5_K_M مذهلاً! انظر لنفسك.

لدينا 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 باستخدام تضمينات 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 يمثل حمولة الطلب لواجهة 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
}

روابط مفيدة