داشبورد توسعه‌دهنده
پرسش از هوش مصنوعی
پرسش از هوش مصنوعی

بهترین شیوه‌های RAG

بازیابی-تقویت شده با تولید (Retrieval-Augmented Generation یا RAG) یک تکنیک قدرتمند است که مدل‌های زبانی بزرگ (LLM) را با ارائه دانش خارجی به آن‌ها تقویت می‌کند. این راهنما بهترین شیوه‌های پیاده‌سازی خطوط لوله RAG کارآمد و موثر با استفاده از AvalAI را پوشش می‌دهد.

مقدمه‌ای بر RAG

RAG نقاط قوت رویکردهای مبتنی بر بازیابی و تولید را ترکیب می‌کند تا پاسخ‌های دقیق‌تر، واقعی‌تر و مرتبط با متن ایجاد کند. به جای اتکا صرفا به دانش رمزگذاری شده در پارامترهای مدل، RAG اطلاعات مرتبط را از منابع خارجی بازیابی می‌کند و از آن برای تقویت زمینه ارائه شده به مدل زبانی استفاده می‌کند.

این راهنما با اقتباس از Best Practices for RAG Pipeline و با تغییراتی برای پیاده‌سازی AvalAI تهیه شده است.

مزایای کلیدی RAG

  • دقت بهبود یافته: دسترسی به اطلاعات به‌روز و خاص دامنه
  • کاهش توهمات: پایه‌گذاری پاسخ‌ها بر اساس اطلاعات واقعی
  • قابلیت گسترش دانش: به‌روزرسانی آسان دانش بدون نیاز به آموزش مجدد مدل
  • استناد منبع: توانایی ذکر منابع اطلاعات

اجزای جریان کاری RAG

یک جریان کاری RAG معمولی شامل چندین جز کلیدی است:

جریان کاری RAG
  1. طبقه‌بندی پرس و جو: تعیین اینکه آیا بازیابی خارجی مورد نیاز است
  2. تکه‌بندی اسناد: شکستن اسناد به قطعات قابل مدیریت
  3. تولید امبدینگ: تبدیل متن به نمایش‌های برداری
  4. ذخیره‌سازی برداری: ذخیره‌سازی و جستجوی کارآمد امبدینگ‌ها
  5. بازیابی: یافتن اسناد مرتبط برای یک پرس و جوی خاص
  6. رتبه‌بندی مجدد: بهبود ارتباط اسناد بازیابی شده
  7. بسته‌بندی مجدد: سازماندهی اسناد برای استفاده بهینه از زمینه
  8. خلاصه‌سازی: فشرده‌سازی اطلاعات برای تناسب با پنجره‌های زمینه
  9. تولید: تولید پاسخ نهایی

بیایید هر جز را به همراه بهترین شیوه‌ها به تفصیل بررسی کنیم.

1. طبقه‌بندی پرس و جو

همه پرس و جوها به بازیابی خارجی نیاز ندارند. پیاده‌سازی طبقه‌بندی پرس و جو می‌تواند با تعیین زمان استفاده از RAG، عملکرد را بهینه کند.

بهترین شیوه‌ها

  • از یک طبقه‌بندی‌کننده برای شناسایی پرس و جوهایی که به اطلاعات خارجی نیاز دارند استفاده کنید
  • برای پرس و جوهایی که می‌توانند با دانش داخلی مدل پاسخ داده شوند، از بازیابی صرف نظر کنید
  • هنگام تعیین ضرورت بازیابی، نوع وظیفه را در نظر بگیرید
python
from openai import OpenAI

client = OpenAI(
    api_key="your-avalai-api-key",
    base_url="https://api.avalai.ir/v1",  # نقطه پایانی AvalAI API
)


def classify_query(query):
    """تعیین می‌کند که آیا یک پرس و جو نیاز به بازیابی خارجی دارد."""
    response = client.chat.completions.create(
        model="gpt-5.5",
        messages=[
            {
                "role": "system",
                "content": "You are a query classifier. Respond with 'RETRIEVE' if the query requires external knowledge, or 'SUFFICIENT' if the model's knowledge is enough.",
            },
            {"role": "user", "content": query},
        ],
    )
    classification = response.choices[0].message.content
    return "RETRIEVE" in classification


# مثال استفاده
query = "What were the key announcements at AvalAI's 2025 developer conference?"
if classify_query(query):
    # ادامه با جریان کاری RAG
    print("درحال بازیابی اطلاعات خارجی...")
else:
    # استفاده از تکمیل استاندارد
    print("درحال استفاده از دانش داخلی مدل...")
javascript
import { OpenAI } from "openai";

const client = new OpenAI({
  apiKey: process.env.AVALAI_API_KEY,
  baseURL: "https://api.avalai.ir/v1", // نقطه پایانی AvalAI API
});

async function classifyQuery(query) {
  // تعیین می‌کند که آیا یک پرس و جو نیاز به بازیابی خارجی دارد
  const response = await client.chat.completions.create({
    model: "gpt-5.5",
    messages: [
      {
        role: "system",
        content:
          "You are a query classifier. Respond with 'RETRIEVE' if the query requires external knowledge, or 'SUFFICIENT' if the model's knowledge is enough.",
      },
      { role: "user", content: query },
    ],
  });

  const classification = response.choices[0].message.content;
  return classification.includes("RETRIEVE");
}

// مثال استفاده
async function processQuery(query) {
  const needsRetrieval = await classifyQuery(query);

  if (needsRetrieval) {
    // ادامه با جریان کاری RAG
    console.log("درحال بازیابی اطلاعات خارجی...");
  } else {
    // استفاده از تکمیل استاندارد
    console.log("درحال استفاده از دانش داخلی مدل...");
  }
}

processQuery(
  "What were the key announcements at AvalAI's 2025 developer conference?",
);
bash
# استفاده از cURL برای طبقه‌بندی یک پرس و جو
curl https://api.avalai.ir/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AVALAI_API_KEY" \
  -d '{
        "model": "gpt-5.5",
        "messages": [
            {
                "role": "system",
                "content": "You are a query classifier. Respond with '\''RETRIEVE'\'' if the query requires external knowledge, or '\''SUFFICIENT'\'' if the model'\''s knowledge is enough."
            },
            {
                "role": "user",
                "content": "What were the key announcements at AvalAI'\''s 2025 developer conference?"
            }
        ]
    }'
go
package main

import (
	"context"
	"fmt"
	openai "github.com/openai/openai-go" // کتابخانه رسمی OpenAI Go
	"strings"
)

func classifyQuery(client *openai.Client, query string) (bool, error) {
	// تعیین می‌کند که آیا یک پرس و جو نیاز به بازیابی خارجی دارد
	resp, err := client.CreateChatCompletion(
		context.Background(),
		openai.ChatCompletionRequest{
			Model: "gpt-5.5",
			Messages: []openai.ChatCompletionMessage{
				{
					Role:    openai.ChatMessageRoleSystem,
					Content: "You are a query classifier. Respond with 'RETRIEVE' if the query requires external knowledge, or 'SUFFICIENT' if the model's knowledge is enough.",
				},
				{
					Role:    openai.ChatMessageRoleUser,
					Content: query,
				},
			},
		},
	)

	if err != nil {
		return false, err
	}

	classification := resp.Choices[0].Message.Content
	return strings.Contains(classification, "RETRIEVE"), nil
}

func main() {
	client := openai.NewClient("YOUR_AVALAI_API_KEY")
	client.BaseURL = "https://api.avalai.ir/v1" // نقطه پایانی AvalAI API

	query := "What were the key announcements at AvalAI's 2025 developer conference?"
	needsRetrieval, err := classifyQuery(client, query)

	if err != nil {
		fmt.Printf("خطا: %v\n", err)
		return
	}

	if needsRetrieval {
		// ادامه با جریان کاری RAG
		fmt.Println("درحال بازیابی اطلاعات خارجی...")
	} else {
		// استفاده از تکمیل استاندارد
		fmt.Println("درحال استفاده از دانش داخلی مدل...")
	}
}
php
<?php
// استفاده از PHP برای طبقه‌بندی یک پرس و جو

$apiKey = getenv('AVALAI_API_KEY');
$apiUrl = 'https://api.avalai.ir/v1/chat/completions';

function classifyQuery($query, $apiKey, $apiUrl) {
    $data = [
        'model' => 'gpt-5.5',
        'messages' => [
            [
                'role' => 'system',
                'content' => "You are a query classifier. Respond with 'RETRIEVE' if the query requires external knowledge, or 'SUFFICIENT' if the model's knowledge is enough."
            ],
            [
                'role' => 'user',
                'content' => $query
            ]
        ]
    ];

    $jsonData = json_encode($data);

    $ch = curl_init($apiUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Content-Type: application/json',
        'Authorization: Bearer ' . $apiKey,
        'Content-Length: ' . strlen($jsonData)
    ]);

    $response = curl_exec($ch);
    $err = curl_error($ch);
    curl_close($ch);

    if ($err) {
        throw new Exception("خطای cURL: " . $err);
    }

    $responseData = json_decode($response, true);
    $classification = $responseData['choices'][0]['message']['content'];

    return strpos($classification, 'RETRIEVE') !== false;
}

// مثال استفاده
$query = "What were the key announcements at AvalAI's 2025 developer conference?";
try {
    $needsRetrieval = classifyQuery($query, $apiKey, $apiUrl);

    if ($needsRetrieval) {
        // ادامه با جریان کاری RAG
        echo "درحال بازیابی اطلاعات خارجی...";
    } else {
        // استفاده از تکمیل استاندارد
        echo "درحال استفاده از دانش داخلی مدل...";
    }
} catch (Exception $e) {
    echo "خطا: " . $e->getMessage();
}
?>
نسخه معادل Responses API

وقتی مدل انتخابی از /v1/responses پشتیبانی می‌کند، این نسخه را کنار مثال Chat Completions استفاده کنید. messages به input منتقل می‌شود و متن نهایی از response.output_text خوانده می‌شود.

python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["AVALAI_API_KEY"],
    base_url="https://api.avalai.ir/v1",
)

response = client.responses.create(
    model="gpt-5.5",
    instructions="You are a helpful assistant.",
    input="What were the key announcements at AvalAI",
)

print(response.output_text)
javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.AVALAI_API_KEY,
  baseURL: "https://api.avalai.ir/v1",
});

const response = await client.responses.create({
  model: "gpt-5.5",
  instructions: "You are a helpful assistant.",
  input: "What were the key announcements at AvalAI",
});

console.log(response.output_text);
bash
curl https://api.avalai.ir/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AVALAI_API_KEY" \
  -d '
  {
    "model": "gpt-5.5",
    "input": "What were the key announcements at AvalAI",
    "instructions": "You are a helpful assistant."
  }'
  • messagesinput
  • پیام سیستمی → instructions یا آیتم developer
  • choices[0].message.contentresponse.output_text
  • برای ابزارها و خروجی‌های چندوجهی، response.output را بر اساس type بررسی کنید.

2. تکه‌بندی اسناد

شکستن اسناد به تکه‌های مناسب برای بازیابی موثر بسیار مهم است. استراتژی تکه‌بندی هم بر دقت بازیابی و هم بر کارایی پردازش تاثیر می‌گذارد.

استراتژی‌های تکه‌بندی

  1. تکه‌بندی در سطح توکن: تقسیم بر اساس تعداد ثابت توکن
  2. تکه‌بندی در سطح جمله: شکستن در مرزهای جمله
  3. تکه‌بندی در سطح معنایی: استفاده از LLM‌ها برای شناسایی نقاط شکست طبیعی

بهترین شیوه‌ها

  • اندازه تکه: از 512-1024 توکن برای تعادل بهینه بین زمینه و ارتباط استفاده کنید
  • همپوشانی: 10-20٪ همپوشانی بین تکه‌ها برای حفظ زمینه در نظر بگیرید
  • حفظ معنا: سعی کنید محتوای مرتبط معنایی را با هم نگه دارید
  • متادیتا: تکه‌ها را با عناوین، سرفصل‌ها و سایر متادیتاها تقویت کنید
python
from openai import OpenAI
import nltk
from nltk.tokenize import sent_tokenize

# اگر منابع NLTK قبلا در دسترس نیستند، آن‌ها را دانلود کنید
nltk.download("punkt")


def chunk_document_by_sentences(document, max_chunk_size=512, overlap=20):
    """
    یک سند را بر اساس جملات با همپوشانی مشخص شده تکه‌بندی می‌کند.

    Args:
            document: سند متنی برای تکه‌بندی
            max_chunk_size: حداکثر اندازه تکه به توکن (تقریبی)
            overlap: تعداد جملات همپوشانی بین تکه‌ها

    Returns:
            لیستی از تکه‌های سند
    """
    # تقسیم سند به جملات
    sentences = sent_tokenize(document)

    chunks = []
    current_chunk = []
    current_size = 0

    for sentence in sentences:
        # تعداد تقریبی توکن (کلمات + علائم نگارشی)
        sentence_size = len(sentence.split())

        if current_size + sentence_size > max_chunk_size and current_chunk:
            # ذخیره تکه فعلی
            chunks.append(" ".join(current_chunk))

            # حفظ جملات همپوشانی برای تکه بعدی
            if overlap > 0:
                overlap_sentences = current_chunk[-min(overlap, len(current_chunk)) :]
                current_chunk = overlap_sentences
                current_size = sum(len(s.split()) for s in overlap_sentences)
            else:
                current_chunk = []
                current_size = 0

        current_chunk.append(sentence)
        current_size += sentence_size

    # اضافه کردن آخرین تکه اگر خالی نیست
    if current_chunk:
        chunks.append(" ".join(current_chunk))

    return chunks


# مثال استفاده
document = """
AvalAI provides access to a wide range of language models through a unified API.
This makes it easy to experiment with different models and choose the best one for your use case.
The platform supports models from various providers including OpenAI, Anthropic, Google, and more.
Each model has different capabilities and pricing, so it's important to understand the tradeoffs.
AvalAI also provides tools for monitoring usage, managing costs, and ensuring compliance with usage policies.
"""

chunks = chunk_document_by_sentences(document, max_chunk_size=100, overlap=1)
for i, chunk in enumerate(chunks):
    print(f"تکه {i+1}: {chunk}")
javascript
// تکه‌بندی سند بر اساس جملات با همپوشانی
import { OpenAI } from "openai";
import natural from "natural";

const tokenizer = new natural.SentenceTokenizer();

function chunkDocumentBySentences(document, maxChunkSize = 512, overlap = 20) {
  // تقسیم سند به جملات
  const sentences = tokenizer.tokenize(document);

  const chunks = [];
  let currentChunk = [];
  let currentSize = 0;

  for (const sentence of sentences) {
    // تعداد تقریبی توکن (کلمات + علائم نگارشی)
    const sentenceSize = sentence.split(/\s+/).length;

    if (currentSize + sentenceSize > maxChunkSize && currentChunk.length > 0) {
      // ذخیره تکه فعلی
      chunks.push(currentChunk.join(" "));

      // حفظ جملات همپوشانی برای تکه بعدی
      if (overlap > 0) {
        const overlapSentences = currentChunk.slice(
          -Math.min(overlap, currentChunk.length),
        );
        currentChunk = overlapSentences;
        currentSize = overlapSentences.reduce(
          (sum, s) => sum + s.split(/\s+/).length,
          0,
        );
      } else {
        currentChunk = [];
        currentSize = 0;
      }
    }

    currentChunk.push(sentence);
    currentSize += sentenceSize;
  }

  // اضافه کردن آخرین تکه اگر خالی نیست
  if (currentChunk.length > 0) {
    chunks.push(currentChunk.join(" "));
  }

  return chunks;
}

// مثال استفاده
const document = `
AvalAI provides access to a wide range of language models through a unified API.
This makes it easy to experiment with different models and choose the best one for your use case.
The platform supports models from various providers including OpenAI, Anthropic, Google, and more.
Each model has different capabilities and pricing, so it's important to understand the tradeoffs.
AvalAI also provides tools for monitoring usage, managing costs, and ensuring compliance with usage policies.
`;

const chunks = chunkDocumentBySentences(document, 100, 1);
chunks.forEach((chunk, i) => {
  console.log(`تکه ${i + 1}: ${chunk}`);
});
bash
# استفاده از یک ابزار خارجی برای تکه‌بندی سند
# این مثال از پایتون در یک اسکریپت شل استفاده می‌کند

cat >chunk_document.py <<'EOF'
import sys
import nltk
from nltk.tokenize import sent_tokenize

# دانلود منابع NLTK
nltk.download('punkt', quiet=True)

def chunk_document(text, max_size=512, overlap=20):
	sentences = sent_tokenize(text)

	chunks = []
	current_chunk = []
	current_size = 0

	for sentence in sentences:
		sentence_size = len(sentence.split())

		if current_size + sentence_size > max_size and current_chunk:
			chunks.append(" ".join(current_chunk))

			if overlap > 0:
				overlap_sentences = current_chunk[-min(overlap, len(current_chunk)):]
				current_chunk = overlap_sentences
				current_size = sum(len(s.split()) for s in overlap_sentences)
			else:
				current_chunk = []
				current_size = 0

		current_chunk.append(sentence)
		current_size += sentence_size

	if current_chunk:
		chunks.append(" ".join(current_chunk))

	return chunks

if __name__ == "__main__":
	text = sys.stdin.read()
	chunks = chunk_document(text, max_size=int(sys.argv[1]), overlap=int(sys.argv[2]))
	for i, chunk in enumerate(chunks):
		print(f"--- تکه {i+1} ---")
		print(chunk)
		print()
EOF

# مثال استفاده
# ابتدا یک فایل document.txt با محتوای نمونه ایجاد کنید
# echo "AvalAI provides access to a wide range of language models through a unified API. This makes it easy to experiment with different models and choose the best one for your use case." > document.txt
cat document.txt | python3 chunk_document.py 100 1
go
package main

import (
	"fmt"
	"strings"
	"unicode"

	"github.com/neurosnap/sentences" // کتابخانه برای تکه‌بندی جملات
)

// ChunkDocumentBySentences یک سند را بر اساس جملات با همپوشانی مشخص شده تکه‌بندی می‌کند
func ChunkDocumentBySentences(document string, maxChunkSize int, overlap int) []string {
	// راه‌اندازی tokenizer جمله
	tokenizer, err := sentences.NewSentenceTokenizer(nil)
	if err != nil {
		panic(err) // در یک برنامه واقعی، خطا را به شکل مناسب‌تری مدیریت کنید
	}

	// تقسیم سند به جملات
	sentenceObjects := tokenizer.Tokenize(document)
	var sentenceTexts []string
	for _, s := range sentenceObjects {
		sentenceTexts = append(sentenceTexts, s.Text)
	}

	chunks := []string{}
	currentChunk := []string{}
	currentSize := 0

	for _, sentence := range sentenceTexts {
		// تعداد تقریبی توکن (کلمات + علائم نگارشی)
		sentenceSize := len(strings.FieldsFunc(sentence, func(r rune) bool {
			return unicode.IsSpace(r)
		}))

		if currentSize+sentenceSize > maxChunkSize && len(currentChunk) > 0 {
			// ذخیره تکه فعلی
			chunks = append(chunks, strings.Join(currentChunk, " "))

			// حفظ جملات همپوشانی برای تکه بعدی
			if overlap > 0 {
				startIdx := len(currentChunk) - min(overlap, len(currentChunk))
				overlapSentences := currentChunk[startIdx:]
				currentChunk = overlapSentences

				// محاسبه مجدد اندازه فعلی
				currentSize = 0
				for _, s := range overlapSentences {
					currentSize += len(strings.FieldsFunc(s, func(r rune) bool {
						return unicode.IsSpace(r)
					}))
				}
			} else {
				currentChunk = []string{}
				currentSize = 0
			}
		}

		currentChunk = append(currentChunk, sentence)
		currentSize += sentenceSize
	}

	// اضافه کردن آخرین تکه اگر خالی نیست
	if len(currentChunk) > 0 {
		chunks = append(chunks, strings.Join(currentChunk, " "))
	}

	return chunks
}

// تابع کمکی برای min
func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func main() {
	document := `
AvalAI provides access to a wide range of language models through a unified API.
This makes it easy to experiment with different models and choose the best one for your use case.
The platform supports models from various providers including OpenAI, Anthropic, Google, and more.
Each model has different capabilities and pricing, so it's important to understand the tradeoffs.
AvalAI also provides tools for monitoring usage, managing costs, and ensuring compliance with usage policies.
`

	chunks := ChunkDocumentBySentences(document, 100, 1)
	for i, chunk := range chunks {
		fmt.Printf("تکه %d: %s\n", i+1, chunk)
	}
}
php
<?php
// تکه‌بندی سند بر اساس جملات با همپوشانی

/**
* تابع tokenizer ساده برای جملات
* توجه: برای استفاده تولیدی، از یک کتابخانه NLP قوی‌تر استفاده کنید
*/
function sentenceTokenize($text) {
	// تقسیم بر اساس نقطه، علامت تعجب و علامت سوال که با فاصله‌ها دنبال می‌شوند
	$pattern = '/(?<=[.!?])\s+(?=[A-Z])/';
	$sentences = preg_split($pattern, $text, -1, PREG_SPLIT_NO_EMPTY);

	// تمیز کردن جملات
	$result = [];
	foreach ($sentences as $sentence) {
		$sentence = trim($sentence);
		if (!empty($sentence)) {
			$result[] = $sentence;
		}
	}

	return $result;
}

function chunkDocumentBySentences($document, $maxChunkSize = 512, $overlap = 20) {
	// تقسیم سند به جملات
	$sentences = sentenceTokenize($document);

	$chunks = [];
	$currentChunk = [];
	$currentSize = 0;

	foreach ($sentences as $sentence) {
		// تعداد تقریبی توکن (کلمات + علائم نگارشی)
		$sentenceSize = count(explode(' ', $sentence));

		if ($currentSize + $sentenceSize > $maxChunkSize && count($currentChunk) > 0) {
			// ذخیره تکه فعلی
			$chunks[] = implode(' ', $currentChunk);

			// حفظ جملات همپوشانی برای تکه بعدی
			if ($overlap > 0) {
				$overlapCount = min($overlap, count($currentChunk));
				$overlapSentences = array_slice($currentChunk, -$overlapCount);
				$currentChunk = $overlapSentences;

				// محاسبه مجدد اندازه فعلی
				$currentSize = 0;
				foreach ($overlapSentences as $s) {
					$currentSize += count(explode(' ', $s));
				}
			} else {
				$currentChunk = [];
				$currentSize = 0;
			}
		}

		$currentChunk[] = $sentence;
		$currentSize += $sentenceSize;
	}

	// اضافه کردن آخرین تکه اگر خالی نیست
	if (count($currentChunk) > 0) {
		$chunks[] = implode(' ', $currentChunk);
	}

	return $chunks;
}

// مثال استفاده
$document = "
AvalAI provides access to a wide range of language models through a unified API.
This makes it easy to experiment with different models and choose the best one for your use case.
The platform supports models from various providers including OpenAI, Anthropic, Google, and more.
Each model has different capabilities and pricing, so it's important to understand the tradeoffs.
AvalAI also provides tools for monitoring usage, managing costs, and ensuring compliance with usage policies.
";

$chunks = chunkDocumentBySentences($document, 100, 1);
foreach ($chunks as $index => $chunk) {
	echo "تکه " . ($index + 1) . ": " . $chunk . "\n";
}
?>

3. تولید امبدینگ

مدل‌های امبدینگ متن را به نمایش‌های برداری تبدیل می‌کنند که معنای معنایی را ثبت می‌کند. انتخاب مدل امبدینگ مناسب برای بازیابی موثر بسیار مهم است.

بهترین شیوه‌ها

  • انتخاب مدل: از مدل‌های امبدینگ بهینه‌سازی شده برای وظایف بازیابی استفاده کنید
  • سازگاری: از یک مدل امبدینگ یکسان برای اسناد و پرس و جوها استفاده کنید
  • ابعاد: تعادل بین اندازه بردار و عملکرد (امبدینگ‌های کوچکتر سریعتر هستند اما ممکن است کمتر دقیق باشند)
  • امبدینگ‌های تخصصی: برای محتوای تخصصی، مدل‌های امبدینگ خاص دامنه را در نظر بگیرید
python
from openai import OpenAI
import numpy as np

client = OpenAI(
    api_key="your-avalai-api-key",
    base_url="https://api.avalai.ir/v1",  # نقطه پایانی AvalAI API
)


def create_embedding(text):
    """امبدینگ برای یک متن معین تولید می‌کند."""
    response = client.embeddings.create(model="text-embedding-3-large", input=text)
    return response.data[0].embedding


# مثال: ایجاد امبدینگ برای تکه‌های سند
chunks = [
    "AvalAI provides access to a wide range of language models through a unified API.",
    "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
    "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
]

# ایجاد امبدینگ برای هر تکه
chunk_embeddings = [create_embedding(chunk) for chunk in chunks]
print(f"تولید شد {len(chunk_embeddings)} امبدینگ با ابعاد {len(chunk_embeddings[0])}")

# ایجاد امبدینگ برای یک پرس و جو
query = "Which AI models does AvalAI support?"
query_embedding = create_embedding(query)


# تابع برای محاسبه شباهت (شباهت کسینوسی)
def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))


# یافتن مشابه‌ترین تکه به پرس و جو
similarities = [
    cosine_similarity(query_embedding, chunk_emb) for chunk_emb in chunk_embeddings
]
most_similar_idx = np.argmax(similarities)
print(f"مشابه‌ترین تکه: {chunks[most_similar_idx]}")
print(f"امتیاز شباهت: {similarities[most_similar_idx]:.4f}")
javascript
import { OpenAI } from "openai";

const client = new OpenAI({
  apiKey: process.env.AVALAI_API_KEY,
  baseURL: "https://api.avalai.ir/v1", // نقطه پایانی AvalAI API
});

async function createEmbedding(text) {
  // امبدینگ برای یک متن معین تولید می‌کند
  const response = await client.embeddings.create({
    model: "text-embedding-3-large",
    input: text,
  });

  return response.data[0].embedding;
}

// محاسبه شباهت کسینوسی بین دو بردار
function cosineSimilarity(a, b) {
  // ضرب داخلی
  let dotProduct = 0;
  for (let i = 0; i < a.length; i++) {
    dotProduct += a[i] * b[i];
  }

  // اندازه‌ها
  let magA = 0;
  let magB = 0;
  for (let i = 0; i < a.length; i++) {
    magA += a[i] * a[i];
    magB += b[i] * b[i];
  }

  return dotProduct / (Math.sqrt(magA) * Math.sqrt(magB));
}

async function findSimilarChunks() {
  // تکه‌های نمونه
  const chunks = [
    "AvalAI provides access to a wide range of language models through a unified API.",
    "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
    "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
  ];

  // ایجاد امبدینگ برای هر تکه
  const chunkEmbeddings = await Promise.all(
    chunks.map((chunk) => createEmbedding(chunk)),
  );

  console.log(
    `تولید شد ${chunkEmbeddings.length} امبدینگ با ابعاد ${chunkEmbeddings[0].length}`,
  );

  // ایجاد امبدینگ برای یک پرس و جو
  const query = "Which AI models does AvalAI support?";
  const queryEmbedding = await createEmbedding(query);

  // یافتن مشابه‌ترین تکه به پرس و جو
  const similarities = chunkEmbeddings.map((embedding) =>
    cosineSimilarity(queryEmbedding, embedding),
  );

  const mostSimilarIdx = similarities.indexOf(Math.max(...similarities));
  console.log(`مشابه‌ترین تکه: ${chunks[mostSimilarIdx]}`);
  console.log(`امتیاز شباهت: ${similarities[mostSimilarIdx].toFixed(4)}`);
}

findSimilarChunks().catch(console.error);
bash
# استفاده از cURL برای تولید امبدینگ
curl https://api.avalai.ir/v1/embeddings \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AVALAI_API_KEY" \
  -d '{
		"model": "text-embedding-3-large",
		"input": "AvalAI provides access to a wide range of language models through a unified API."
	}'

# برای چندین امبدینگ در یک درخواست:
curl https://api.avalai.ir/v1/embeddings \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AVALAI_API_KEY" \
  -d '{
		"model": "text-embedding-3-large",
		"input": [
			"AvalAI provides access to a wide range of language models through a unified API.",
			"The platform supports models from various providers including OpenAI, Anthropic, Google, and more."
		]
	}'
go
package main

import (
	"context"
	"fmt"
	openai "github.com/openai/openai-go" // کتابخانه رسمی OpenAI Go
	"math"
	"os" // برای دسترسی به متغیرهای محیطی
)

// CreateEmbedding امبدینگ برای یک متن معین تولید می‌کند
func CreateEmbedding(client *openai.Client, text string) ([]float32, error) {
	resp, err := client.CreateEmbedding(
		context.Background(),
		openai.EmbeddingRequest{
			Model: "text-embedding-3-large",
			Input: []string{text}, // API انتظار یک آرایه از رشته‌ها را دارد
		},
	)

	if err != nil {
		return nil, err
	}

	return resp.Data[0].Embedding, nil
}

// CosineSimilarity شباهت کسینوسی بین دو بردار را محاسبه می‌کند
func CosineSimilarity(a, b []float32) float64 {
	var dotProduct float64
	var normA float64
	var normB float64

	for i := range a {
		dotProduct += float64(a[i] * b[i])
		normA += float64(a[i] * a[i])
		normB += float64(b[i] * b[i])
	}

	if normA == 0 || normB == 0 {
		return 0.0 // برای جلوگیری از تقسیم بر صفر
	}
	return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}

func main() {
	apiKey := os.Getenv("AVALAI_API_KEY")
	if apiKey == "" {
		fmt.Println("متغیر محیطی AVALAI_API_KEY تنظیم نشده است.")
		return
	}
	client := openai.NewClient(apiKey)
	client.BaseURL = "https://api.avalai.ir/v1" // نقطه پایانی AvalAI API

	// تکه‌های نمونه
	chunks := []string{
		"AvalAI provides access to a wide range of language models through a unified API.",
		"The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
		"Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
	}

	// ایجاد امبدینگ برای هر تکه
	var chunkEmbeddings [][]float32
	for _, chunk := range chunks {
		embedding, err := CreateEmbedding(client, chunk)
		if err != nil {
			fmt.Printf("خطا در ایجاد امبدینگ: %v\n", err)
			return
		}
		chunkEmbeddings = append(chunkEmbeddings, embedding)
	}

	if len(chunkEmbeddings) > 0 && len(chunkEmbeddings[0]) > 0 {
		fmt.Printf("تولید شد %d امبدینگ با ابعاد %d\n", len(chunkEmbeddings), len(chunkEmbeddings[0]))
	} else {
		fmt.Println("هیچ امبدینگی تولید نشد یا امبدینگ‌ها خالی هستند.")
		return
	}

	// ایجاد امبدینگ برای یک پرس و جو
	query := "Which AI models does AvalAI support?"
	queryEmbedding, err := CreateEmbedding(client, query)
	if err != nil {
		fmt.Printf("خطا در ایجاد امبدینگ پرس و جو: %v\n", err)
		return
	}

	// یافتن مشابه‌ترین تکه به پرس و جو
	var similarities []float64
	var maxSimilarity float64 = -1.0 // مقدار اولیه باید کمتر از هر شباهت ممکن باشد
	var mostSimilarIdx int = -1

	for i, embedding := range chunkEmbeddings {
		similarity := CosineSimilarity(queryEmbedding, embedding)
		similarities = append(similarities, similarity)

		if mostSimilarIdx == -1 || similarity > maxSimilarity {
			maxSimilarity = similarity
			mostSimilarIdx = i
		}
	}

	if mostSimilarIdx != -1 {
		fmt.Printf("مشابه‌ترین تکه: %s\n", chunks[mostSimilarIdx])
		fmt.Printf("امتیاز شباهت: %.4f\n", maxSimilarity)
	} else {
		fmt.Println("امکان یافتن مشابه‌ترین تکه وجود نداشت.")
	}
}
php
<?php
// تولید امبدینگ با استفاده از AvalAI API

$apiKey = getenv('AVALAI_API_KEY');
$apiUrl = 'https://api.avalai.ir/v1/embeddings';

function createEmbedding($text, $apiKey, $apiUrl) {
	// هم متن تکی و هم آرایه‌ای از متون را مدیریت می‌کند
	$input = is_array($text) ? $text : $text; // در اینجا باید $text باشد نه [$text] برای حالت تکی

	$data = [
	'model' => 'text-embedding-3-large',
	'input' => $input
	];

	$jsonData = json_encode($data);

	$ch = curl_init($apiUrl);
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	curl_setopt($ch, CURLOPT_POST, true);
	curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
	curl_setopt($ch, CURLOPT_HTTPHEADER, [
	'Content-Type: application/json',
	'Authorization: Bearer ' . $apiKey,
	'Content-Length: ' . strlen($jsonData)
	]);

	$response = curl_exec($ch);
	$err = curl_error($ch);
	curl_close($ch);

	if ($err) {
	throw new Exception("خطای cURL: " . $err);
	}

	$responseData = json_decode($response, true);

	if (isset($responseData['error'])) {
		throw new Exception("API Error: " . $responseData['error']['message']);
	}

	// بازگرداندن امبدینگ(ها)
	// اگر ورودی یک آرایه بود، یک آرایه از امبدینگ‌ها را برمی‌گرداند
	// در غیر این صورت، یک امبدینگ تکی را برمی‌گرداند
	if (is_array($input)) {
	    return array_map(function($item) { return $item['embedding']; }, $responseData['data']);
	} else {
	    return $responseData['data'][0]['embedding'];
	}
}

// محاسبه شباهت کسینوسی بین دو بردار
function cosineSimilarity($a, $b) {
	$dotProduct = 0;
	$normA = 0;
	$normB = 0;

	for ($i = 0; $i < count($a); $i++) {
		$dotProduct += $a[$i] * $b[$i];
		$normA += $a[$i] * $a[$i];
		$normB += $b[$i] * $b[$i];
	}

	if ($normA == 0 || $normB == 0) {
	    return 0.0; // برای جلوگیری از تقسیم بر صفر
	}

	return $dotProduct / (sqrt($normA) * sqrt($normB));
}

// مثال استفاده
try {
	// تکه‌های نمونه
	$chunks = [
	"AvalAI provides access to a wide range of language models through a unified API.",
	"The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
	"Each model has different capabilities and pricing, so it's important to understand the tradeoffs."
	];

	// ایجاد امبدینگ برای هر تکه (درخواست دسته‌ای)
	$chunkEmbeddings = createEmbedding($chunks, $apiKey, $apiUrl);
	echo "تولید شد " . count($chunkEmbeddings) . " امبدینگ با ابعاد " . (count($chunkEmbeddings) > 0 ? count($chunkEmbeddings[0]) : 0) . "\n";

4. ذخیره‌سازی برداری

ذخیره‌سازی و بازیابی کارآمد امبدینگ‌های برداری برای عملکرد RAG بسیار مهم است. پایگاه‌های داده برداری برای این منظور تخصصی شده‌اند.

بهترین شیوه‌ها

  • انتخاب پایگاه داده برداری مناسب: پایگاه داده‌ای را انتخاب کنید که تعادل بین سرعت، مقیاس‌پذیری و غنای ویژگی‌ها را برقرار کند.
  • بهینه‌سازی شاخص: از روش‌های شاخص‌گذاری مناسب (مانند HNSW، IVF) برای نیازهای بازیابی خود استفاده کنید.
  • فیلتر کردن با متادیتا: متادیتا را برای بازیابی دقیق‌تر ذخیره و استفاده کنید.
  • پردازش دسته‌ای: از عملیات دسته‌ای برای شاخص‌گذاری و پرس و جوی کارآمد استفاده کنید.
python
from openai import OpenAI
import numpy as np
import faiss  # کتابخانه FAISS برای جستجوی شباهت کارآمد
import pickle  # برای ذخیره و بارگذاری اشیا پایتون

client = OpenAI(
    api_key="your-avalai-api-key",
    base_url="https://api.avalai.ir/v1",  # نقطه پایانی AvalAI API
)


# پایگاه داده برداری ساده با استفاده از FAISS
class SimpleVectorDB:
    def __init__(self, dimension):
        """یک پایگاه داده برداری ساده را راه‌اندازی می‌کند."""
        # استفاده از فاصله L2 (اقلیدسی)
        self.index = faiss.IndexFlatL2(dimension)
        self.texts = []  # برای ذخیره متون اصلی
        self.metadata = []  # برای ذخیره متادیتا

    def add_texts(self, texts, embeddings, metadata=None):
        """متون و امبدینگ‌های آن‌ها را به پایگاه داده اضافه می‌کند."""
        if metadata is None:
            metadata = [{} for _ in texts]

        # تبدیل امبدینگ‌ها به آرایه numpy
        embeddings_array = np.array(embeddings).astype("float32")

        # اضافه کردن به شاخص FAISS
        self.index.add(embeddings_array)

        # ذخیره متون و متادیتا
        self.texts.extend(texts)
        self.metadata.extend(metadata)

        # بازگرداندن شناسه‌های متون اضافه شده
        return list(range(len(self.texts) - len(texts), len(self.texts)))

    def similarity_search(self, query_embedding, k=5):
        """جستجو برای بردارهای مشابه."""
        # تبدیل به آرایه numpy
        query_embedding_array = np.array([query_embedding]).astype("float32")

        # جستجو
        distances, indices = self.index.search(query_embedding_array, k)

        # بازگرداندن نتایج
        results = []
        for i, idx in enumerate(indices[0]):
            if idx < len(self.texts) and idx >= 0:  # شاخص معتبر
                results.append(
                    {
                        "text": self.texts[idx],
                        "metadata": self.metadata[idx],
                        "distance": distances[0][i],
                    }
                )
        return results

    def save(self, filepath):
        """پایگاه داده برداری را روی دیسک ذخیره می‌کند."""
        # ذخیره شاخص FAISS
        with open(filepath + ".faiss", "wb") as f:
            faiss.write_index(
                self.index, f.fileno()
            )  # برای سازگاری با نسخه‌های جدیدتر FAISS ممکن است به f.fileno() نیاز باشد

        # ذخیره متون و متادیتا
        with open(filepath + ".pkl", "wb") as f:
            pickle.dump({"texts": self.texts, "metadata": self.metadata}, f)
        print(f"پایگاه داده برداری در {filepath}.faiss و {filepath}.pkl ذخیره شد.")

    @classmethod
    def load(cls, filepath, dimension):
        """پایگاه داده برداری را از دیسک بارگذاری می‌کند."""
        db = cls(dimension)

        # بارگذاری شاخص FAISS
        with open(filepath + ".faiss", "rb") as f:
            db.index = faiss.read_index(
                f.fileno()
            )  # برای سازگاری با نسخه‌های جدیدتر FAISS

        # بارگذاری متون و متادیتا
        with open(filepath + ".pkl", "rb") as f:
            data = pickle.load(f)
            db.texts = data["texts"]
            db.metadata = data["metadata"]

        print(f"پایگاه داده برداری از {filepath} بارگذاری شد.")
        return db


# مثال استفاده
def create_embedding(text):
    """امبدینگ برای یک متن معین تولید می‌کند."""
    response = client.embeddings.create(model="text-embedding-3-large", input=text)
    return response.data[0].embedding


# ایجاد یک پایگاه داده برداری
dimension = 1536  # ابعاد امبدینگ مدل text-embedding-3-large
vector_db = SimpleVectorDB(dimension)

# اسناد نمونه با متادیتا
documents = [
    {
        "text": "AvalAI provides access to a wide range of language models through a unified API.",
        "metadata": {"source": "docs", "page": 1},
    },
    {
        "text": "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
        "metadata": {"source": "docs", "page": 1},
    },
    {
        "text": "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
        "metadata": {"source": "docs", "page": 2},
    },
]

# ایجاد امبدینگ و اضافه کردن به پایگاه داده
texts = [doc["text"] for doc in documents]
embeddings = [create_embedding(text) for text in texts]
metadata = [doc["metadata"] for doc in documents]

vector_db.add_texts(texts, embeddings, metadata)
print(f"{len(texts)} سند به پایگاه داده اضافه شد.")

# ذخیره پایگاه داده
vector_db.save("my_vector_db")

# بارگذاری پایگاه داده (برای آزمایش)
# loaded_vector_db = SimpleVectorDB.load("my_vector_db", dimension)
javascript
import { OpenAI } from "openai";
import Faiss from "faiss-node"; // کتابخانه faiss-node برای کار با FAISS در Node.js
import fs from "fs";
import path from "path"; // برای کار با مسیرهای فایل

const client = new OpenAI({
	apiKey: process.env.AVALAI_API_KEY,
	baseURL: "https://api.avalai.ir/v1", // نقطه پایانی AvalAI API
});

// پایگاه داده برداری ساده با استفاده از FAISS
class SimpleVectorDB {
	constructor(dimension) {
		// استفاده از فاصله L2 (اقلیدسی)
		this.index = new Faiss.IndexFlatL2(dimension);
		this.texts = []; // برای ذخیره متون اصلی
		this.metadata = []; // برای ذخیره متادیتا
		this.dimension = dimension; // ذخیره ابعاد برای بارگذاری مجدد
	}

	addTexts(texts, embeddings, metadata = null) {
		if (!metadata) {
			metadata = texts.map(() => ({}));
		}

		// تبدیل امبدینگ‌ها به Float32Array
		// faiss-node انتظار دارد که امبدینگ‌ها به صورت یک آرایه مسطح باشند
		const flatEmbeddings = embeddings.flat();
		const embeddingsArray = new Float32Array(flatEmbeddings);

		// تعداد بردارها برای اضافه کردن
		const numVectors = embeddings.length;

		if (numVectors > 0) {
			// اضافه کردن به شاخص FAISS
			// متد add در faiss-node نیاز به تعداد بردارها دارد اگر آرایه مسطح باشد
			this.index.add(embeddingsArray);
		}


		// ذخیره متون و متادیتا
		this.texts.push(...texts);
		this.metadata.push(...metadata);

		// بازگرداندن شناسه‌های متون اضافه شده
		return Array.from(
			{ length: texts.length },
			(_, i) => this.texts.length - texts.length + i,
		);
	}

	similaritySearch(queryEmbedding, k = 5) {
		// تبدیل به Float32Array
		const queryEmbeddingArray = new Float32Array(queryEmbedding);

		// اطمینان از اینکه k از تعداد کل آیتم‌ها در شاخص بیشتر نباشد
		const actualK = Math.min(k, this.index.ntotal());
		if (actualK === 0) {
			return []; // اگر شاخص خالی باشد، نتیجه‌ای وجود ندارد
		}


		// جستجو
		// متد search در faiss-node یک شی با distances و labels برمی‌گرداند
		const result = this.index.search(queryEmbeddingArray, actualK);
		const { distances, labels } = result;

		// بازگرداندن نتایج
		const results = [];
		for (let i = 0; i < labels.length; i++) {
			const idx = labels[i];
			if (idx < this.texts.length && idx >= 0) {
				// شاخص معتبر
				results.push({
					text: this.texts[idx],
					metadata: this.metadata[idx],
					distance: distances[i], // distances یک آرایه از فاصله‌ها است
				});
			}
		}
		return results;
	}

	save(filepath) {
		const dir = path.dirname(filepath);
		if (!fs.existsSync(dir)) {
			fs.mkdirSync(dir, { recursive: true });
		}
		// ذخیره شاخص FAISS
		this.index.write(`${filepath}.faiss`);

		// ذخیره متون و متادیتا و ابعاد
		fs.writeFileSync(
			`${filepath}.json`,
			JSON.stringify({
				texts: this.texts,
				metadata: this.metadata,
				dimension: this.dimension
			}),
		);
		console.log(`پایگاه داده برداری در ${filepath}.faiss و ${filepath}.json ذخیره شد.`);
	}

	static load(filepath) {
		// بارگذاری متون، متادیتا و ابعاد
		const jsonData = JSON.parse(fs.readFileSync(`${filepath}.json`, 'utf8'));
		const db = new SimpleVectorDB(jsonData.dimension); // ایجاد نمونه با ابعاد ذخیره شده

		// بارگذاری شاخص FAISS
		// IndexFlatL2 نیاز به ابعاد در سازنده دارد، اما read آن را بازنویسی می‌کند
		db.index = Faiss.read(`${filepath}.faiss`);

		db.texts = jsonData.texts;
		db.metadata = jsonData.metadata;

		console.log(`پایگاه داده برداری از ${filepath} بارگذاری شد. تعداد کل آیتم‌ها در شاخص: ${db.index.ntotal()}`);
		return db;
	}
}

// مثال استفاده
async function createEmbedding(text) {
	const response = await client.embeddings.create({
		model: "text-embedding-3-large",
		input: text,
	});
	return response.data[0].embedding;
}

async function main() {
	// ایجاد یک پایگاه داده برداری
	const dimension = 1536; // ابعاد امبدینگ مدل text-embedding-3-large
	const vectorDb = new SimpleVectorDB(dimension);

	// اسناد نمونه با متادیتا
	const documents = [
		{
			text: "AvalAI provides access to a wide range of language models through a unified API.",
			metadata: { source: "docs", page: 1 },
		},
		{
			text: "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
			metadata: { source: "docs", page: 1 },
		},
		{
			text: "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
			metadata: { source: "docs", page: 2 },
		},
	];

	// ایجاد امبدینگ و اضافه کردن به پایگاه داده
	const texts = documents.map((doc) => doc.text);
	const embeddings = await Promise.all(
		texts.map((text) => createEmbedding(text)),
	);
	const metadata = documents.map((doc) => doc.metadata);

	vectorDb.addTexts(texts, embeddings, metadata);
	console.log(`${texts.length} سند به پایگاه داده اضافه شد.`);

	// ذخیره پایگاه داده
	const dbPath = "db_artefacts/my_vector_db_js"; // استفاده از یک زیرپوشه
	vectorDb.save(dbPath);

	// بارگذاری پایگاه داده (برای آزمایش)
	// const loadedVectorDb = SimpleVectorDB.load(dbPath);

	// پرس و جو از پایگاه داده
	const query = "Which AI models does AvalAI support?";
	const queryEmbedding = await createEmbedding(query);

	const results = vectorDb.similaritySearch(queryEmbedding, 2);
	// const results = loadedVectorDb.similaritySearch(queryEmbedding, 2); // برای آزمایش بارگذاری شده
bash
# مثال استفاده از پایگاه داده برداری Milvus با Docker
# ابتدا، یک نمونه Milvus را با استفاده از Docker راه‌اندازی کنید
# ممکن است نیاز باشد نسخه Milvus را با آخرین نسخه پایدار جایگزین کنید
docker run -d --name milvus-standalone \
  -p 19530:19530 \
  -p 19121:19121 \
  milvusdb/milvus:v2.3.3 standalone # یا هر نسخه دیگری که استفاده می‌کنید

# چند ثانیه صبر کنید تا Milvus راه‌اندازی شود
sleep 10

# نصب وابستگی‌های پایتون
# pip install pymilvus openai numpy # اطمینان حاصل کنید که openai نیز نصب شده اگر قبلا نصب نشده
# 'avalai' در اینجا به کتابخانه openai اشاره دارد که با base_url پیکربندی شده است

# یک اسکریپت پایتون برای تعامل با Milvus ایجاد کنید
cat >milvus_example.py <<'EOF'
from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType, utility
from openai import OpenAI # استفاده از کتابخانه رسمی OpenAI
import numpy as np
import time
import os

# اتصال به AvalAI
client = OpenAI(
	api_key=os.getenv("AVALAI_API_KEY", "your-avalai-api-key"), # از متغیر محیطی یا مقدار پیش‌فرض استفاده کنید
	base_url="https://api.avalai.ir/v1", # نقطه پایانی AvalAI API
)

# اتصال به Milvus
MILVUS_HOST = os.getenv("MILVUS_HOST", "localhost")
MILVUS_PORT = os.getenv("MILVUS_PORT", "19530")
print(f"درحال اتصال به Milvus در {MILVUS_HOST}:{MILVUS_PORT}...")
try:
	connections.connect(host=MILVUS_HOST, port=MILVUS_PORT)
	print("اتصال به Milvus موفقیت‌آمیز بود.")
except Exception as e:
	print(f"خطا در اتصال به Milvus: {e}")
	exit()

# تعریف اسکیمای کالکشن
COLLECTION_NAME = "avalai_docs_rag"
ID_FIELD = "id"
TEXT_FIELD = "text"
SOURCE_FIELD = "source"
PAGE_FIELD = "page"
EMBEDDING_FIELD = "embedding"
DIMENSION = 1536 # ابعاد برای text-embedding-3-large

fields = [
	FieldSchema(name=ID_FIELD, dtype=DataType.INT64, is_primary=True, auto_id=True),
	FieldSchema(name=TEXT_FIELD, dtype=DataType.VARCHAR, max_length=65535), # افزایش طول برای متون طولانی‌تر
	FieldSchema(name=SOURCE_FIELD, dtype=DataType.VARCHAR, max_length=255),
	FieldSchema(name=PAGE_FIELD, dtype=DataType.INT64),
	FieldSchema(name=EMBEDDING_FIELD, dtype=DataType.FLOAT_VECTOR, dim=DIMENSION)
]
schema = CollectionSchema(fields, description="کالکشن اسناد RAG برای AvalAI")

# ایجاد یا دریافت کالکشن
print(f"درحال بررسی کالکشن '{COLLECTION_NAME}'...")
if utility.has_collection(COLLECTION_NAME):
	print(f"کالکشن '{COLLECTION_NAME}' از قبل وجود دارد. در حال بارگذاری...")
	collection = Collection(name=COLLECTION_NAME)
else:
	print(f"کالکشن '{COLLECTION_NAME}' وجود ندارد. در حال ایجاد...")
	collection = Collection(name=COLLECTION_NAME, schema=schema)
	# ایجاد شاخص برای فیلد برداری
	print(f"درحال ایجاد شاخص برای فیلد '{EMBEDDING_FIELD}'...")
	index_params = {
		"metric_type": "L2", # نوع متریک فاصله
		"index_type": "HNSW", # الگوریتم شاخص‌گذاری
		"params": {"M": 8, "efConstruction": 64} # پارامترهای شاخص
	}
	collection.create_index(field_name=EMBEDDING_FIELD, index_params=index_params)
	print("شاخص با موفقیت ایجاد شد.")

# تابع برای دریافت امبدینگ‌ها
def create_embedding(text_input):
	# اطمینان حاصل کنید که ورودی یک رشته یا لیستی از رشته‌ها است
	if isinstance(text_input, str):
		text_input = [text_input] # API انتظار یک لیست دارد

	response = client.embeddings.create(
		model="text-embedding-3-large",
		input=text_input
	)
	# اگر ورودی یک رشته بود، فقط اولین امبدینگ را برگردانید
	if len(text_input) == 1:
		return response.data[0].embedding
	# در غیر این صورت، لیست امبدینگ‌ها را برگردانید
	return [data.embedding for data in response.data]

# اسناد نمونه
documents = [
	{
		"text": "AvalAI provides access to a wide range of language models through a unified API.",
		"source": "docs",
		"page": 1
	},
	{
		"text": "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
		"source": "docs",
		"page": 1
	},
	{
		"text": "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
		"source": "docs",
		"page": 2
	}
]

# بررسی اینکه آیا اسناد قبلا اضافه شده‌اند (برای جلوگیری از تکرار)
# این یک بررسی ساده است؛ برای تولید، یک سیستم مدیریت شناسه قوی‌تر لازم است
collection.load() # بارگذاری کالکشن برای جستجو
existing_texts_count = collection.query(expr="text != ''", output_fields=[TEXT_FIELD], limit=len(documents))
if len(existing_texts_count) < len(documents): # اگر همه اسناد وجود ندارند
	print("درحال وارد کردن داده‌ها...")
	data_to_insert = []
	texts_to_embed = [doc["text"] for doc in documents]

	# دریافت امبدینگ‌ها به صورت دسته‌ای در صورت امکان (بستگی به پیاده‌سازی create_embedding دارد)
	# در اینجا، ما برای سادگی به صورت تکی انجام می‌دهیم
	for doc in documents:
		embedding = create_embedding(doc["text"])
		data_to_insert.append([None, doc["text"], doc["source"], doc["page"], embedding]) # None برای id با auto_id=True

	if data_to_insert:
		insert_result = collection.insert(data_to_insert)
		collection.flush() # اطمینان از نوشته شدن داده‌ها روی دیسک
		print(f"{len(insert_result.primary_keys)} سند وارد شد.")
	else:
		print("هیچ داده جدیدی برای وارد کردن وجود ندارد.")
else:
	print("به نظر می‌رسد اسناد قبلا وارد شده‌اند. از وارد کردن مجدد صرف‌نظر می‌شود.")


# بارگذاری کالکشن برای جستجو
collection.load()
print(f"تعداد کل موجودیت‌ها در کالکشن: {collection.num_entities}")

# جستجو
query = "Which AI models does AvalAI support?"
query_embedding = create_embedding(query)

search_params = {
	"metric_type": "L2",
	"params": {"ef": 32} # پارامتر جستجو برای HNSW
}

print(f"\nدرحال جستجو برای پرس و جوی: '{query}'")
results = collection.search(
	data=[query_embedding], # داده‌های پرس و جو
	anns_field=EMBEDDING_FIELD, # نام فیلد برداری
	param=search_params,
	limit=2, # تعداد نتایج برای بازگرداندن
	output_fields=[TEXT_FIELD, SOURCE_FIELD, PAGE_FIELD] # فیلدهایی که باید در نتایج بازگردانده شوند
)

for hits in results:
	print(f"تعداد نتایج یافت شده: {len(hits)}")
	for hit in hits:
		print(f"متن: {hit.entity.get(TEXT_FIELD)}")
		print(f"منبع: {hit.entity.get(SOURCE_FIELD)}, صفحه: {hit.entity.get(PAGE_FIELD)}")
		print(f"فاصله: {hit.distance:.4f}\n")

# قطع اتصال
connections.disconnect("default")
print("اتصال به Milvus قطع شد.")
EOF

# اجرای مثال
# اطمینان حاصل کنید که متغیر محیطی AVALAI_API_KEY تنظیم شده است
# export AVALAI_API_KEY="your-actual-api-key"
python3 milvus_example.py
go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"time" // برای تاخیر

	"github.com/milvus-io/milvus-sdk-go/v2/client" // کتابخانه رسمی Milvus Go SDK
	"github.com/milvus-io/milvus-sdk-go/v2/entity"
	openai "github.com/openai/openai-go" // کتابخانه رسمی OpenAI Go
)

const (
	// پارامترهای کالکشن
	collectionName = "avalai_docs_rag_go"
	dimension      = 1536 // ابعاد برای text-embedding-3-large
	idField        = "id"
	textField      = "text"
	sourceField    = "source"
	pageField      = "page"
	embeddingField = "embedding"
	milvusHost     = "localhost"
	milvusPort     = "19530"
)

// تابع برای ایجاد امبدینگ‌ها
func createEmbedding(ctx context.Context, avalaiClient *openai.Client, textInput string) ([]float32, error) {
	resp, err := avalaiClient.CreateEmbedding(
		ctx,
		openai.EmbeddingRequest{
			Model: "text-embedding-3-large",
			Input: []string{textInput}, // API انتظار یک آرایه از رشته‌ها را دارد
		},
	)
	if err != nil {
		return nil, fmt.Errorf("خطا در ایجاد امبدینگ: %w", err)
	}
	if len(resp.Data) == 0 {
		return nil, fmt.Errorf("هیچ امبدینگی از API بازگردانده نشد برای متن: %s", textInput)
	}
	return resp.Data[0].Embedding, nil
}

func main() {
	ctx := context.Background()

	// اتصال به AvalAI
	avalaiAPIKey := os.Getenv("AVALAI_API_KEY")
	if avalaiAPIKey == "" {
		log.Fatal("متغیر محیطی AVALAI_API_KEY تنظیم نشده است.")
	}
	avalaiClient := openai.NewClient(avalaiAPIKey)
	avalaiClient.BaseURL = "https://api.avalai.ir/v1" // نقطه پایانی AvalAI API

	// اتصال به Milvus
	log.Printf("درحال اتصال به Milvus در %s:%s...\n", milvusHost, milvusPort)
	milvusClient, err := client.NewGrpcClient(ctx, fmt.Sprintf("%s:%s", milvusHost, milvusPort))
	if err != nil {
		log.Fatalf("خطا در اتصال به Milvus: %v", err)
	}
	defer milvusClient.Close()
	log.Println("اتصال به Milvus موفقیت‌آمیز بود.")

	// بررسی وجود کالکشن
	log.Printf("درحال بررسی کالکشن '%s'...\n", collectionName)
	hasCollection, err := milvusClient.HasCollection(ctx, collectionName)
	if err != nil {
		log.Fatalf("خطا در بررسی کالکشن: %v", err)
	}

	if !hasCollection {
		log.Printf("کالکشن '%s' وجود ندارد. در حال ایجاد...\n", collectionName)
		// تعریف اسکیمای کالکشن
		schema := &entity.Schema{
			CollectionName: collectionName,
			Description:    "کالکشن اسناد RAG برای AvalAI (Go)",
			Fields: []*entity.Field{
				{Name: idField, DataType: entity.FieldTypeInt64, IsPrimaryKey: true, AutoID: true},
				{Name: textField, DataType: entity.FieldTypeVarChar, MaxLength: 65535},
				{Name: sourceField, DataType: entity.FieldTypeVarChar, MaxLength: 255},
				{Name: pageField, DataType: entity.FieldTypeInt64},
				{Name: embeddingField, DataType: entity.FieldTypeFloatVector, TypeParams: map[string]string{"dim": fmt.Sprintf("%d", dimension)}},
			},
		}
		err = milvusClient.CreateCollection(ctx, schema, entity.DefaultShardNumber)
		if err != nil {
			log.Fatalf("خطا در ایجاد کالکشن: %v", err)
		}
		log.Printf("کالکشن '%s' با موفقیت ایجاد شد.\n", collectionName)

		// ایجاد شاخص
		log.Printf("درحال ایجاد شاخص برای فیلد '%s'...\n", embeddingField)
		idx, err := entity.NewIndexHNSW(entity.L2, 8, 64) // پارامترهای HNSW: M, efConstruction
		if err != nil {
			log.Fatalf("خطا در ایجاد پارامترهای شاخص: %v", err)
		}
		err = milvusClient.CreateIndex(ctx, collectionName, embeddingField, idx, false, client.WithIndexName("hnsw_idx"))
		if err != nil {
			log.Fatalf("خطا در ایجاد شاخص: %v", err)
		}
		log.Println("شاخص با موفقیت ایجاد شد.")
	} else {
		log.Printf("کالکشن '%s' از قبل وجود دارد.\n", collectionName)
	}

	// اسناد نمونه
	documents := []struct {
		Text   string
		Source string
		Page   int64
	}{
		{
			Text:   "AvalAI provides access to a wide range of language models through a unified API.",
			Source: "docs-go",
			Page:   1,
		},
		{
			Text:   "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
			Source: "docs-go",
			Page:   1,
		},
		{
			Text:   "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
			Source: "docs-go",
			Page:   2,
		},
	}

	// بارگذاری کالکشن قبل از وارد کردن یا جستجو
	err = milvusClient.LoadCollection(ctx, collectionName, false)
	if err != nil {
		log.Fatalf("خطا در بارگذاری کالکشن: %v", err)
	}
	log.Printf("کالکشن '%s' بارگذاری شد.\n", collectionName)

	// بررسی اینکه آیا اسناد قبلا اضافه شده‌اند (برای جلوگیری از تکرار)
	// این یک بررسی ساده است؛ برای تولید، یک سیستم مدیریت شناسه قوی‌تر لازم است
	var pks []entity.Column
	pks, err = milvusClient.Query(ctx, collectionName, []string{}, fmt.Sprintf("%s != \"\"", textField), []string{idField}, client.WithLimit(int64(len(documents))))
	if err != nil {
		log.Printf("خطا در کوئری برای بررسی اسناد موجود: %v\n", err)
		// ادامه می‌دهیم و سعی می‌کنیم اسناد را اضافه کنیم
	}

	if len(pks) == 0 || (len(pks) > 0 && pks[0].Len() < len(documents)) {
		log.Println("درحال وارد کردن داده‌ها...")
		// آماده‌سازی داده‌ها برای وارد کردن
		textsCol := make([]string, 0, len(documents))
		sourcesCol := make([]string, 0, len(documents))
		pagesCol := make([]int64, 0, len(documents))
		embeddingsCol := make([][]float32, 0, len(documents))

		for _, doc := range documents {
			embedding, err := createEmbedding(ctx, avalaiClient, doc.Text)
			if err != nil {
				log.Printf("خطا در ایجاد امبدینگ برای '%s': %v. از این سند صرف‌نظر می‌شود.\n", doc.Text, err)
				continue
			}
			textsCol = append(textsCol, doc.Text)
			sourcesCol = append(sourcesCol, doc.Source)
			pagesCol = append(pagesCol, doc.Page)
			embeddingsCol = append(embeddingsCol, embedding)
		}

		if len(textsCol) > 0 {
			textColumn := entity.NewColumnVarChar(textField, textsCol)
			sourceColumn := entity.NewColumnVarChar(sourceField, sourcesCol)
			pageColumn := entity.NewColumnInt64(pageField, pagesCol)
			embeddingColumn := entity.NewColumnFloatVector(embeddingField, dimension, embeddingsCol)

			_, err = milvusClient.Insert(ctx, collectionName, "", textColumn, sourceColumn, pageColumn, embeddingColumn)
			if err != nil {
				log.Fatalf("خطا در وارد کردن داده‌ها: %v", err)
			}
			log.Printf("%d سند با موفقیت وارد شد.\n", len(textsCol))

			// Milvus ممکن است برای flush کردن داده‌ها به کمی زمان نیاز داشته باشد
			log.Println("کمی صبر برای flush شدن داده‌ها...")
			time.Sleep(2 * time.Second)

		} else {
			log.Println("هیچ داده جدیدی برای وارد کردن وجود ندارد (احتمالا به دلیل خطای امبدینگ).")
		}
	} else {
		log.Println("به نظر می‌رسد اسناد قبلا وارد شده‌اند. از وارد کردن مجدد صرف‌نظر می‌شود.")
	}

	// دریافت تعداد موجودیت‌ها
	stats, err := milvusClient.GetCollectionStatistics(ctx, collectionName)
	if err != nil {
		log.Fatalf("خطا در دریافت آمار کالکشن: %v", err)
	}
	rowCount := 0
	for _, stat := range stats {
		if stat.Key == "row_count" {
			rowCount = int(stat.Value) // مقدار به صورت رشته‌ای است
			break
		}
	}
	log.Printf("تعداد کل موجودیت‌ها در کالکشن '%s': %d\n", collectionName, rowCount)

	// جستجو
	query := "Which AI models does AvalAI support?"
	queryEmbedding, err := createEmbedding(ctx, avalaiClient, query)
	if err != nil {
		log.Fatalf("خطا در ایجاد امبدینگ پرس و جو: %v", err)
	}

	log.Printf("\nدرحال جستجو برای پرس و جوی: '%s'\n", query)
	// پارامترهای جستجو
	sp, _ := entity.NewIndexHNSWSearchParams(32) // ef برای HNSW

	searchResult, err := milvusClient.Search(
		ctx,
		collectionName,
		[]string{}, // نام پارتیشن‌ها، اگر خالی باشد در کل کالکشن جستجو می‌کند
		"",         // عبارت بولی فیلتر، خالی برای بدون فیلتر
		[]string{textField, sourceField, pageField},         // فیلدهای خروجی
		[]entity.Vector{entity.FloatVector(queryEmbedding)}, // بردارهای پرس و جو
		embeddingField, // نام فیلد برداری برای جستجو
		entity.L2,      // نوع متریک
		2,              // تعداد نتایج برتر (topK)
		sp,             // پارامترهای جستجو
	)
	if err != nil {
		log.Fatalf("خطا در جستجو: %v", err)
	}

	log.Printf("تعداد نتایج یافت شده: %d\n", len(searchResult[0].IDs))
	for _, resultSet := range searchResult {
		for i := 0; i < resultSet.ResultCount; i++ {
			textFieldValue, _ := resultSet.Fields.Get(textField, i)
			sourceFieldValue, _ := resultSet.Fields.Get(sourceField, i)
			pageFieldValue, _ := resultSet.Fields.Get(pageField, i)

			fmt.Printf("متن: %s\n", textFieldValue)
			fmt.Printf("منبع: %s, صفحه: %d\n", sourceFieldValue, pageFieldValue)
			fmt.Printf("امتیاز (فاصله): %.4f\n\n", resultSet.Scores[i])
		}
	}
}

برای پاکسازی پس از اتمام (اختیاری):

docker stop milvus-standalone

5. بازیابی

بازیابی فرآیند یافتن اسناد مرتبط برای یک پرس و جوی معین است. روش‌های مختلف بازیابی می‌توانند به طور قابل توجهی بر کیفیت نتایج تاثیر بگذارند.

روش‌های بازیابی

  1. بازیابی متراکم (Dense Retrieval): از شباهت برداری برای یافتن اسناد مشابه معنایی استفاده می‌کند.
  2. بازیابی پراکنده (Sparse Retrieval): از تطابق کلمات کلیدی (مانند BM25، TF-IDF) استفاده می‌کند.
  3. بازیابی ترکیبی (Hybrid Retrieval): رویکردهای متراکم و پراکنده را ترکیب می‌کند.

بهترین شیوه‌ها

  • روش‌های گروهی (Ensemble Methods): چندین استراتژی بازیابی را برای نتایج بهتر ترکیب کنید.
  • گسترش پرس و جو (Query Expansion): پرس و جوها را با عبارات مرتبط یا فرمول‌بندی‌های مجدد تقویت کنید.
  • فیلتر کردن: از متادیتا برای محدود کردن فضای جستجو استفاده کنید.
  • بازیابی متنی (Contextual Retrieval): بازیابی را بر اساس تاریخچه مکالمه تطبیق دهید.
python
from openai import OpenAI
import numpy as np
from rank_bm25 import BM25Okapi  # کتابخانه برای BM25
import nltk
from nltk.tokenize import word_tokenize

# اگر منابع NLTK قبلا در دسترس نیستند، آن‌ها را دانلود کنید
try:
    word_tokenize("test")
except LookupError:
    nltk.download("punkt")

client = OpenAI(
    api_key="your-avalai-api-key",
    base_url="https://api.avalai.ir/v1",  # نقطه پایانی AvalAI API
)


class HybridRetriever:
    """بازیاب ترکیبی که بازیابی متراکم و پراکنده را ترکیب می‌کند."""

    def __init__(self, texts, dense_weight=0.7):
        self.texts = texts
        self.dense_weight = dense_weight
        self.sparse_weight = 1.0 - dense_weight

        # آماده‌سازی بازیابی پراکنده (BM25)
        tokenized_texts = [word_tokenize(text.lower()) for text in texts]
        self.bm25 = BM25Okapi(tokenized_texts)

        # آماده‌سازی بازیابی متراکم
        # امبدینگ‌ها در اولین جستجوی متراکم یا به صورت جداگانه ایجاد می‌شوند
        self.embeddings = None  # یا می‌توانید اینجا آن‌ها را ایجاد کنید: [self._create_embedding(text) for text in texts]

    def _create_embedding(self, text):
        """امبدینگ برای یک متن معین تولید می‌کند."""
        response = client.embeddings.create(model="text-embedding-3-large", input=text)
        return response.data[0].embedding

    def ensure_embeddings_created(self):
        """اطمینان حاصل می‌کند که امبدینگ‌ها برای همه متون ایجاد شده‌اند."""
        if self.embeddings is None:
            print("درحال ایجاد امبدینگ برای بازیابی متراکم...")
            self.embeddings = [self._create_embedding(text) for text in self.texts]
            print(f"{len(self.embeddings)} امبدینگ ایجاد شد.")

    def _cosine_similarity(self, a, b):
        """شباهت کسینوسی بین دو بردار را محاسبه می‌کند."""
        return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

    def _dense_search(self, query, top_k=5):
        """بازیابی متراکم را با استفاده از شباهت برداری انجام می‌دهد."""
        self.ensure_embeddings_created()  # اطمینان از ایجاد امبدینگ‌ها
        query_embedding = self._create_embedding(query)

        # محاسبه شباهت‌ها
        similarities = [
            self._cosine_similarity(query_embedding, doc_embedding)
            for doc_embedding in self.embeddings
        ]

        # دریافت شاخص‌ها و امتیازات top-k
        # np.argsort امتیازات را به ترتیب صعودی برمی‌گرداند، بنابراین از [::-1] برای معکوس کردن استفاده می‌کنیم
        top_indices = np.argsort(similarities)[-top_k:][::-1]
        top_scores = [similarities[i] for i in top_indices]

        return list(zip(top_indices, top_scores))

    def _sparse_search(self, query, top_k=5):
        """بازیابی پراکنده را با استفاده از BM25 انجام می‌دهد."""
        tokenized_query = word_tokenize(query.lower())
        scores = self.bm25.get_scores(tokenized_query)

        # دریافت شاخص‌ها و امتیازات top-k
        top_indices = np.argsort(scores)[-top_k:][::-1]
        top_scores = [scores[i] for i in top_indices]

        return list(zip(top_indices, top_scores))

    def search(self, query, top_k=5):
        """جستجوی ترکیبی را با ترکیب بازیابی متراکم و پراکنده انجام می‌دهد."""
        dense_results = self._dense_search(
            query, top_k=top_k * 2
        )  # دریافت نتایج بیشتر برای ترکیب بهتر
        sparse_results = self._sparse_search(query, top_k=top_k * 2)

        # نرمال‌سازی امتیازات (یک روش ساده)
        # برای تولید، ممکن است به روش‌های نرمال‌سازی پیچیده‌تری نیاز باشد
        max_dense_score = (
            max([score for _, score in dense_results]) if dense_results else 1.0
        )
        max_sparse_score = (
            max([score for _, score in sparse_results]) if sparse_results else 1.0
        )

        # جلوگیری از تقسیم بر صفر اگر همه امتیازات صفر باشند
        if max_dense_score == 0:
            max_dense_score = 1.0
        if max_sparse_score == 0:
            max_sparse_score = 1.0

        normalized_dense = {
            idx: score / max_dense_score for idx, score in dense_results
        }
        normalized_sparse = {
            idx: score / max_sparse_score for idx, score in sparse_results
        }

        # ترکیب امتیازات
        combined_scores = {}
        all_indices = set(normalized_dense.keys()).union(set(normalized_sparse.keys()))

        for idx in all_indices:
            dense_s = normalized_dense.get(idx, 0.0)
            sparse_s = normalized_sparse.get(idx, 0.0)
            combined_scores[idx] = (dense_s * self.dense_weight) + (
                sparse_s * self.sparse_weight
            )

        # دریافت نتایج top-k
        # مرتب‌سازی بر اساس امتیازات ترکیبی به ترتیب نزولی
        sorted_indices = sorted(combined_scores, key=combined_scores.get, reverse=True)[
            :top_k
        ]

        results = []
        for idx in sorted_indices:
            results.append(
                {
                    "text": self.texts[idx],
                    "score": combined_scores[idx],
                    "dense_contribution": normalized_dense.get(idx, 0.0)
                    * self.dense_weight,
                    "sparse_contribution": normalized_sparse.get(idx, 0.0)
                    * self.sparse_weight,
                    "original_dense_score": next(
                        (s for i, s in dense_results if i == idx), 0.0
                    ),  # برای اشکال‌زدایی
                    "original_sparse_score": next(
                        (s for i, s in sparse_results if i == idx), 0.0
                    ),  # برای اشکال‌زدایی
                }
            )
        return results


# مثال استفاده
texts = [
    "AvalAI provides access to a wide range of language models through a unified API.",
    "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
    "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
    "AvalAI offers tools for monitoring usage and managing costs effectively.",
    "The documentation includes examples and best practices for different use cases.",
]

retriever = HybridRetriever(
    texts, dense_weight=0.6
)  # وزن بیشتری به بازیابی متراکم می‌دهیم
query = "Which AI models does AvalAI support?"

results = retriever.search(query, top_k=2)
print(f"\nنتایج جستجوی ترکیبی برای: '{query}'")
for i, result in enumerate(results):
    print(f"نتیجه {i+1}: {result['text']}")
    print(f"  امتیاز کل: {result['score']:.4f}")
    print(
        f"  سهم متراکم: {result['dense_contribution']:.4f} (امتیاز اصلی: {result['original_dense_score']:.4f})"
    )
    print(
        f"  سهم پراکنده: {result['sparse_contribution']:.4f} (امتیاز اصلی: {result['original_sparse_score']:.4f})\n"
    )
javascript
import { OpenAI } from "openai";
import natural from "natural"; // کتابخانه Natural برای توکنایز کردن و BM25
// BM25 به طور مستقیم از natural قابل دسترسی است، نیازی به import جداگانه BM25 نیست

const client = new OpenAI({
	apiKey: process.env.AVALAI_API_KEY,
	baseURL: "https://api.avalai.ir/v1", // نقطه پایانی AvalAI API
});

class HybridRetriever {
	/**
	 * بازیاب ترکیبی که بازیابی متراکم و پراکنده را ترکیب می‌کند
	 * @param {string[]} texts - آرایه‌ای از متون اسناد
	 * @param {number} denseWeight - وزن برای بازیابی متراکم (0-1)
	 */
	constructor(texts, denseWeight = 0.7) {
		this.texts = texts;
		this.denseWeight = denseWeight;
		this.sparseWeight = 1.0 - denseWeight;

		// آماده‌سازی بازیابی پراکنده (BM25)
		this.tokenizer = new natural.WordTokenizer();
		const tokenizedTexts = texts.map((text) =>
			this.tokenizer.tokenize(text.toLowerCase()),
		);
		this.bm25 = new natural.BM25(tokenizedTexts); // BM25 از natural

		// امبدینگ‌ها به صورت تنبل بارگذاری/ایجاد می‌شوند
		this.embeddings = null;
	}

	/**
	 * امبدینگ برای یک متن معین تولید می‌کند
	 * @param {string} text - متن ورودی
	 * @returns {Promise<number[]>} - بردار امبدینگ
	 */
	async _createEmbedding(text) {
		const response = await client.embeddings.create({
			model: "text-embedding-3-large",
			input: text,
		});
		return response.data[0].embedding;
	}

	/**
	 * اطمینان حاصل می‌کند که امبدینگ‌ها برای همه متون ایجاد شده‌اند.
	 */
	async _ensureEmbeddingsCreated() {
		if (!this.embeddings) {
			console.log("درحال ایجاد امبدینگ برای بازیابی متراکم...");
			this.embeddings = await Promise.all(
				this.texts.map((text) => this._createEmbedding(text)),
			);
			console.log(`${this.embeddings.length} امبدینگ ایجاد شد.`);
		}
	}


	/**
	 * شباهت کسینوسی بین دو بردار را محاسبه می‌کند
	 * @param {number[]} a - بردار اول
	 * @param {number[]} b - بردار دوم
	 * @returns {number} - امتیاز شباهت
	 */
	_cosineSimilarity(a, b) {
		let dotProduct = 0;
		let normA = 0;
		let normB = 0;

		for (let i = 0; i < a.length; i++) {
			dotProduct += a[i] * b[i];
			normA += a[i] * a[i];
			normB += b[i] * b[i];
		}

		if (normA === 0 || normB === 0) return 0; // جلوگیری از تقسیم بر صفر
		return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
	}

	/**
	 * بازیابی متراکم را با استفاده از شباهت برداری انجام می‌دهد
	 * @param {string} query - پرس و جوی جستجو
	 * @param {number} topK - تعداد نتایج برای بازگرداندن
	 * @returns {Promise<Array<[number, number]>>} - آرایه‌ای از جفت‌های [شاخص، امتیاز]
	 */
	async _denseSearch(query, topK = 5) {
		await this._ensureEmbeddingsCreated(); // اطمینان از ایجاد امبدینگ‌ها

		const queryEmbedding = await this._createEmbedding(query);

		// محاسبه شباهت‌ها
		const similarities = this.embeddings.map((docEmbedding) =>
			this._cosineSimilarity(queryEmbedding, docEmbedding),
		);

		// دریافت شاخص‌ها و امتیازات top-k
		const indexedScores = similarities.map((score, index) => [index, score]);
		indexedScores.sort((a, b) => b[1] - a[1]); // مرتب‌سازی بر اساس امتیاز به ترتیب نزولی

		return indexedScores.slice(0, topK);
	}

	/**
	 * بازیابی پراکنده را با استفاده از BM25 انجام می‌دهد
	 * @param {string} query - پرس و جوی جستجو
	 * @param {number} topK - تعداد نتایج برای بازگرداندن
	 * @returns {Array<[number, number]>} - آرایه‌ای از جفت‌های [شاخص، امتیاز]
	 */
	_sparseSearch(query, topK = 5) {
		const tokenizedQuery = this.tokenizer.tokenize(query.toLowerCase());
		// متد search در BM25 از natural، امتیازات را برای همه اسناد برمی‌گرداند
		const allScores = this.bm25.tfidfs(tokenizedQuery, (i, measure) => measure);


		// دریافت شاخص‌ها و امتیازات top-k
		const indexedScores = allScores.map((score, index) => [index, score]);
		indexedScores.sort((a, b) => b[1] - a[1]); // مرتب‌سازی بر اساس امتیاز به ترتیب نزولی

		// BM25 ممکن است امتیازات منفی برگرداند، آن‌ها را به 0 محدود می‌کنیم
		return indexedScores.slice(0, topK).map(([index, score]) => [index, Math.max(0, score)]);
	}

	/**
	 * جستجوی ترکیبی را با ترکیب بازیابی متراکم و پراکنده انجام می‌دهد
	 * @param {string} query - پرس و جوی جستجو
	 * @param {number} topK - تعداد نتایج برای بازگرداندن
	 * @returns {Promise<Array<Object>>} - آرایه‌ای از اشیا نتیجه
	 */
	async search(query, topK = 5) {
		const denseResults = await this._denseSearch(query, topK * 2); // دریافت نتایج بیشتر
		const sparseResults = this._sparseSearch(query, topK * 2);

		// نرمال‌سازی امتیازات
		const maxDenseScore = denseResults.length > 0 ? Math.max(...denseResults.map(([_, score]) => score)) : 1.0;
		const maxSparseScore = sparseResults.length > 0 ? Math.max(...sparseResults.map(([_, score]) => score)) : 1.0;

		const normalizedDense = Object.fromEntries(
			denseResults.map(([idx, score]) => [idx, maxDenseScore > 0 ? score / maxDenseScore : 0]),
		);
		const normalizedSparse = Object.fromEntries(
			sparseResults.map(([idx, score]) => [idx, maxSparseScore > 0 ? score / maxSparseScore : 0]),
		);

		// ترکیب امتیازات
		const combinedScores = {};
		const allIndices = new Set([...Object.keys(normalizedDense).map(Number), ...Object.keys(normalizedSparse).map(Number)]);

		for (const idx of allIndices) {
			const denseS = normalizedDense[idx] || 0.0;
			const sparseS = normalizedSparse[idx] || 0.0;
			combinedScores[idx] = (denseS * this.denseWeight) + (sparseS * this.sparseWeight);
		}

		// دریافت نتایج top-k
		const sortedIndices = Object.keys(combinedScores)
			.map(Number)
			.sort((a, b) => combinedScores[b] - combinedScores[a])
			.slice(0, topK);

		const results = sortedIndices.map((idx) => ({
			text: this.texts[idx],
			score: combinedScores[idx],
			denseContribution: (normalizedDense[idx] || 0) * this.denseWeight,
			sparseContribution: (normalizedSparse[idx] || 0) * this.sparseWeight,
			originalDenseScore: denseResults.find(r => r[0] === idx)?.[1] || 0,
			originalSparseScore: sparseResults.find(r => r[0] === idx)?.[1] || 0,
		}));

		return results;
	}
}

// مثال استفاده
async function main() {
	const texts = [
		"AvalAI provides access to a wide range of language models through a unified API.",
		"The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
		"Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
		"AvalAI offers tools for monitoring usage and managing costs effectively.",
go
package main

import (
	"context"
	"fmt"
	"log"
	"math"
	"os"
	"sort"
	"strings"

	"github.com/james-bowman/nlp" // کتابخانه برای TF-IDF
	"github.com/james-bowman/sparse" // برای ماتریس‌های پراکنده
	openai "github.com/openai/openai-go"
)

// HybridRetriever متدهای بازیابی متراکم و پراکنده را ترکیب می‌کند
type HybridRetriever struct {
	texts []string
	denseWeight float64
	sparseWeight float64

	// بازیابی متراکم
	embeddings [][]float32

	// بازیابی پراکنده
	vectorizer *nlp.CountVectorizer // برای تبدیل متن به شمارش کلمات
	transformer *nlp.TfidfTransformer // برای تبدیل شمارش کلمات به امتیازات TF-IDF
	docTermMatrix sparse.Matrix // ماتریس سند-عبارت TF-IDF

	avalaiClient *openai.Client
}

// NewHybridRetriever یک بازیاب ترکیبی جدید ایجاد می‌کند
func NewHybridRetriever(texts []string, avalaiClient *openai.Client, denseWeight float64) *HybridRetriever {
	return &HybridRetriever{
		texts: texts,
		denseWeight: denseWeight,
		sparseWeight: 1.0 - denseWeight,
		avalaiClient: avalaiClient,
	}
}

// Initialize بازیاب را آماده می‌کند
func (r *HybridRetriever) Initialize(ctx context.Context) error {
	log.Println("درحال راه‌اندازی بازیاب پراکنده (TF-IDF)...")
	// راه‌اندازی بازیابی پراکنده
	r.vectorizer = nlp.NewCountVectorizer(nlp.StopWords("en"), nlp.ToLower(true)) // اضافه کردن کلمات توقف و تبدیل به حروف کوچک
	r.transformer = nlp.NewTfidfTransformer()

	// ایجاد ماتریس سند-عبارت
	docTermCounts, err := r.vectorizer.FitTransform(r.texts...)
	if err != nil {
		return fmt.Errorf("خطا در برداری‌سازی متون: %w", err)
	}

	// اعمال تبدیل TF-IDF
	r.docTermMatrix, err = r.transformer.FitTransform(docTermCounts)
	if err != nil {
		return fmt.Errorf("خطا در تبدیل به TF-IDF: %w", err)
	}
	log.Println("بازیاب پراکنده با موفقیت راه‌اندازی شد.")

	// راه‌اندازی بازیابی متراکم با ایجاد امبدینگ‌ها
	log.Println("درحال ایجاد امبدینگ‌ها برای بازیابی متراکم...")
	r.embeddings = make([][]float32, len(r.texts))
	for i, text := range r.texts {
		embedding, errCr := r.createEmbedding(ctx, text)
		if errCr != nil {
			// در صورت خطا، می‌توانیم ادامه دهیم و فقط از بازیابی پراکنده استفاده کنیم یا خطا را برگردانیم
			log.Printf("هشدار: خطا در ایجاد امبدینگ برای متن %d ('%s'): %v. این متن در جستجوی متراکم نادیده گرفته می‌شود.", i, text[:30], errCr)
			// r.embeddings[i] = make([]float32, dimension) // یا یک بردار صفر قرار دهید
		} else {
			r.embeddings[i] = embedding
		}
	}
	log.Printf("%d امبدینگ برای بازیابی متراکم ایجاد شد.\n", len(r.embeddings))
	return nil
}

// createEmbedding امبدینگ برای یک متن معین تولید می‌کند
func (r *HybridRetriever) createEmbedding(ctx context.Context, text string) ([]float32, error) {
	resp, err := r.avalaiClient.CreateEmbedding(
		ctx,
		openai.EmbeddingRequest{
			Model: "text-embedding-3-large",
			Input: []string{text},
		},
	)
	if err != nil {
		return nil, err
	}
	if len(resp.Data) == 0 {
		return nil, fmt.Errorf("هیچ امبدینگی برای متن بازگردانده نشد: %s", text)
	}
	return resp.Data[0].Embedding, nil
}

// cosineSimilarity شباهت کسینوسی بین دو بردار را محاسبه می‌کند
func cosineSimilarity(a, b []float32) float64 {
	if len(a) == 0 || len(b) == 0 || len(a) != len(b) { // بررسی برای بردارهای خالی یا با ابعاد متفاوت
		return 0.0
	}
	var dotProduct, normA, normB float64
	for i := range a {
		dotProduct += float64(a[i] * b[i])
		normA += float64(a[i] * a[i])
		normB += float64(b[i] * b[i])
	}
	if normA == 0 || normB == 0 {
		return 0.0
	}
	return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}

type searchResult struct {
	Index int
	Score float64
}

// denseSearch بازیابی متراکم را با استفاده از شباهت برداری انجام می‌دهد
func (r *HybridRetriever) denseSearch(ctx context.Context, query string, topK int) ([]searchResult, error) {
	queryEmbedding, err := r.createEmbedding(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("خطا در ایجاد امبدینگ پرس و جو: %w", err)
	}

	similarities := make([]searchResult, 0, len(r.embeddings))
	for i, docEmbedding := range r.embeddings {
		if docEmbedding == nil { // اگر امبدینگ برای سندی ایجاد نشده باشد
			continue
		}
		similarities = append(similarities, searchResult{
			Index: i,
			Score: cosineSimilarity(queryEmbedding, docEmbedding),
		})
	}

	sort.Slice(similarities, func(i, j int) bool {
		return similarities[i].Score > similarities[j].Score
	})

	if topK > len(similarities) {
		topK = len(similarities)
	}
	return similarities[:topK], nil
}

// sparseSearch بازیابی پراکنده را با استفاده از TF-IDF انجام می‌دهد
func (r *HybridRetriever) sparseSearch(query string, topK int) ([]searchResult, error) {
	queryVector, err := r.vectorizer.Transform(query)
	if err != nil {
		return nil, fmt.Errorf("خطا در برداری‌سازی پرس و جو: %w", err)
	}
	queryTfidf, err := r.transformer.Transform(queryVector)
	if err != nil {
		return nil, fmt.Errorf("خطا در تبدیل TF-IDF پرس و جو: %w", err)
	}

	similarities := make([]searchResult, r.docTermMatrix.Rows())
	for i := 0; i < r.docTermMatrix.Rows(); i++ {
		docVector := r.docTermMatrix.RowView(i)
		score := nlp.CosineSimilarity(queryTfidf, docVector) // nlp.CosineSimilarity امتیازات بین 0 و 1 را برمی‌گرداند
		if math.IsNaN(score) { // بررسی NaN
			score = 0.0
		}
		similarities[i] = searchResult{Index: i, Score: score}
	}

	sort.Slice(similarities, func(i, j int) bool {
		return similarities[i].Score > similarities[j].Score
	})

	if topK > len(similarities) {
		topK = len(similarities)
	}
	return similarities[:topK], nil
}

// Search جستجوی ترکیبی را با ترکیب بازیابی متراکم و پراکنده انجام می‌دهد
func (r *HybridRetriever) Search(ctx context.Context, query string, topK int) ([]map[string]interface{}, error) {
	denseResults, errDense := r.denseSearch(ctx, query, topK*2) // دریافت نتایج بیشتر
	if errDense != nil {
		log.Printf("هشدار: خطای بازیابی متراکم: %v. ادامه با بازیابی پراکنده...\n", errDense)
		denseResults = []searchResult{} // در صورت خطا، نتایج متراکم را خالی در نظر بگیرید
	}

	sparseResults, errSparse := r.sparseSearch(query, topK*2)
	if errSparse != nil {
		log.Printf("هشدار: خطای بازیابی پراکنده: %v. ادامه با بازیابی متراکم...\n", errSparse)
		sparseResults = []searchResult{} // در صورت خطا، نتایج پراکنده را خالی در نظر بگیرید
	}

	if errDense != nil && errSparse != nil {
		return nil, fmt.Errorf("هر دو بازیابی متراکم و پراکنده با خطا مواجه شدند. متراکم: %v، پراکنده: %v", errDense, errSparse)
	}


	// نرمال‌سازی امتیازات
	maxDenseScore := 0.0
	if len(denseResults) > 0 {
		for _, result := range denseResults {
			if result.Score > maxDenseScore { maxDenseScore = result.Score }
		}
	}
	if maxDenseScore == 0 && len(denseResults) > 0 { maxDenseScore = 1.0 } // اگر همه امتیازات 0 باشند، از تقسیم بر صفر جلوگیری کنید

	maxSparseScore := 0.0
	if len(sparseResults) > 0 {
		for _, result := range sparseResults {
			if result.Score > maxSparseScore { maxSparseScore = result.Score }
		}
	}
	if maxSparseScore == 0 && len(sparseResults) > 0 { maxSparseScore = 1.0 }


	normalizedDense := make(map[int]float64)
	for _, result := range denseResults {
		if maxDenseScore > 0 {
			normalizedDense[result.Index] = result.Score / maxDenseScore
		} else {
			normalizedDense[result.Index] = 0.0
		}
	}

	normalizedSparse := make(map[int]float64)
	for _, result := range sparseResults {
		if maxSparseScore > 0 {
			normalizedSparse[result.Index] = result.Score / maxSparseScore
		} else {
			normalizedSparse[result.Index] = 0.0
		}
	}

	// ترکیب امتیازات
	combinedScores := make(map[int]float64)
	allIndices := make(map[int]bool)
	for _, res := range denseResults { allIndices[res.Index] = true }
	for _, res := range sparseResults { allIndices[res.Index] = true }


	for idx := range allIndices {
		denseS := normalizedDense[idx] // اگر وجود نداشته باشد، 0.0 است
		sparseS := normalizedSparse[idx]
		combinedScores[idx] = (denseS * r.denseWeight) + (sparseS * r.sparseWeight)
	}

	type rankedResult struct {
		Index int
		Score float64
		DenseContribution float64
		SparseContribution float64
		OriginalDenseScore float64
		OriginalSparseScore float64
	}

	var finalRankedResults []rankedResult
	for idx, score := range combinedScores {
		denseCont := (normalizedDense[idx] * r.denseWeight)
		sparseCont := (normalizedSparse[idx] * r.sparseWeight)

		var origDense, origSparse float64
		for _, dr := range denseResults { if dr.Index == idx { origDense = dr.Score; break } }
		for _, sr := range sparseResults { if sr.Index == idx { origSparse = sr.Score; break } }

		finalRankedResults = append(finalRankedResults, rankedResult{
			Index: idx,
			Score: score,
			DenseContribution: denseCont,
			SparseContribution: sparseCont,
			OriginalDenseScore: origDense,
			OriginalSparseScore: origSparse,
		})
	}

	sort.Slice(finalRankedResults, func(i, j int) bool {
		return finalRankedResults[i].Score > finalRankedResults[j].Score
	})

	if topK > len(finalRankedResults) {
		topK = len(finalRankedResults)
	}

	outputResults := make([]map[string]interface{}, topK)
	for i := 0; i < topK; i++ {
		res := finalRankedResults[i]
		outputResults[i] = map[string]interface{}{
			"text": r.texts[res.Index],
			"score": res.Score,
			"dense_contribution": res.DenseContribution,

6. رتبه‌بندی مجدد (Re-ranking)

رتبه‌بندی مجدد با اعمال یک مدل پیچیده‌تر برای مرتب‌سازی مجدد نتایج بازیابی اولیه، کیفیت بازیابی را بهبود می‌بخشد.

بهترین شیوه‌ها

  • مدل‌های Cross-Encoder: از cross-encoderها برای ارزیابی بهتر ارتباط استفاده کنید.
  • بازیابی دو مرحله‌ای: ابتدا تعداد بیشتری سند را بازیابی کنید، سپس رتبه‌بندی مجدد انجام دهید.
  • غنی‌سازی ویژگی‌ها: ویژگی‌های اضافی مانند تازگی یا محبوبیت را در رتبه‌بندی مجدد لحاظ کنید.
  • تنوع‌بخشی (Diversification): از تنوع نتایج برای پوشش جنبه‌های مختلف پرس و جو اطمینان حاصل کنید.
python
from openai import OpenAI
import numpy as np
from sentence_transformers import CrossEncoder  # کتابخانه برای مدل‌های Cross-Encoder

client = OpenAI(
    api_key="your-avalai-api-key",
    base_url="https://api.avalai.ir/v1",  # نقطه پایانی AvalAI API
)


class ReRanker:
    """اسناد بازیابی شده را با استفاده از یک مدل cross-encoder رتبه‌بندی مجدد می‌کند."""

    def __init__(self, model_name="cross-encoder/ms-marco-MiniLM-L-6-v2"):
        # مدل‌های پیشنهادی دیگر برای زبان فارسی ممکن است نیاز به بررسی داشته باشند
        # "cross-encoder/ms-marco-MiniLM-L-6-v2" یک مدل چندزبانه خوب است
        self.model = CrossEncoder(model_name)
        print(f"مدل CrossEncoder '{model_name}' بارگذاری شد.")

    def rerank(self, query, documents, top_k=None):
        """
        اسناد را بر اساس ارتباط با پرس و جو رتبه‌بندی مجدد می‌کند.

        Args:
                query: پرس و جوی جستجو
                documents: لیستی از متون اسناد یا دیکشنری‌هایی با کلید 'text'
                top_k: تعداد اسناد برتر برای بازگرداندن (None برای همه)

        Returns:
                لیستی از دیکشنری‌ها با متن و امتیاز
        """
        if not documents:
            return []

        # آماده‌سازی متون اسناد
        if isinstance(documents[0], dict):
            texts = [doc["text"] for doc in documents]
        else:
            texts = documents  # فرض بر این است که لیستی از رشته‌ها است

        # ایجاد جفت‌های (پرس و جو، سند)
        pairs = [[query, text] for text in texts]

        # دریافت امتیازات ارتباط
        print(f"درحال محاسبه امتیازات ارتباط برای {len(pairs)} جفت...")
        scores = self.model.predict(pairs, show_progress_bar=True)  # نمایش نوار پیشرفت

        # ایجاد نتایج با امتیازات
        results = []
        for i, text_content in enumerate(
            texts
        ):  # استفاده از texts به جای documents برای سازگاری
            result = {"text": text_content, "score": float(scores[i])}

            # اضافه کردن متادیتای اصلی در صورت وجود
            if isinstance(documents[0], dict) and i < len(documents):
                # اطمینان از اینکه documents[i] یک دیکشنری است و کلیدهای دیگر دارد
                original_doc = documents[i]
                if isinstance(original_doc, dict):
                    for key, value in original_doc.items():
                        if key not in [
                            "text",
                            "score",
                            "similarity",
                        ]:  # از بازنویسی امتیازات قبلی جلوگیری کنید
                            result[key] = value
            results.append(result)

        # مرتب‌سازی بر اساس امتیاز (نزولی)
        results.sort(key=lambda x: x["score"], reverse=True)

        # بازگرداندن نتایج top-k
        if top_k is not None:
            results = results[:top_k]

        return results


# مثال استفاده
def create_embedding(text):
    """امبدینگ برای یک متن معین تولید می‌کند."""
    response = client.embeddings.create(model="text-embedding-3-large", input=text)
    return response.data[0].embedding


def vector_search(query, documents_with_meta, top_k=5):
    """تابع جستجوی برداری ساده."""
    if not documents_with_meta:
        return []

    query_embedding = create_embedding(query)
    results = []

    for doc in documents_with_meta:
        doc_text = doc.get("text")
        if not doc_text:  # اگر متن وجود نداشته باشد، از این سند صرف‌نظر کنید
            continue

        doc_embedding = create_embedding(doc_text)

        # اطمینان از اینکه امبدینگ‌ها خالی نیستند
        if (
            query_embedding is None
            or doc_embedding is None
            or len(query_embedding) == 0
            or len(doc_embedding) == 0
        ):
            similarity = 0.0
        else:
            similarity = np.dot(query_embedding, doc_embedding) / (
                np.linalg.norm(query_embedding) * np.linalg.norm(doc_embedding)
            )

        # کپی کردن متادیتا برای جلوگیری از تغییر شی اصلی
        current_result = {
            "text": doc_text,
            "metadata": doc.get("metadata", {}).copy(),  # کپی کردن متادیتا
            "similarity": similarity,  # امتیاز اولیه از جستجوی برداری
        }
        results.append(current_result)

    results.sort(key=lambda x: x["similarity"], reverse=True)
    return results[:top_k]


# اسناد نمونه
documents_for_rerank = [
    {
        "text": "AvalAI provides access to a wide range of language models through a unified API.",
        "metadata": {"source": "docs", "page": 1, "id": "doc1"},
    },
    {
        "text": "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
        "metadata": {"source": "docs", "page": 1, "id": "doc2"},
    },
    {
        "text": "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
        "metadata": {"source": "docs", "page": 2, "id": "doc3"},
    },
    {
        "text": "AvalAI offers tools for monitoring usage and managing costs effectively.",
        "metadata": {"source": "docs", "page": 3, "id": "doc4"},
    },
    {
        "text": "The documentation includes examples and best practices for different use cases.",
        "metadata": {"source": "docs", "page": 4, "id": "doc5"},
    },
]

query_for_rerank = "Which AI models does AvalAI support?"
javascript
import { OpenAI } from "openai";

const client = new OpenAI({
  apiKey: process.env.AVALAI_API_KEY,
  baseURL: "https://api.avalai.ir/v1", // نقطه پایانی AvalAI API
});

class ReRanker {
  /**
   * اسناد بازیابی شده را با استفاده از مدل رتبه‌بندی مجدد AvalAI رتبه‌بندی مجدد می‌کند
   */
  constructor() {
    // استفاده از مدل رتبه‌بندی مجدد AvalAI
    this.modelName = "rerank-multilingual-v2.0"; // یا مدل دیگری که AvalAI ارائه می‌دهد
    console.log(`آماده استفاده از مدل رتبه‌بندی مجدد: ${this.modelName}`);
  }

  /**
   * اسناد را بر اساس ارتباط با پرس و جو رتبه‌بندی مجدد می‌کند
   * @param {string} query - پرس و جوی جستجو
   * @param {Array<string|Object>} documents - لیستی از متون اسناد یا اشیا با کلید 'text'
   * @param {number} [topK=null] - تعداد اسناد برتر برای بازگرداندن (اختیاری)
   * @returns {Promise<Array<Object>>} - لیستی از اسناد با امتیازها
   */
  async rerank(query, documents, topK = null) {
    if (!documents || documents.length === 0) {
      return [];
    }
    // آماده‌سازی متون اسناد
    const texts = documents.map((doc) =>
      typeof doc === "string" ? doc : doc.text,
    );

    console.log(
      `درحال ارسال ${texts.length} سند به API رتبه‌بندی مجدد برای پرس و جوی: '${query}'`,
    );
    // ایجاد درخواست به API رتبه‌بندی مجدد
    const response = await client.reranking.create({
      model: this.modelName,
      query: query,
      documents: texts, // ارسال فقط متون
      top_n: topK || texts.length, // اگر topK مشخص نشده، همه نتایج را برگردان
    });

    // client.reranking.create مستقیما نتایج مرتب شده را برمی‌گرداند
    // هر نتیجه شامل 'document' (متن اصلی) و 'relevance_score' است
    // و همچنین 'index' که به موقعیت اصلی سند در لیست ورودی اشاره دارد

    const rerankedResults = response.results.map((apiResult) => {
      const originalDocument = documents[apiResult.index]; // دریافت سند اصلی با استفاده از شاخص
      const resultObj = {
        text: apiResult.document.text, // متن از پاسخ API
        score: apiResult.relevanceScore, // امتیاز ارتباط از API
      };

      // اضافه کردن متادیتای اصلی در صورت وجود
      if (typeof originalDocument === "object" && originalDocument.metadata) {
        resultObj.metadata = originalDocument.metadata;
      } else if (typeof originalDocument === "object") {
        // اگر originalDocument یک شی است اما فاقد فیلد metadata است،
        // سایر فیلدها (به جز text) را به عنوان متادیتا در نظر بگیرید
        resultObj.metadata = { ...originalDocument };
        delete resultObj.metadata.text; // حذف کلید متن از متادیتا
      }
      return resultObj;
    });

    // API قبلا نتایج را بر اساس امتیاز مرتب کرده و top_n را اعمال کرده است
    return rerankedResults;
  }
}

/**
 * امبدینگ برای یک متن معین تولید می‌کند
 * @param {string} text - متن ورودی
 * @returns {Promise<number[]>} - بردار امبدینگ
 */
async function createEmbedding(text) {
  const response = await client.embeddings.create({
    model: "text-embedding-3-large",
    input: text,
  });
  return response.data[0].embedding;
}

/**
 * تابع جستجوی برداری ساده
 * @param {string} query - پرس و جوی جستجو
 * @param {Array<Object>} documentsWithMeta - لیستی از اسناد با متادیتا
 * @param {number} topK - تعداد نتایج برای بازگرداندن
 * @returns {Promise<Array<Object>>} - نتایج برتر
 */
async function vectorSearch(query, documentsWithMeta, topK = 5) {
  if (!documentsWithMeta || documentsWithMeta.length === 0) return [];

  const queryEmbedding = await createEmbedding(query);
  const results = [];

  for (const doc of documentsWithMeta) {
    const docText = doc.text;
    if (!docText) continue;

    const docEmbedding = await createEmbedding(docText);

    let dotProduct = 0;
    let normA = 0;
    let normB = 0;
    for (let i = 0; i < queryEmbedding.length; i++) {
      dotProduct += queryEmbedding[i] * docEmbedding[i];
      normA += queryEmbedding[i] * queryEmbedding[i];
      normB += docEmbedding[i] * docEmbedding[i];
    }
    const similarity =
      normA === 0 || normB === 0
        ? 0
        : dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));

    results.push({
      text: docText,
      metadata: doc.metadata || {},
      similarity: similarity,
    });
  }

  results.sort((a, b) => b.similarity - a.similarity);
  return results.slice(0, topK);
}

// مثال استفاده
async function main() {
  // اسناد نمونه
  const documentsForJsRerank = [
    {
      text: "AvalAI provides access to a wide range of language models through a unified API.",
      metadata: { source: "docs-js", page: 1, id: "jsdoc1" },
    },
    {
      text: "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
      metadata: { source: "docs-js", page: 1, id: "jsdoc2" },
    },
    {
      text: "Each model has different capabilities and pricing, so it's important to understand the tradeoffs.",
      metadata: { source: "docs-js", page: 2, id: "jsdoc3" },
    },
    {
      text: "AvalAI offers tools for monitoring usage and managing costs effectively.",
      metadata: { source: "docs-js", page: 3, id: "jsdoc4" },
    },
    {
      text: "The documentation includes examples and best practices for different use cases.",
      metadata: { source: "docs-js", page: 4, id: "jsdoc5" },
    },
  ];

  const queryForJsRerank = "Which AI models does AvalAI support?";

  // بازیابی مرحله اول
  console.log("درحال انجام بازیابی مرحله اول (جاوااسکریپت)...");
  const firstStageResultsJs = await vectorSearch(
    queryForJsRerank,
    documentsForJsRerank,
    4,
  );

  console.log("\nنتایج بازیابی مرحله اول (جاوااسکریپت):");
  firstStageResultsJs.forEach((result, i) => {
    console.log(
      `${i + 1}. ${result.text} (امتیاز اولیه: ${result.similarity.toFixed(4)})`,
    );
    console.log(` متادیتا: ${JSON.stringify(result.metadata)}`);
  });

  // رتبه‌بندی مجدد
  const rerankerJs = new ReRanker();
  console.log("\nدرحال انجام رتبه‌بندی مجدد (جاوااسکریپت)...");
  // ارسال نتایج مرحله اول (که شامل متادیتا است) به rerank
  const rerankedResultsJs = await rerankerJs.rerank(
    queryForJsRerank,
    firstStageResultsJs,
    2,
  );

  console.log("\nنتایج رتبه‌بندی مجدد (جاوااسکریپت):");
  rerankedResultsJs.forEach((result, i) => {
    console.log(
      `${i + 1}. ${result.text} (امتیاز رتبه‌بندی مجدد: ${result.score.toFixed(4)})`,
    );
    const originalMetadata = result.metadata || {};
    console.log(
      ` منبع: ${originalMetadata.source}, صفحه: ${originalMetadata.page}, شناسه: ${originalMetadata.id}`,
    );
  });
}

main().catch(console.error);
bash
# استفاده از cURL برای رتبه‌بندی مجدد اسناد با AvalAI API
# اطمینان حاصل کنید که متغیر محیطی AVALAI_API_KEY شما تنظیم شده است
# export AVALAI_API_KEY="YOUR_ACTUAL_API_KEY"

MODEL_RERANK="rerank-multilingual-v2.0" # یا هر مدل رتبه‌بندی مجدد دیگری که AvalAI پشتیبانی می‌کند
QUERY_RERANK="Which AI models does AvalAI support?"
# اسنادی که باید رتبه‌بندی مجدد شوند (می‌تواند خروجی یک مرحله بازیابی اولیه باشد)
DOCUMENTS_JSON_RERANK='[
	"AvalAI provides access to a wide range of language models through a unified API.",
	"The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
	"Each model has different capabilities and pricing, so it\\'s important to understand the tradeoffs.",

7. تولید (Generation)

مرحله نهایی در یک جریان کاری RAG، تولید پاسخ بر اساس اطلاعات بازیابی شده است. این شامل ارائه زمینه مناسب به LLM و دادن پرامپت موثر به آن است.

بهترین شیوه‌ها

  • فرمت زمینه: زمینه بازیابی شده را به روشی واضح و سازمان‌یافته ساختاربندی کنید.
  • استناد (Citation): اطلاعات منبع را برای ارجاع‌دهی لحاظ کنید.
  • مهندسی پرامپت (Prompt Engineering): از پرامپت‌های موثری استفاده کنید که به مدل دستور می‌دهند چگونه از اطلاعات بازیابی شده استفاده کند.
  • پاسخ‌های جریانی (Streaming): برای تجربه کاربری بهتر با پاسخ‌های طولانی، از پاسخ‌های جریانی استفاده کنید.
  • ارزیابی: کیفیت پاسخ‌های تولید شده را به طور منظم ارزیابی کنید.
python
from openai import OpenAI

client = OpenAI(
    api_key="your-avalai-api-key",
    base_url="https://api.avalai.ir/v1",  # نقطه پایانی AvalAI API
)


def generate_rag_response(query, retrieved_documents, model="gpt-5.5"):
    """
    پاسخی را بر اساس اسناد بازیابی شده تولید می‌کند.

    Args:
            query: سوال کاربر
            retrieved_documents: لیستی از اسناد بازیابی شده با متادیتا
            model: مدلی که برای تولید استفاده می‌شود

    Returns:
            پاسخ تولید شده
    """
    if not retrieved_documents:
        # اگر هیچ سندی بازیابی نشد، یک پاسخ استاندارد بدون زمینه ارائه دهید
        # یا به کاربر اطلاع دهید که اطلاعات کافی یافت نشد
        print(
            "هیچ سند مرتبطی برای تولید پاسخ یافت نشد. تلاش برای پاسخ بدون زمینه اضافی..."
        )
        plain_response = client.chat.completions.create(
            model=model,
            messages=[
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": query},
            ],
        )
        return plain_response.choices[0].message.content

    # فرمت‌بندی زمینه بازیابی شده با استنادات
    formatted_context = ""
    for i, doc in enumerate(retrieved_documents):
        # اطمینان از اینکه doc یک دیکشنری است و کلید 'text' دارد
        doc_text = doc.get("text", "محتوای سند در دسترس نیست")
        metadata = doc.get("metadata", {})
        source_info = f"[{i+1}] منبع: {metadata.get('source', 'نامشخص')}"
        if "page" in metadata:
            source_info += f"، صفحه: {metadata['page']}"

        formatted_context += f"{doc_text}\n{source_info}\n\n"

    # ایجاد پرامپت سیستم
    system_prompt = f"""شما یک دستیار مفید هستید که به سوالات بر اساس زمینه ارائه شده پاسخ می‌دهید.
	این قوانین را دنبال کنید:
	1. فقط بر اساس زمینه ارائه شده پاسخ دهید.
	2. اگر زمینه حاوی پاسخ نیست، بگویید "اطلاعات کافی برای پاسخ به این سوال ندارم".
	3. هنگام ارجاع به اطلاعات از زمینه، استنادات [1]، [2] و غیره را لحاظ کنید.
	4. مختصر و مفید باشید.
	5. پاسخ خود را به روشی واضح و خوانا فرمت‌بندی کنید."""

    # ایجاد پرامپت کاربر
    user_prompt = f"""زمینه:
	{formatted_context}

	سوال: {query}"""

    # تولید پاسخ
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
    )

    return response.choices[0].message.content


# مثال استفاده
query_gen = "Which AI models does AvalAI support?"

# اسناد بازیابی شده نمونه (در یک سناریوی واقعی، این‌ها از مرحله بازیابی می‌آیند)
retrieved_documents_gen = [
    {
        "text": "AvalAI provides access to a wide range of language models through a unified API.",
        "metadata": {"source": "docs-py", "page": 1},
        "score": 0.92,  # امتیاز از مرحله بازیابی/رتبه‌بندی مجدد
    },
    {
        "text": "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
        "metadata": {"source": "docs-py", "page": 1},
        "score": 0.87,
    },
    {  # سند اضافی برای نشان دادن عدم پاسخ در صورت عدم ارتباط
        "text": "The weather in Tehran is sunny today.",
        "metadata": {"source": "weather-api", "city": "Tehran"},
        "score": 0.1,
    },
]

# فیلتر کردن اسناد بازیابی شده بر اساس یک آستانه امتیاز (اختیاری)
# threshold = 0.5
# filtered_documents = [doc for doc in retrieved_documents_gen if doc.get("score", 0) > threshold]

response_gen = generate_rag_response(
    query_gen, retrieved_documents_gen
)  # استفاده از همه اسناد برای این مثال
print(f"\nپرس و جو: {query_gen}\n")
print(f"پاسخ:\n{response_gen}")

query_gen_no_context = "What is the capital of France?"
response_no_context = generate_rag_response(query_gen_no_context, [])
print(f"\nپرس و جو (بدون زمینه بازیابی شده): {query_gen_no_context}\n")
print(f"پاسخ:\n{response_no_context}")
javascript
import { OpenAI } from "openai";

const client = new OpenAI({
  apiKey: process.env.AVALAI_API_KEY,
  baseURL: "https://api.avalai.ir/v1", // نقطه پایانی AvalAI API
});

/**
 * پاسخی را بر اساس اسناد بازیابی شده تولید می‌کند
 * @param {string} query - سوال کاربر
 * @param {Array<Object>} retrievedDocuments - لیستی از اسناد بازیابی شده با متادیتا
 * @param {string} [model="gpt-5.5"] - مدلی که برای تولید استفاده می‌شود
 * @returns {Promise<string>} - پاسخ تولید شده
 */
async function generateRagResponse(
  query,
  retrievedDocuments,
  model = "gpt-5.5",
) {
  if (!retrievedDocuments || retrievedDocuments.length === 0) {
    console.log(
      "هیچ سند مرتبطی برای تولید پاسخ یافت نشد. تلاش برای پاسخ بدون زمینه اضافی...",
    );
    const plainResponse = await client.chat.completions.create({
      model: model,
      messages: [
        { role: "system", content: "You are a helpful assistant." },
        { role: "user", content: query },
      ],
    });
    return plainResponse.choices[0].message.content;
  }
  // فرمت‌بندی زمینه بازیابی شده با استنادات
  let formattedContext = "";

  retrievedDocuments.forEach((doc, i) => {
    const docText = doc.text || "محتوای سند در دسترس نیست";
    const metadata = doc.metadata || {};
    let sourceInfo = `[${i + 1}] منبع: ${metadata.source || "نامشخص"}`;

    if (metadata.page !== undefined) {
      sourceInfo += `, صفحه: ${metadata.page}`;
    }
    formattedContext += `${docText}\n${sourceInfo}\n\n`;
  });

  // ایجاد پرامپت سیستم
  const systemPrompt = `شما یک دستیار مفید هستید که به سوالات بر اساس زمینه ارائه شده پاسخ می‌دهید.
	این قوانین را دنبال کنید:
	1. فقط بر اساس زمینه ارائه شده پاسخ دهید.
	2. اگر زمینه حاوی پاسخ نیست، بگویید "اطلاعات کافی برای پاسخ به این سوال ندارم".
	3. هنگام ارجاع به اطلاعات از زمینه، استنادات [1]، [2] و غیره را لحاظ کنید.
	4. مختصر و مفید باشید.
	5. پاسخ خود را به روشی واضح و خوانا فرمت‌بندی کنید.`;

  // ایجاد پرامپت کاربر
  const userPrompt = `زمینه:
	${formattedContext}

	سوال: ${query}`;

  // تولید پاسخ
  const response = await client.chat.completions.create({
    model: model,
    messages: [
      { role: "system", content: systemPrompt },
      { role: "user", content: userPrompt },
    ],
  });

  return response.choices[0].message.content;
}

// مثال استفاده
async function mainGeneration() {
  const queryGenJs = "Which AI models does AvalAI support?";

  const retrievedDocumentsJs = [
    {
      text: "AvalAI provides access to a wide range of language models through a unified API.",
      metadata: { source: "docs-js", page: 1 },
      score: 0.92,
    },
    {
      text: "The platform supports models from various providers including OpenAI, Anthropic, Google, and more.",
      metadata: { source: "docs-js", page: 1 },
      score: 0.87,
    },
  ];

  const responseJs = await generateRagResponse(
    queryGenJs,
    retrievedDocumentsJs,
  );
  console.log(`\nپرس و جو: ${queryGenJs}\n`);
  console.log(`پاسخ:\n${responseJs}`);

  const queryGenJsNoContext = "What is the capital of Iran?";
  const responseJsNoContext = await generateRagResponse(
    queryGenJsNoContext,
    [],
  );
  console.log(`\nپرس و جو (بدون زمینه بازیابی شده): ${queryGenJsNoContext}\n`);
  console.log(`پاسخ:\n${responseJsNoContext}`);
}

mainGeneration().catch(console.error);
go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings" // برای strings.Builder

	openai "github.com/openai/openai-go"
)

// GenerateRagResponse پاسخی را بر اساس اسناد بازیابی شده تولید می‌کند
func GenerateRagResponse(ctx context.Context, client *openai.Client, query string, retrievedDocuments []map[string]interface{}, model string) (string, error) {
	if len(retrievedDocuments) == 0 {
		log.Println("هیچ سند مرتبطی برای تولید پاسخ یافت نشد. تلاش برای پاسخ بدون زمینه اضافی...")
		plainResponse, err := client.CreateChatCompletion(
			ctx,
			openai.ChatCompletionRequest{
				Model: model,
				Messages: []openai.ChatCompletionMessage{
					{Role: openai.ChatMessageRoleSystem, Content: "You are a helpful assistant."},
					{Role: openai.ChatMessageRoleUser, Content: query},
				},
			},
		)
		if err != nil {
			return "", fmt.Errorf("خطا در ایجاد پاسخ ساده: %w", err)
		}
		return plainResponse.Choices[0].Message.Content, nil
	}

	// فرمت‌بندی زمینه بازیابی شده با استنادات
	var formattedContext strings.Builder

	for i, doc := range retrievedDocuments {
		text, okText := doc["text"].(string)
		if !okText {
			text = "محتوای سند در دسترس نیست"
		}

		metadata, okMeta := doc["metadata"].(map[string]interface{})
		if !okMeta {
			metadata = make(map[string]interface{}) // ایجاد یک مپ خالی اگر وجود ندارد
		}


		source, okSource := metadata["source"].(string)
		if !okSource {
			source = "نامشخص"
		}
		sourceInfo := fmt.Sprintf("[%d] منبع: %s", i+1, source)

		if page, okPage := metadata["page"]; okPage {
			sourceInfo += fmt.Sprintf("، صفحه: %v", page)
		}

		formattedContext.WriteString(fmt.Sprintf("%s\n%s\n\n", text, sourceInfo))
	}

	// ایجاد پرامپت سیستم
	systemPrompt := `شما یک دستیار مفید هستید که به سوالات بر اساس زمینه ارائه شده پاسخ می‌دهید.
	این قوانین را دنبال کنید:
	1. فقط بر اساس زمینه ارائه شده پاسخ دهید.
	2. اگر زمینه حاوی پاسخ نیست، بگویید "اطلاعات کافی برای پاسخ به این سوال ندارم".
	3. هنگام ارجاع به اطلاعات از زمینه، استنادات [1]، [2] و غیره را لحاظ کنید.
	4. مختصر و مفید باشید.
	5. پاسخ خود را به روشی واضح و خوانا فرمت‌بندی کنید.`

	// ایجاد پرامپت کاربر
	userPrompt := fmt.Sprintf(`زمینه:
	%s

	سوال: %s`, formattedContext.String(), query)

	// تولید پاسخ
	resp, err := client.CreateChatCompletion(
		ctx,
		openai.ChatCompletionRequest{
			Model: model,
			Messages: []openai.ChatCompletionMessage{
				{
نسخه معادل Responses API

وقتی مدل انتخابی از /v1/responses پشتیبانی می‌کند، این نسخه را کنار مثال Chat Completions استفاده کنید. messages به input منتقل می‌شود و متن نهایی از response.output_text خوانده می‌شود.

python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["AVALAI_API_KEY"],
    base_url="https://api.avalai.ir/v1",
)

response = client.responses.create(
    model="gpt-5.5",
    input=[
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": "What's the weather like in Boston?"},
                {"type": "input_file", "file_id": "file_abc123"},
            ],
        }
    ],
)

print(response.output_text)
javascript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.AVALAI_API_KEY,
  baseURL: "https://api.avalai.ir/v1",
});

const response = await client.responses.create({
  model: "gpt-5.5",
  input: [
    {
      role: "user",
      content: [
        { type: "input_text", text: "What's the weather like in Boston?" },
        { type: "input_file", file_id: "file_abc123" },
      ],
    },
  ],
});

console.log(response.output_text);
bash
curl https://api.avalai.ir/v1/responses \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $AVALAI_API_KEY" \
  -d '
  {
    "model": "gpt-5.5",
    "input": [
      {
        "role": "user",
        "content": [
          {
            "type": "input_text",
            "text": "What's the weather like in Boston?"
          },
          {
            "type": "input_file",
            "file_id": "file_abc123"
          }
        ]
      }
    ]
  }'
  • messagesinput
  • پیام سیستمی → instructions یا آیتم developer
  • choices[0].message.contentresponse.output_text
  • برای ابزارها و خروجی‌های چندوجهی، response.output را بر اساس type بررسی کنید.

8. کنار هم قرار دادن همه چیز (Putting It All Together)

یک جریان کاری RAG کامل، تمام اجزایی را که بحث کردیم ترکیب می‌کند. در اینجا مثالی از یک پیاده‌سازی کامل RAG آورده شده است:

python
from openai import OpenAI
import numpy as np
from typing import List, Dict, Any
import faiss  # برای ذخیره‌سازی و جستجوی برداری کارآمد
import pickle  # برای ذخیره و بارگذاری وضعیت جریان کاری

# راه‌اندازی کلاینت
# مطمئن شوید که متغیر محیطی AVALAI_API_KEY شما تنظیم شده است
# یا کلید API خود را مستقیما اینجا قرار دهید (برای تولید توصیه نمی‌شود)
client = OpenAI(
    api_key=os.getenv("AVALAI_API_KEY", "your-avalai-api-key"),
    base_url="https://api.avalai.ir/v1",  # نقطه پایانی AvalAI API
)


class RAGPipeline:
    """پیاده‌سازی کامل جریان کاری RAG."""

    def __init__(self, model="gpt-5.5", embedding_model="text-embedding-3-large"):
        self.model = model
        self.embedding_model = embedding_model
        self.index = None  # شاخص FAISS
        self.texts = []  # لیست متون اصلی
        self.metadata = []  # لیست متادیتای متناظر
        print(
            f"جریان کاری RAG با مدل '{model}' و مدل امبدینگ '{embedding_model}' راه‌اندازی شد."
        )

    def add_documents(self, documents: List[Dict[str, Any]]):
        """
        اسناد را به جریان کاری RAG اضافه می‌کند.

        Args:
                documents: لیستی از دیکشنری‌ها با کلید 'text' و 'metadata' اختیاری
        """
        if not documents:
            print("هیچ سندی برای اضافه کردن ارائه نشده است.")
            return []

        texts_to_add = [doc["text"] for doc in documents if "text" in doc]
        metadata_to_add = [
            doc.get("metadata", {}) for doc in documents if "text" in doc
        ]

        if not texts_to_add:
            print("هیچ متن معتبری در اسناد ارائه شده برای اضافه کردن یافت نشد.")
            return []

        print(f"درحال ایجاد امبدینگ برای {len(texts_to_add)} سند جدید...")
        # ایجاد امبدینگ‌ها
        embeddings = [self._create_embedding(text) for text in texts_to_add]
        # حذف مواردی که امبدینگ برای آن‌ها ایجاد نشده است (None)
        valid_embeddings = [emb for emb in embeddings if emb is not None]
        valid_texts = [
            texts_to_add[i] for i, emb in enumerate(embeddings) if emb is not None
        ]
        valid_metadata = [
            metadata_to_add[i] for i, emb in enumerate(embeddings) if emb is not None
        ]

        if not valid_embeddings:
            print("امکان ایجاد هیچ امبدینگ معتبری برای اسناد ارائه شده وجود نداشت.")
            return []

        embeddings_array = np.array(valid_embeddings).astype("float32")
        print(f"{len(valid_embeddings)} امبدینگ با موفقیت ایجاد شد.")

        # راه‌اندازی یا به‌روزرسانی شاخص
        if self.index is None:
            dimension = len(valid_embeddings[0])
            self.index = faiss.IndexFlatL2(dimension)
            print(f"شاخص FAISS با ابعاد {dimension} راه‌اندازی شد.")

        # اضافه کردن به شاخص
        self.index.add(embeddings_array)
        print(
            f"{embeddings_array.shape[0]} بردار به شاخص FAISS اضافه شد. تعداد کل بردارها: {self.index.ntotal}"
        )

        # ذخیره متون و متادیتا
        start_idx = len(self.texts)
        self.texts.extend(valid_texts)
        self.metadata.extend(valid_metadata)

        print(f"{len(valid_texts)} سند به پایگاه داده داخلی اضافه شد.")
        return list(range(start_idx, start_idx + len(valid_texts)))

    def _create_embedding(self, text):
        """امبدینگ برای یک متن معین تولید می‌کند."""
        try:
            response = client.embeddings.create(model=self.embedding_model, input=text)
            return response.data[0].embedding
        except Exception as e:
            print(f"خطا در ایجاد امبدینگ برای متن '{text[:50]}...': {e}")
            return None

    def _query_needs_retrieval(self, query):
        """تعیین می‌کند که آیا پرس و جو نیاز به بازیابی خارجی دارد."""
        try:
            response = client.chat.completions.create(
                model=self.model,  # استفاده از مدل اصلی برای طبقه‌بندی
                messages=[
                    {
                        "role": "system",
                        "content": "You are a query classifier. Respond with 'RETRIEVE' if the query requires external knowledge, or 'SUFFICIENT' if the model's knowledge is enough. Be concise.",
                    },
                    {"role": "user", "content": query},
                ],
                max_tokens=10,  # محدود کردن توکن‌ها برای پاسخ طبقه‌بندی
            )
            classification = response.choices[
                0
            ].message.content.upper()  # تبدیل به حروف بزرگ برای تطابق قابل اعتماد
            print(f"طبقه‌بندی پرس و جو '{query}': {classification}")
            return "RETRIEVE" in classification
        except Exception as e:
            print(f"خطا در طبقه‌بندی پرس و جو: {e}. فرض بر نیاز به بازیابی.")
            return True  # در صورت خطا، به طور پیش‌فرض بازیابی انجام شود

    def _retrieve(self, query, k=5):
        """اسناد مرتبط را برای یک پرس و جو بازیابی می‌کند."""
        if self.index is None or self.index.ntotal == 0:
            print("شاخص خالی است. امکان بازیابی وجود ندارد.")
            return []

        print(f"درحال بازیابی {k} سند برتر برای پرس و جوی: '{query}'")
        query_embedding = self._create_embedding(query)
        if query_embedding is None:
            print("امکان ایجاد امبدینگ برای پرس و جو وجود نداشت. بازیابی لغو شد.")
            return []

        query_embedding_array = np.array([query_embedding]).astype("float32")

        # جستجو
        distances, indices = self.index.search(
            query_embedding_array, min(k, self.index.ntotal)
        )  # k نباید از تعداد کل آیتم‌ها بیشتر باشد

        # بازگرداندن نتایج
        results = []
        for i, idx in enumerate(indices[0]):
            # بررسی اینکه آیا شاخص در محدوده معتبر است
            if 0 <= idx < len(self.texts):
                results.append(
                    {
                        "text": self.texts[idx],
                        "metadata": self.metadata[idx],
                        "distance": float(distances[0][i]),
                    }
                )
            else:
                print(f"هشدار: شاخص نامعتبر {idx} در نتایج جستجو یافت شد.")

        print(f"{len(results)} سند بازیابی شد.")
        return results

    def _generate_response(self, query, retrieved_documents):
        """پاسخی را بر اساس اسناد بازیابی شده تولید می‌کند."""
        # فرمت‌بندی زمینه بازیابی شده با استنادات
        formatted_context = ""
        if retrieved_documents:
            for i, doc in enumerate(retrieved_documents):
                doc_text = doc.get("text", "محتوای سند در دسترس نیست")
                metadata = doc.get("metadata", {})
                source_info = f"[{i+1}] منبع: {metadata.get('source', 'نامشخص')}"
                if "page" in metadata:
                    source_info += f"، صفحه: {metadata['page']}"
                formatted_context += f"{doc_text}\n{source_info}\n\n"
        else:
            formatted_context = "هیچ زمینه‌ای ارائه نشده است.\n"

        # ایجاد پرامپت سیستم
        system_prompt = f"""شما یک دستیار مفید هستید که به سوالات بر اساس زمینه ارائه شده پاسخ می‌دهید.
		این قوانین را دنبال کنید:
		1. فقط بر اساس زمینه ارائه شده پاسخ دهید.
		2. اگر زمینه حاوی پاسخ نیست یا برای پاسخ کافی نیست، بگویید "اطلاعات کافی برای پاسخ به این سوال در زمینه ارائه شده ندارم".
		3. هنگام ارجاع به اطلاعات از زمینه، استنادات [1]، [2] و غیره را لحاظ کنید.
		4. مختصر و مفید باشید.
		5. پاسخ خود را به روشی واضح و خوانا فرمت‌بندی کنید."""

        # ایجاد پرامپت کاربر
        user_prompt = f"""زمینه:
		{formatted_context}
		سوال: {query}"""

        print("درحال تولید پاسخ با استفاده از LLM...")
        # تولید پاسخ
        response = client.chat.completions.create(
            model=self.model,
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
        )
        return response.choices[0].message.content

    def query(self, query_text: str, k: int = 3):
        """
        یک پرس و جو را از طریق جریان کاری RAG پردازش می‌کند.

        Args:
                query_text: سوال کاربر
                k: تعداد اسناد برای بازیابی

        Returns:
                (پاسخ تولید شده, لیست اسناد بازیابی شده)
        """
        print(f"\nدرحال پردازش پرس و جو: '{query_text}'")
        # بررسی اینکه آیا پرس و جو نیاز به بازیابی دارد
        retrieved_documents = []
        if self._query_needs_retrieval(query_text):
            if self.index is not None and self.index.ntotal > 0:
                retrieved_documents = self._retrieve(query_text, k)
            else:
                print("شاخص خالی است یا راه‌اندازی نشده است، بازیابی انجام نمی‌شود.")
        else:
            print("پرس و جو نیازی به بازیابی ندارد. پاسخ مستقیم از LLM.")

        # تولید پاسخ با یا بدون زمینه
        final_response = self._generate_response(query_text, retrieved_documents)
        return final_response, retrieved_documents

    def save(self, filepath: str):
        """جریان کاری RAG را روی دیسک ذخیره می‌کند."""
        if self.index is None:
            print("شاخص راه‌اندازی نشده است. چیزی برای ذخیره وجود ندارد.")
            return

        # ذخیره شاخص FAISS
        faiss_path = filepath + ".faiss"
        with open(faiss_path, "wb") as f:
            faiss.write_index(
                self.index, f.fileno()
            )  # استفاده از fileno() برای سازگاری
        print(f"شاخص FAISS در '{faiss_path}' ذخیره شد.")

        # ذخیره سایر داده‌ها
        data_path = filepath + ".pkl"
        with open(data_path, "wb") as f:
            pickle.dump(
                {
                    "texts": self.texts,
                    "metadata": self.metadata,
                    "model": self.model,
                    "embedding_model": self.embedding_model,
                },
                f,
            )
        print(f"داده‌های جریان کاری در '{data_path}' ذخیره شدند.")

    @classmethod
    def load(cls, filepath: str):
        """جریان کاری RAG را از دیسک بارگذاری می‌کند."""
        # بارگذاری داده‌های pickled
        data_path = filepath + ".pkl"
        try:
            with open(data_path, "rb") as f:
                data = pickle.load(f)
        except FileNotFoundError:
            print(f"خطا: فایل داده '{data_path}' یافت نشد.")
            return None
        except Exception as e:
            print(f"خطا در بارگذاری فایل داده '{data_path}': {e}")
            return None

        rag = cls(model=data["model"], embedding_model=data["embedding_model"])
        rag.texts = data["texts"]
        rag.metadata = data["metadata"]
        print(f"داده‌های جریان کاری از '{data_path}' بارگذاری شدند.")

        # بارگذاری شاخص FAISS
        faiss_path = filepath + ".faiss"
        try:
            with open(faiss_path, "rb") as f:
                rag.index = faiss.read_index(f.fileno())  # استفاده از fileno()
            print(
                f"شاخص FAISS از '{faiss_path}' بارگذاری شد. تعداد کل بردارها: {rag.index.ntotal if rag.index else 'N/A'}"
            )
        except FileNotFoundError:
            print(
                f"هشدار: فایل شاخص FAISS '{faiss_path}' یافت نشد. شاخص خالی خواهد بود."
            )
            # اگر فایل شاخص وجود نداشته باشد، ممکن است بخواهید یک شاخص خالی ایجاد کنید
            # یا اجازه دهید None باقی بماند تا در add_documents ایجاد شود.
            # rag.index = None
        except Exception as e:
            print(f"خطا در بارگذاری شاخص FAISS '{faiss_path}': {e}")
            return None  # یا مدیریت خطای مناسب‌تر

        return rag


# مثال استفاده
# ایجاد و راه‌اندازی جریان کاری RAG
rag_pipeline = RAGPipeline()

# اضافه کردن اسناد
documents_to_add = [
    {
        "text": "AvalAI دسترسی به طیف گسترده‌ای از مدل‌های زبانی را از طریق یک API یکپارچه فراهم می‌کند.",
        "metadata": {"source": "docs-fa", "page": 1, "lang": "fa"},
    },
    {
        "text": "این پلتفرم از مدل‌های ارائه‌دهندگان مختلف از جمله OpenAI، Anthropic، Google و موارد دیگر پشتیبانی می‌کند.",
        "metadata": {"source": "docs-fa", "page": 1, "lang": "fa"},
    },
    {
        "text": "هر مدل دارای قابلیت‌ها و قیمت‌گذاری متفاوتی است، بنابراین درک مبادلات مهم است.",
        "metadata": {"source": "docs-fa", "page": 2, "lang": "fa"},
    },
    {
        "text": "AvalAI ابزارهایی برای نظارت بر استفاده و مدیریت موثر هزینه‌ها ارائه می‌دهد.",
        "metadata": {"source": "docs-fa", "page": 3, "lang": "fa"},
    },
    {
        "text": "مستندات شامل مثال‌ها و بهترین شیوه‌ها برای موارد استفاده مختلف است.",
        "metadata": {"source": "docs-fa", "page": 4, "lang": "fa"},
    },
]

rag_pipeline.add_documents(documents_to_add)

# ذخیره جریان کاری RAG
pipeline_filepath = "my_full_rag_pipeline"
rag_pipeline.save(pipeline_filepath)

# بارگذاری جریان کاری RAG (برای آزمایش)
# rag_pipeline_loaded = RAGPipeline.load(pipeline_filepath)
# if rag_pipeline_loaded is None:
# 	print("بارگذاری جریان کاری با شکست مواجه شد.")
# 	exit()


# پرس و جو از جریان کاری RAG
query1 = "AvalAI از کدام مدل‌های هوش مصنوعی پشتیبانی می‌کند؟"
response1, retrieved_docs1 = rag_pipeline.query(
    query1, k=2
)  # یا rag_pipeline_loaded.query(query1, k=2)

print(f"\n--- نتایج برای پرس و جوی ۱ ---")
print(f"پرس و جو: {query1}")
print(f"\nاسناد بازیابی شده:")
if retrieved_docs1:
    for i, doc in enumerate(retrieved_docs1):
        print(
            f"{i+1}. {doc.get('text', 'متن نامشخص')} (فاصله: {doc.get('distance', -1):.4f})"
        )
else:
    print("هیچ سندی بازیابی نشد.")
print(f"\nپاسخ نهایی:\n{response1}")


# پرس و جوی دیگر که ممکن است نیازی به بازیابی نداشته باشد یا زمینه کافی نداشته باشد
query2 = "پایتخت فرانسه کجاست؟"
response2, retrieved_docs2 = rag_pipeline.query(query2, k=2)

print(f"\n--- نتایج برای پرس و جوی ۲ ---")
print(f"پرس و جو: {query2}")
print(f"\nاسناد بازیابی شده:")
if retrieved_docs2:
    for i, doc in enumerate(retrieved_docs2):
        print(
            f"{i+1}. {doc.get('text', 'متن نامشخص')} (فاصله: {doc.get('distance', -1):.4f})"
        )
else:
    print("هیچ سندی بازیابی نشد.")
print(f"\nپاسخ نهایی:\n{response2}")
نسخه معادل Responses API

وقتی مدل انتخابی از /v1/responses پشتیبانی می‌کند، این نسخه را کنار مثال Chat Completions استفاده کنید. messages به input منتقل می‌شود و متن نهایی از response.output_text خوانده می‌شود.

python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["AVALAI_API_KEY"],
    base_url="https://api.avalai.ir/v1",
)

response = client.responses.create(
    model="gpt-5.5",
    input=[
        {
            "role": "user",
            "content": [
                {"type": "input_text", "text": "Summarize the uploaded file."},
                {"type": "input_file", "file_id": "file_abc123"},
            ],
        }
    ],
)

print(response.output_text)
  • messagesinput
  • پیام سیستمی → instructions یا آیتم developer
  • choices[0].message.contentresponse.output_text
  • برای ابزارها و خروجی‌های چندوجهی، response.output را بر اساس type بررسی کنید.

نتیجه‌گیری

پیاده‌سازی یک جریان کاری RAG موثر نیازمند توجه دقیق به هر جز است. با دنبال کردن این بهترین شیوه‌ها، می‌توانید سیستمی بسازید که پاسخ‌های دقیق، مرتبط و واقعی به پرس و جوهای کاربران ارائه دهد.

به یاد داشته باشید که RAG یک راه‌حل یکسان برای همه نیست. با پیکربندی‌ها و رویکردهای مختلف آزمایش کنید تا بهترین گزینه را برای مورد استفاده خاص خود بیابید. ارزیابی و اصلاح مداوم برای حفظ و بهبود عملکرد در طول زمان کلیدی است.

برای اطلاعات بیشتر و موضوعات پیشرفته، به مستندات AvalAI در مورد اجزای خاص مانند امبدینگ‌ها، رتبه‌بندی مجدد و تکمیل چت مراجعه کنید.