تخيل أن لديك نظامًا لإدارة المستندات والفواتير في شركة. بعد عدة أشهر، أصبح لديك عشرات الآلاف من الملفات: صور فواتير، تسجيلات صوتية لمكالمات العملاء، ملفات PDF، صور إيصالات، مستندات ممسوحة ضوئيًا، ونصوص ورسائل داخل النظام.
ثم يأتي المستخدم ويكتب:
«أريد الفواتير التي يظهر فيها مبلغ دفع كبير.»
المشكلة أن هذه الجملة قد لا تظهر حرفيًا في أي مكان. قد تكون الصورة باسم IMG_84921.jpg، ولا يوجد عليها tag باسم invoice، وربما لا تحتوي قاعدة البيانات أصلًا على نص مستخرج منها. البحث التقليدي لن يجد شيئًا.
لكن ماذا لو استطعنا تحويل النص والصورة والصوت إلى تمثيل رياضي للمعنى نفسه، ثم البحث بينها باستخدام التشابه الدلالي؟ هنا تظهر فكرة Multimodal Embeddings.
والأهم أن Laravel AI SDK الحديث يوفر API موحدًا لإنشاء embeddings من النص والصور والصوت، مع دعم multimodal inputs لدى مزودين محددين مثل Gemini، بالإضافة إلى دعم التخزين والبحث باستخدام vector columns في PostgreSQL.
1. المشكلة: البحث التقليدي لا يفهم المحتوى
لنأخذ تطبيقًا بسيطًا لإدارة مستندات شركة:
documents
├── invoice_001.jpg
├── receipt_002.png
├── customer-call.mp3
├── contract.pdf
└── invoice_981.jpgلو استخدمنا البحث التقليدي:
SELECT *
FROM documents
WHERE filename LIKE '%invoice%';فإننا نبحث عن الكلمات الموجودة في metadata، وليس عن محتوى الملف. وهذا يؤدي إلى مشكلة أساسية:
Filename / Tags / Keywords
↓
Keyword Search
↓
Exact Matchبينما المستخدم يفكر بطريقة مختلفة:
"فاتورة فيها مبلغ كبير"
↓
Meaning
↓
Semantic Search
↓
Relevant Documentsالفرق هنا جوهري. المستخدم لا يهتم باسم الملف، هو يهتم بالمعنى الموجود داخل الملف.
2. ما هو Multimodal Search؟
في البحث التقليدي لدينا عادة:
Text
↓
Embedding
↓
Vector
↓
Similarity Searchأما في Multimodal Search فنستطيع التعامل مع أنواع متعددة من البيانات:
┌─────────────┐
│ Text │
└──────┬──────┘
│
┌──────▼──────┐
│ │
Image ───────►│ Embedding │◄────── Audio
│ │
Video ───────►│ │◄────── Documents
└──────┬──────┘
│
▼
Vector Store
│
▼
Semantic Searchالفكرة أن الـ embedding يمثل المحتوى كمتجه رقمي يمكن مقارنة قربه من متجه آخر. فبدل أن نقول:
هل الملف يحتوي على كلمة "فاتورة"؟
نصبح قادرين على السؤال:
ما الملفات الأكثر تشابهًا مع مفهوم "فاتورة فيها مبلغ دفع كبير"؟
وهذا هو الفرق بين lexical search وsemantic search.
Laravel AI SDK يدعم حاليًا إنشاء embeddings من strings، كما أن Embeddings::for() يقبل inputs متعددة الوسائط مثل الصور والصوت والمستندات والفيديو، مع اختلاف أنواع الوسائط المدعومة حسب الـ provider/model. توضح وثائق Laravel الحالية أن Gemini يدعم embeddings للصور والصوت والمستندات والفيديو، بينما VoyageAI يدعم الصور والفيديو في هذا السياق.
3. Architecture التي سنبنيها
سنفترض أننا نبني نظامًا Production لإدارة المستندات. الـ architecture ستكون تقريبًا:
User
│
▼
Search Endpoint
│
▼
Search Service
│
▼
Query Embedding
│
▼
PostgreSQL
+ pgvector
│
Similarity Search
│
▼
Authorization Filter
│
▼
Top K Results
│
▼
API Responseأما عند رفع ملف جديد:
User Upload
│
▼
Laravel Controller
│
├── Validate
│
├── Store File
│
└── Create Media Record
│
▼
Queue Job
│
▼
Generate Embedding
│
▼
Store Vector
│
▼
Mark as Indexedوهذه نقطة مهمة جدًا: لا ننشئ embedding داخل HTTP request إذا كان الملف كبيرًا أو عدد الملفات كبيرًا. سنستخدم Queue.
4. Laravel AI SDK
الـ Laravel AI SDK هو package رسمي من Laravel يوفر API موحدًا للتعامل مع مزودي الذكاء الاصطناعي، ويغطي embeddings وtranscription وimages وagents وvector stores وغيرها. في وثائق Laravel الحالية يتم تثبيته باستخدام:
composer require laravel/aiثم نشر إعداداته وتشغيل migrations:
php artisan vendor:publish --provider="Laravel\Ai\AiServiceProvider"
php artisan migrateيمكنك أيضًا تعريف مفاتيح المزود في .env، ومنها GEMINI_API_KEY وOPENAI_API_KEY وغيرها بحسب provider الذي ستستخدمه.
5. لماذا PostgreSQL + pgvector؟
لدينا الآن مشكلة أخرى: أين نخزن الـ vectors؟ هناك حلول كثيرة مثل Pinecone وWeaviate وQdrant وMilvus وElasticsearch وPostgreSQL + pgvector.
لكن إذا كان تطبيق Laravel يعتمد أصلًا على PostgreSQL، فإن pgvector خيار ممتاز للعديد من التطبيقات لأنه يسمح بتخزين vectors بجانب البيانات العادية والاستفادة من SQL وtransactions وindexes وJOINs.
Laravel يوفر دعمًا أصليًا لـ vector columns في PostgreSQL عند استخدام pgvector. وpgvector نفسه يدعم exact وapproximate nearest-neighbor search، إضافة إلى مسافات مثل cosine distance وL2 وinner product، ويدعم HNSW وIVFFlat indexes.
6. Database Design
لنبدأ بجدول بسيط للملفات:
media
-------------------------
id
user_id
type
disk
path
original_name
mime_type
status
created_at
updated_atلكننا نحتاج أيضًا إلى جدول منفصل للـ embeddings، لأن الملف قد يكون له أكثر من representation. مثلاً:
invoice.jpg
│
├── image embedding
│
├── extracted text
│
└── metadataلذلك يمكن أن يكون التصميم:
users
│
└── media
│
└── embeddings7. Migration
سنستخدم PostgreSQL وvector column:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::ensureVectorExtensionExists();
Schema::create('media', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->cascadeOnDelete();
$table->string('type');
$table->string('disk');
$table->string('path');
$table->string('original_name');
$table->string('mime_type');
$table->string('status')
->default('pending');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('media');
}
};ثم جدول embeddings:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('media_embeddings', function (Blueprint $table) {
$table->id();
$table->foreignId('media_id')
->constrained()
->cascadeOnDelete();
$table->string('modality');
$table->string('provider');
$table->string('model');
$table->unsignedInteger('dimensions');
$table->vector(
'embedding',
dimensions: 1536
);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('media_embeddings');
}
};لاحظ أن dimensions هنا ليست رقمًا يمكن اختياره عشوائيًا. يجب أن تتطابق مع أبعاد الـ embedding الذي ينتجه الـ model المستخدم. Laravel نفسها توضح أن vector column يجب أن يحدد عدد dimensions المطابق لمخرجات مزود الـ embedding.
8. لماذا نحتاج provider و model؟
هذه من التفاصيل التي كثيرًا ما يتم تجاهلها في المشاريع الحقيقية. افترض أنك بدأت باستخدام:
Provider: Gemini
Model: Model A
Dimensions: 1536ثم بعد عدة أشهر قررت الانتقال إلى:
Provider: Another Provider
Model: Model B
Dimensions: 1024لا يجب أن تفترض أن vectors القديمة والجديدة قابلة للمقارنة. الـ embedding space مرتبط بالنموذج. لذلك من الأفضل تسجيل provider وmodel وdimensions مع كل embedding.
وفي الأنظمة الكبيرة يمكن إضافة embedding_version مثل v1 وv2 وv3. وهذا يجعل إعادة الفهرسة migration أسهل بكثير.
9. Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MediaEmbedding extends Model
{
protected $fillable = [
'media_id',
'modality',
'provider',
'model',
'dimensions',
'embedding',
];
public function media(): BelongsTo
{
return $this->belongsTo(Media::class);
}
}والـ Media:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Media extends Model
{
protected $fillable = [
'user_id',
'type',
'disk',
'path',
'original_name',
'mime_type',
'status',
];
public function embeddings(): HasMany
{
return $this->hasMany(MediaEmbedding::class);
}
}10. رفع صورة
Controller بسيط:
public function store(Request $request)
{
$validated = $request->validate([
'file' => [
'required',
'file',
'mimes:jpg,jpeg,png,webp,mp3,wav,m4a',
'max:51200',
],
]);
$file = $validated['file'];
$path = $file->store('media');
$media = Media::create([
'user_id' => $request->user()->id,
'type' => str_starts_with($file->getMimeType(), 'image/')
? 'image'
: 'audio',
'disk' => config('filesystems.default'),
'path' => $path,
'original_name' => $file->getClientOriginalName(),
'mime_type' => $file->getMimeType(),
'status' => 'pending',
]);
GenerateMediaEmbedding::dispatch($media);
return response()->json([
'id' => $media->id,
'status' => $media->status,
], 202);
}لاحظ أننا لا ننتظر الـ AI API. نعيد 202 Accepted ثم يعمل الـ Job في الخلفية.
11. لماذا Queue؟
توليد embedding يعني عادة:
Laravel
↓
External AI API
↓
Model Processing
↓
Vector
↓
Laravelوهذا يعني أن العملية قد تكون أبطأ من request عادي. إذا رفع المستخدم 100 صورة، لا نريد معالجتها كلها داخل نفس الطلب لأن ذلك سيؤدي إلى:
- Timeout.
- تجربة مستخدم سيئة.
- ضغط على PHP workers.
- مشاكل عند rate limits.
- صعوبة في retry.
الأفضل:
Upload → Save → Dispatch Job → 202ثم:
Queue Worker → Generate Embedding → Save VectorLaravel AI SDK يدعم queueing لعدد من عمليات AI، كما أن SDK يوفر events مرتبطة بتوليد embeddings يمكن استخدامها للمراقبة والتسجيل.
12. Job لتوليد Image Embedding
الـ API الحالي في Laravel AI SDK يسمح بتمرير Files\Image إلى Embeddings::for():
namespace App\Jobs;
use App\Models\Media;
use App\Models\MediaEmbedding;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Image;
class GenerateMediaEmbedding implements ShouldQueue
{
use Queueable;
public function __construct(
public Media $media
) {}
public function handle(): void
{
$input = match ($this->media->type) {
'image' => Image::fromStorage(
$this->media->path
),
default => throw new \RuntimeException(
'Unsupported media type'
),
};
$response = Embeddings::for([
$input,
])->generate(Lab::Gemini);
$embedding = $response->embeddings[0];
MediaEmbedding::updateOrCreate(
[
'media_id' => $this->media->id,
'provider' => 'gemini',
'model' => config(
'ai.providers.gemini.models.embeddings.default'
),
],
[
'modality' => $this->media->type,
'dimensions' => count($embedding),
'embedding' => $embedding,
]
);
$this->media->update([
'status' => 'indexed',
]);
}
}الفكرة الأساسية هنا هي:
Image → Files\Image → Embeddings::for() → Embedding Vector → PostgreSQLLaravel توضح رسميًا أن Image::fromStorage() وImage::fromPath() وImage::fromUpload() يمكن استخدامها مع multimodal embeddings، وكذلك توجد طرق مقابلة للصوت مثل Audio::fromStorage() وAudio::fromUpload().
13. ماذا عن Audio؟
الصوت يفتح بابًا مهمًا جدًا. لنفترض أن لدينا تسجيلات لمكالمات العملاء:
call_001.mp3
call_002.mp3
call_003.mp3يمكننا التعامل مع الصوت كـ multimodal input:
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use Laravel\Ai\Files\Audio;
$response = Embeddings::for([
Audio::fromStorage('calls/call_001.mp3'),
])->generate(Lab::Gemini);وهكذا يصبح لدينا:
Audio → Multimodal Embedding → Vectorوهنا يمكن بناء تجربة مثل:
"اعثر على المكالمات التي تتحدث عن تأخر الشحن."
بدل البحث فقط عن transcript حرفي. لكن هناك قرار معماري مهم: لا تجعل multimodal embedding بديلًا عن transcription دائمًا.
في تطبيقات Production قد يكون الأفضل الاحتفاظ بالاثنين:
Audio
│
├───────────────► Audio Embedding
│
└───────────────► Transcription
│
▼
Text Embeddingلأن transcript مفيد جدًا في عرض النص للمستخدم، والبحث بالكلمات، واستخراج timestamps، وspeaker diarization، وauditing، وanalytics.
وLaravel AI SDK يدعم transcription ويمكن تشغيله أيضًا عبر Queue، كما يدعم diarization عندما تحتاج إلى تقسيم النص حسب المتحدث.
14. Search Query
الآن وصلنا إلى الجزء المهم. المستخدم يكتب:
صورة فاتورة فيها دفعة بقيمة كبيرة
نحتاج أولًا إلى تحويل query إلى embedding:
$response = Embeddings::for([
$request->string('q')->toString(),
])->generate(Lab::Gemini);
$queryVector = $response->embeddings[0];ثم نستخدم vector similarity. الفكرة رياضيًا:
Query Vector
│
▼
[0.21, -0.08, 0.71, ...]
│
▼
Compare
│
├── Image Vector A
├── Image Vector B
├── Audio Vector C
└── Image Vector Dثم نرتب النتائج حسب similarity.
15. Cosine Similarity
واحدة من أكثر الطرق شيوعًا لمقارنة embeddings هي cosine distance/similarity. في pgvector، الرمز <=> يمثل cosine distance، والـ cosine similarity يمكن حسابها كـ 1 - cosine_distance. وهذا موثق في pgvector رسميًا.
مثال SQL:
SELECT
media_id,
1 - (embedding <=> '[QUERY_VECTOR]') AS similarity
FROM media_embeddings
ORDER BY embedding <=> '[QUERY_VECTOR]'
LIMIT 20;16. HNSW
إذا كان لديك 1,000 vector فالبحث المباشر قد يكون مقبولًا. لكن ماذا لو لديك 10 مليون vector؟ هنا تحتاج إلى Approximate Nearest Neighbor indexing. أحد الخيارات المهمة هو HNSW — Hierarchical Navigable Small World، ويدعمه pgvector رسميًا:
CREATE INDEX media_embeddings_embedding_hnsw
ON media_embeddings
USING hnsw (embedding vector_cosine_ops);HNSW عادة يعطي trade-off جيدًا بين سرعة البحث وrecall، لكنه يحتاج ذاكرة أكثر ووقت بناء أكبر من IVFFlat. كما يمكن إنشاؤه دون مرحلة training مسبقة مثل IVFFlat.
17. Search Service
بدل وضع كل شيء داخل Controller، سننشئ Service:
namespace App\Services;
use Laravel\Ai\Embeddings;
use Laravel\Ai\Enums\Lab;
use App\Models\MediaEmbedding;
class SemanticSearchService
{
public function search(
string $query,
int $userId,
int $limit = 20
) {
$response = Embeddings::for([
$query,
])->generate(Lab::Gemini);
$vector = $response->embeddings[0];
return MediaEmbedding::query()
->select('media_embeddings.*')
->selectRaw(
'1 - (embedding <=> ?) AS similarity',
[$vector]
)
->whereHas('media', function ($query) use ($userId) {
$query->where('user_id', $userId);
})
->orderByRaw(
'embedding <=> ?',
[$vector]
)
->limit($limit)
->with('media')
->get();
}
}بهذا يصبح Controller نظيفًا:
public function search(
Request $request,
SemanticSearchService $search
) {
$validated = $request->validate([
'q' => ['required', 'string', 'max:500'],
'limit' => ['nullable', 'integer', 'min:1', 'max:50'],
]);
return $search->search(
$validated['q'],
$request->user()->id,
$validated['limit'] ?? 20
);
}18. Authorization ليست اختيارية
هناك خطأ خطير جدًا في أنظمة semantic search: تنفيذ البحث في vector DB، إحضار أفضل 20 نتيجة، ثم تطبيق authorization بعد ذلك. هذا قد يؤدي إلى تسريب معلومات.
تخيل أن User A يبحث عن "عقد العميل X"، والـ nearest vector هو ملف يخص User B. إذا استخرجناه أولًا ثم طبقنا authorization لاحقًا، فقد تكون عملية ranking نفسها قد أعطت المستخدم وصولًا إلى metadata لا يجب أن يراها.
الأفضل أن يكون access scope جزءًا من query نفسها:
Query → Embedding → Vector Search + Authorization Filter → Allowed Resultsمثل:
->whereHas('media', function ($query) use ($userId) {
$query->where('user_id', $userId);
})وفي نظام متعدد المستأجرين:
->where('tenant_id', $tenantId)والأفضل أن يكون tenant_id جزءًا من التصميم منذ البداية.
19. Multitenancy
في SaaS application، يجب ألا يكون vector search عالميًا بدون filter:
SELECT ...
FROM media_embeddings
WHERE tenant_id = ?
ORDER BY embedding <=> ?
LIMIT 20;وهذا ليس مجرد optimization، إنه جزء من security boundary.
20. Hybrid Search
هناك نقطة مهمة جدًا: لا تستخدم semantic search لكل شيء. مثلاً INV-2026-00481 هذا identifier، والبحث الدلالي ليس بالضرورة أفضل طريقة له. قد يكون الجمع بين Exact Search وSemantic Search أفضل، وهذا يسمى Hybrid Search:
User Query
│
├──────────────► Full Text Search
│
└──────────────► Vector Search
│
▼
Candidate Set
│
▼
Merge
│
▼
Rankوpgvector يمكن استخدامه مع PostgreSQL full-text search لبناء hybrid search، ويمكن دمج النتائج باستخدام تقنيات مثل Reciprocal Rank Fusion أو cross-encoder.
21. مثال Production أكثر واقعية
لنفرض أن لدينا نظام إدارة فواتير. كل Invoice تحتوي على invoice.jpg ومعلومات structured مثل invoice_number وcustomer_id وamount وcurrency وcreated_at وstatus. والـ embedding يمثل محتوى الصورة.
المستخدم يكتب:
"أظهر لي الفواتير التي تبدو أنها تحتوي على دفعات كبيرة."
يمكننا بناء search pipeline:
User Query
│
▼
Query Embedding
│
▼
Vector Search
│
▼
Top 100 Candidates
│
├── tenant filter
├── permission filter
├── date filter
└── status filter
│
▼
Top 20
│
▼
Optional Reranking
│
▼
Final Resultsوهنا نبدأ بفصل مفهومين:
Retrieval
العثور على candidates.
Ranking
ترتيب candidates. وهذا مهم جدًا عندما يصبح النظام كبيرًا.
22. لماذا لا نأخذ Top 5 مباشرة؟
خطأ شائع هو تنفيذ vector search ثم أخذ LIMIT 5 مباشرة — قد تكون النتائج الخمسة الأولى غير مثالية. الأفضل في بعض الأنظمة:
Vector Search → Top 100 → Metadata Filtering → Reranking → Top 10Laravel AI SDK يدعم أيضًا reranking ضمن قدراته، مع providers مخصصة لهذه الوظيفة.
23. Error Handling
ماذا يحدث إذا فشل AI provider؟ مثلاً:
Upload → Queue → Embedding API → 429 Too Many Requestsلا نريد أن تبقى الحالة failed إلى الأبد. الأفضل استخدام retries وbackoff:
class GenerateMediaEmbedding implements ShouldQueue
{
public int $tries = 5;
public function backoff(): array
{
return [
10,
30,
60,
120,
];
}
// ...
}ثم عند الفشل:
pending → processing → failed → retry → indexedويفضل تخزين error_message وattempts وlast_attempt_at في جدول منفصل أو metadata مناسبة.
24. Idempotency
ماذا لو اشتغل الـ Job مرتين؟ يجب ألا ينتج عدة embeddings مكررة لنفس الملف. بل يجب أن يكون لدينا unique constraint مناسب:
$table->unique([
'media_id',
'provider',
'model',
]);وبالتالي updateOrCreate(...) يصبح آمنًا أكثر.
25. Embedding Caching
توليد نفس embedding مرارًا يمكن أن يهدر التكلفة. Laravel AI SDK يدعم caching للـ embeddings، ويمكن تفعيله من إعدادات SDK، كما أن cache key يعتمد على provider/model/dimensions/input، بحيث يمكن إعادة استخدام النتائج المتطابقة بدل إرسال نفس الطلب مرة أخرى:
'caching' => [
'embeddings' => [
'cache' => true,
'store' => env('CACHE_STORE', 'database'),
],
],لكن يجب الانتباه إلى أن caching مناسب أكثر للمدخلات التي تتكرر. أما ملفات المستخدمين، فمن الأفضل أن يكون لديك أيضًا lifecycle واضح للـ embedding المرتبط بالـ media نفسه.
26. لا تخزن الـ embedding فقط
في Production، لا تجعل vector هو المصدر الوحيد للمعلومات. احتفظ بالملف الأصلي والـ metadata والـ status بجانب الـ embeddings، وفي كل embedding سجّل provider وmodel وdimensions وversion وcreated_at.
لأنك قد تحتاج لاحقًا إلى إعادة الفهرسة (Re-index) عند الانتقال من Model V1 إلى Model V2، وعندها تستطيع تشغيل Re-embedding Job على الملفات القديمة.
27. Security عند رفع الملفات
Multimodal systems تتعامل مع ملفات يرفعها المستخدم، وهذا يعني أن validation يجب أن تكون صارمة. لا تعتمد فقط على $file->getClientOriginalExtension() لأن extension قيمة يرسلها العميل. استخدم Laravel validation:
$request->validate([
'file' => [
'required',
'file',
'mimes:jpg,jpeg,png,webp,mp3,wav,m4a',
'max:51200',
],
]);وفي الأنظمة الحساسة أضف:
- MIME verification.
- Antivirus scanning.
- Image decoding validation.
- Maximum dimensions.
- Maximum audio duration.
- Storage isolation.
- Private disks.
- Signed URLs عند الحاجة.
ولا تجعل الملفات الحساسة public لمجرد أن AI provider يحتاج إلى الوصول إليها.
28. ماذا عن Privacy؟
هذه النقطة مهمة جدًا في Production. إذا كان لديك مستندات طبية أو مالية أو مكالمات عملاء أو عقود، فأنت لا ترسل الملفات إلى AI provider دون معرفة:
- أين تتم المعالجة؟
- ما سياسة الاحتفاظ بالبيانات؟
- هل يتم استخدام البيانات للتدريب؟
- ما region المستخدمة؟
- ما متطلبات compliance؟
- هل يوجد data residency requirement؟
Laravel AI SDK يسهل التعامل مع أكثر من provider، لكن اختيار provider وسياسة البيانات مسؤولية التطبيق نفسه. Laravel توضح أن SDK يعمل مع عدة providers ويمكن أيضًا توجيه الطلبات عبر custom base URLs أو gateways، وهو مفيد عندما تحتاج إلى طبقة تحكم مركزية على حركة AI.
29. Performance
أولًا: Batch Embeddings
إذا كان لديك 1000 مستند، لا ترسل 1000 طلب API منفصل. إذا كان provider يسمح بتجميع المدخلات، استخدم:
Embeddings::for([
$input1,
$input2,
$input3,
])->generate();Laravel توضح أن Embeddings::for() يمكنها معالجة عدة inputs في طلب واحد، وهو أكثر كفاءة من إنشاء embedding لكل input بشكل منفصل.
ثانيًا: Queue Workers
بدل معالجة الطلب مباشرة عبر AI، مرّره عبر Queue وWorkers، ويمكن زيادة workers حسب الحمل.
ثالثًا: HNSW
عندما يصبح عدد vectors كبيرًا، استخدم approximate nearest-neighbor indexing مثل HNSW بدل مقارنة كل vector مع كل vector. لكن لا تفترض أن HNSW دائمًا أفضل، فهو trade-off بين Speed وRecall وMemory. وتوضح وثائق pgvector أن HNSW يوفر أداء بحث أفضل عادة ضمن trade-off السرعة/الاسترجاع مقارنة بـ IVFFlat، مقابل وقت بناء وذاكرة أكبر.
30. Cost
التكلفة ليست فقط سعر الـ embedding الواحد، بل مجموع Upload وStorage وEmbedding وRetries وRe-indexing وSearch Queries وReranking وTranscription. لذلك من الأفضل أن نضع metrics:
| المقياس | الغرض |
|---|---|
embeddings_generated | عدد الـ embeddings المولّدة |
embedding_failures | عدد حالات الفشل |
embedding_latency | زمن الاستجابة |
search_latency | زمن البحث |
cache_hit_rate | نسبة إعادة الاستخدام من الكاش |
وLaravel AI SDK يوفر events مثل GeneratingEmbeddings وEmbeddingsGenerated، والتي يمكن استخدامها لتسجيل ومراقبة استخدام AI.
31. ماذا لو غيّرنا AI Provider؟
هذه من أهم مزايا استخدام Laravel AI SDK. بدل ربط المشروع مباشرة بعشرات APIs منفصلة، يمكن أن تكون لدينا طبقة abstraction موحدة:
Laravel Application
│
▼
Laravel AI SDK
│
┌─────┼─────┐
▼ ▼ ▼
OpenAI Gemini Otherوالـ SDK يوفر abstraction موحدًا لعدد من providers، كما يدعم حاليًا OpenAI-compatible providers أيضًا، مما يسمح بتوجيه embeddings أو عمليات أخرى إلى endpoints متوافقة مع OpenAI API.
32. لا تجعل Controller يعرف تفاصيل AI
من الأخطاء المعمارية أن يحتوي Controller على validation وgeneration وquery وauthorization وreranking وformatting وlogging كلها في مكان واحد. الأفضل فصل المسؤوليات:
Controller
↓
SearchService
↓
EmbeddingService
↓
VectorRepository
↓
Rerankerمثلاً:
app/
├── Ai/
│ ├── Embeddings/
│ └── Services/
│
├── Jobs/
│ └── GenerateMediaEmbedding.php
│
├── Models/
│ ├── Media.php
│ └── MediaEmbedding.php
│
└── Services/
└── SemanticSearchService.phpوهذا يجعل testing وprovider replacement أسهل.
33. Testing
AI calls لا يجب أن تكون جزءًا من كل test حقيقي؛ أنت لا تريد أن يرسل php artisan test مئات الطلبات إلى AI provider. Laravel AI SDK يوفر fake support للـ embeddings وغيرها من قدرات SDK، مما يسمح باختبار التطبيق دون الاعتماد على provider حقيقي:
use Laravel\Ai\Embeddings;
Embeddings::fake();ثم تختبر مسار Upload → Job → Embedding → Database دون اتصال خارجي.
34. Workflow كامل
لنلخص دورة حياة الصورة:
┌──────────────┐
│ User Upload │
└──────┬───────┘
│
▼
┌─────────────────┐
│ Validate File │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Private Storage │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Create Media │
└────────┬────────┘
│
▼
Dispatch Job
│
▼
┌─────────────────┐
│ AI Embedding │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Store Vector │
└────────┬────────┘
│
▼
┌─────────────────┐
│ status=indexed │
└─────────────────┘وعند البحث:
User Query
│
▼
Validation
│
▼
Query Embedding
│
▼
PostgreSQL + pgvector
│
├── Tenant Filter
├── Authorization
├── Metadata Filter
│
▼
Vector Similarity
│
▼
Top 100
│
▼
Optional Reranking
│
▼
Top 20
│
▼
API Response35. Architecture النهائية
في مشروع Production حقيقي، يمكن أن تكون البنية:
┌──────────────┐
│ Frontend │
└──────┬───────┘
│
▼
┌─────────────────┐
│ Laravel API │
└────────┬────────┘
│
┌───────────────┼────────────────┐
│ │ │
▼ ▼ ▼
Authentication Search Service Upload Service
│ │
│ ▼
│ Object Storage
│ │
│ ▼
│ Queue
│ │
│ ▼
│ Embedding Service
│ │
│ ▼
│ Laravel AI SDK
│ │
│ ▼
│ AI Provider
│
▼
PostgreSQL
+ pgvector
│
▼
Vector Searchوالـ data model:
users
│
└── media
│
├── original file
│
└── media_embeddings
│
├── provider
├── model
├── dimensions
├── modality
└── embedding36. Best Practices
إذا كنت ستبني النظام فعليًا، فهذه أهم القواعد:
1. لا تولّد embeddings داخل request العادي
استخدم Queue للعمليات الثقيلة.
2. لا تضع الـ vector في نفس جدول كل شيء
افصل embedding lifecycle عن media lifecycle عندما يكون المشروع قابلًا للنمو.
3. سجّل provider/model/version
لأنك ستحتاج إلى re-indexing لاحقًا.
4. لا تعتمد على vector search وحده
استخدم Hybrid Search عندما تكون exact identifiers أو keyword matching مهمة.
5. طبّق Authorization داخل retrieval
لا تستخرج نتائج غير مسموحة ثم تحذفها بعد ذلك.
6. استخدم HNSW عند الحاجة فعلًا
ولا تضفه لمجرد أن المشروع يستخدم AI.
7. راقب التكلفة
خصوصًا embedding generation وtranscription وreranking وretries وre-indexing.
8. Cache ما يمكن إعادة استخدامه
Laravel AI SDK يدعم embedding caching.
9. اختبر بدون AI حقيقي
استخدم fakes في automated tests.
10. افصل AI Provider عن Business Logic
لا تجعل domain logic مرتبطًا مباشرة بـ Gemini أو OpenAI.
37. ما الذي لا أنصح به؟
لا أنصح بأي من هذه الأنماط:
Controller → OpenAI/Gemini API → Save JSON
Everything → One giant embeddings table
Every Search → Search every vector manually
User Upload → Generate embedding synchronously → Wait
Vector Search → Return whatever came firstبدلًا من ذلك، فكّر في النظام كـ pipeline:
Ingestion → Processing → Embedding → Indexing → Retrieval → Filtering → Ranking → Authorization → Response38. الفكرة الأكبر
Multimodal Search ليس مجرد "أضف AI إلى search". الفكرة الحقيقية هي تغيير طريقة تخزين وفهم البيانات.
في النظام التقليدي:
File → Filename → Tags → Keywordsفي النظام الدلالي:
File → Semantic Representation → Embedding → Vector Space → Similarityوهذا يسمح لنا بالبحث عن المفهوم بدل البحث عن النص نفسه. صورة فاتورة يمكن أن تكون قريبة دلاليًا من "فاتورة دفع كبيرة"، وتسجيل صوتي يمكن أن يكون قريبًا من "عميل يشتكي من تأخر الطلب"، حتى عندما لا يكون query نفسه موجودًا حرفيًا داخل الملف.
وهنا تصبح قاعدة البيانات ليست مجرد مكان لتخزين الملفات، بل جزءًا من semantic retrieval system.
39. الخلاصة
مع Laravel AI SDK الحديث أصبح بناء Multimodal Search أقرب بكثير إلى Laravel application طبيعي:
Text / Images / Audio / Documents / Videos
│
▼
Multimodal Embeddings
│
▼
PostgreSQL + pgvector
│
▼
HNSW / Vector Search
│
▼
Hybrid Retrieval
│
▼
Reranking
│
▼
Authorization
│
▼
Semantic Resultsالقيمة الحقيقية ليست في إنشاء embedding واحد، بل في بناء pipeline موثوق وقابل للتوسع يبدأ من لحظة رفع الملف وينتهي بنتيجة بحث مفيدة وآمنة.
ومع Laravel، يمكن الحفاظ على architecture مألوفة:
Controllers → Services → Jobs → Laravel AI SDK → PostgreSQL / pgvectorبدل بناء نظام AI منفصل بالكامل عن التطبيق.
والأهم: لا تبدأ باختيار vector database أو model قبل أن تحدد طبيعة البحث الذي تريد حله. ابدأ من السؤال:
ما الذي يريد المستخدم العثور عليه فعلًا؟
ثم صمّم الـ embeddings والـ retrieval والـ ranking حول هذا السؤال.
التعليقات (0)
لا توجد تعليقات بعد — كن أول من يشارك رأيه.
أضف تعليقك