التخزين المؤقت الدلالي (Semantic Cache) لاستدعاءات LLM مع مجموعات المتجهات Redis (Vector Sets) (2026)
٨ يوليو ٢٠٢٦
يخزن التخزين المؤقت الدلالي (Semantic cache) استجابات نماذج اللغة الكبيرة (LLM) بناءً على المعنى، وليس النص المطابق تماماً. باستخدام أوامر مجموعات المتجهات الأصلية VADD و VSIM في Redis 8، يمكن لسؤال تمت إعادة صياغته أن يحقق "hit" في التخزين المؤقت عبر تشابه جيب التمام (cosine similarity)، مما يقلل من استدعاءات LLM المتكررة دون الحاجة لإنشاء قاعدة بيانات متجهات منفصلة.
ملخص
ستقوم ببناء فئة SemanticCache في Node.js و TypeScript تقوم بتخزين تضمينات الاستعلام (query embeddings) في مجموعة متجهات Redis 8 باستخدام VADD، والبحث عن أقرب استعلام مخزن مؤقتاً باستخدام VSIM عند كل طلب، وإرجاع الاستجابة المخزنة عندما يتجاوز تشابه جيب التمام عتبة (threshold) تتحكم بها. التقنيات المستخدمة: Redis 8.81 (Docker)، Redis 6.1.02 (عميل Node.js الرسمي)، TypeScript 6.0.3، Node.js 24 LTS3. الوقت المتوقع حوالي 25 دقيقة، ولا يتطلب التسجيل في أي قاعدة بيانات متجهات.
ما ستتعلمه
- لماذا يفشل التخزين المؤقت للنصوص المتطابقة (exact-string) في معظم الأسئلة المتكررة، وما الذي يحله التخزين المؤقت الدلالي
- كيفية تشغيل Redis 8 محلياً باستخدام Docker والتأكد من توفر مجموعات المتجهات
- كيفية عمل أوامر
VADDوVSIMفي Redis: التكميم (quantization)، عامل الاستكشاف (exploration factor)، ودلالات النتائج (score semantics) - كيفية بناء فئة
SemanticCacheتغلفvAdd/vSimWithScores/vGetAttr - كيفية ربط التخزين المؤقت بمسار استدعاء LLM بنمط "التحقق من التخزين المؤقت ثم الاستدعاء"
- كيفية إنهاء صلاحية مدخلات التخزين المؤقت القديمة، بما أن مجموعات المتجهات لا تملك TTL أصلي لكل عنصر
- كيفية ضبط عتبة التشابه (similarity threshold) لنموذج التضمين الخاص بك
- كيفية اختبار منطق قرار التخزين المؤقت بوحدات اختبار (unit-test) بدون خادم Redis حي
المتطلبات الأساسية
- Docker (لإصدار Redis 8.8 — تتطلب مجموعات المتجهات إصدار Redis 8.0 أو أحدث)
- Node.js 24 LTS3 (الإصدار المدعوم طويل الأمد الحالي؛ Node 20+ يعمل أيضاً — يتطلب
Redis6.x إصدار Node ≥20) - npm 10+
- مفتاح API من OpenAI إذا كنت تريد تضمينات حقيقية؛ مسار التطوير المحلي في البرنامج التعليمي يعمل بدونه
الخطوة 1 — لماذا يفشل التخزين المؤقت للمطابقة التامة
يعتمد التخزين المؤقت التقليدي لاستجابات LLM على سلسلة النص الحرفية للمطالبة (prompt): يتم عمل hash للمدخلات، وتخزين المخرجات تحت ذلك الـ hash، والتحقق من الـ hash في الطلب التالي. هذا لا ينجح إلا عندما يرسل شخص ما نفس الأحرف بالضبط مرة أخرى. في الواقع، نادراً ما يفعل المستخدمون ذلك. "ما هي سياسة الاسترداد؟" و "كيف تعمل عمليات الاسترداد؟" هما نفس السؤال بالنسبة للإنسان ولـ LLM، لكنهما ينتجان مفتاحي تخزين مؤقت مختلفين تماماً — لذا يفشل التخزين المؤقت في المرتين وتدفع ثمن استدعاءين كاملين للنموذج.
يحل التخزين المؤقت الدلالي هذا عن طريق الاعتماد على المعنى. يتم تحويل كل استعلام وارد إلى تضمين (embedding) — وهو متجه يلتقط محتواه الدلالي — ويتم تخزينه في فهرس متجهات بجانب الاستجابة. يتم تضمين الاستعلام التالي أيضاً، وبدلاً من البحث عن مطابقة تامة، تبحث في الفهرس عن أقرب جار باستخدام تشابه جيب التمام. إذا كان أقرب استعلام مخزن مشابهاً بما يكفي، فإنك تعيد استجابته المخزنة بدلاً من استدعاء النموذج مرة أخرى. توضح الأسئلة الشائعة الخاصة بالتخزين المؤقت الدلالي في Redis الفائدة بالأرقام: يضيف البحث عن تشابه المتجهات نفسه من 5 إلى 20 مللي ثانية فقط من التأخير، لكن تخطي استدعاء LLM يوفر من 1 إلى 5 ثوانٍ — عادةً ما تعود نتائج التخزين المؤقت أسرع بـ 2-4 مرات، وفي بعض الحالات تصل إلى 50-100 مرة.4
الخطوة 2 — تشغيل Redis 8 باستخدام Docker
تم إطلاق مجموعات المتجهات (عائلة VADD/VSIM) كنوع بيانات جديد في Redis 8.0، الذي صدر في 2 مايو 2025.5 لا تزال هذه الميزة مصنفة رسمياً كنسخة تجريبية (beta) — تحذر وثائق Redis الخاصة من أن API "قد يتغير، أو حتى يتوقف عن العمل" في إصدار مستقبلي5 — لذا تعامل معها كبنية تحتية قادرة على العمل في الإنتاج ولكن يجب عليك مراقبة ملاحظات سجل التغييرات، وليس كعقد نهائي تماماً.
# Docker-compose.yml
services:
Redis:
image: Redis:8.8-alpine
ports:
- '6379:6379'
volumes:
- Redis-data:/data
healthcheck:
test: ['CMD', 'Redis-cli', 'ping']
interval: 5s
timeout: 3s
retries: 5
volumes:
Redis-data:
Docker compose up -d
Docker compose exec Redis Redis-cli ping
# PONG
Docker compose exec Redis Redis-cli VADD test VALUES 3 0.1 0.2 0.3 hello
# (integer) 1 <- confirms VADD exists and works (Redis 8.0+ only)
Docker compose exec Redis Redis-cli DEL test
إذا أرجع VADD خطأ "unknown command"، فهذا يعني أن صورة Redis الخاصة بك أقدم من 8.0 — تحقق جيداً من الـ tag.
الخطوة 3 — إعداد المشروع
mkdir semantic-cache-demo && cd semantic-cache-demo
npm init -y
npm install Redis@6.1.0 openai@6.45.0
npm install -D TypeScript@6.0.3 tsx@4.23.0 @types/node@24
mkdir src
Redis 6.1.0 هو إصدار npm الحالي لعميل Node.js الرسمي (اسم الحزمة Redis، وحزمة مساحة العمل @Redis/client)، وهو يتطلب Node ≥20.0.0.2 كما انتقل إلى RESP3 كبروتوكول اتصال افتراضي في هذا الإصدار الرئيسي — لا تحتاج إلى تكوين أي شيء لذلك، ولكن من المفيد معرفة ذلك إذا كنت تقوم بتصحيح الأخطاء باستخدام تتبع Redis-cli قديم.6
الخطوة 4 — دالة التضمين
يحتاج التخزين المؤقت إلى طريقة لتحويل النص إلى متجه. للتطوير المحلي بدون مفتاح API، يكفي تضمين يعتمد على الـ hashing وبدون تبعيات لإثبات نجاح التوصيل — فهو يلتقط تداخل الكلمات وليس المعاني الحقيقية، لذا تعامل معه كبديل مؤقت وليس كنموذج تضمين للإنتاج.
// src/embed.ts
import { createHash } from 'node:crypto';
export const EMBEDDING_DIMENSIONS = 1536;
/**
* Deterministic, dependency-free stand-in for a real embedding model.
* Bag-of-words SHA-256 hashing into a 1536-dim vector, L2-normalized.
* Good enough to demo the caching MECHANISM (wiring, thresholds, TTLs)
* without an API key. It does NOT capture real semantic meaning -
* swap in embedWithOpenAI() below for production.
*/
export function embedLocally(text: string): number[] {
const vector = new Array<number>(EMBEDDING_DIMENSIONS).fill(0);
const words = text.toLowerCase().trim().split(/\s+/).filter(Boolean);
for (const word of words) {
const hash = createHash('sha256').update(word).digest();
for (let i = 0; i < EMBEDDING_DIMENSIONS; i++) {
const byte = hash[i % hash.length]!;
vector[i] = vector[i]! + ((byte / 255) * 2 - 1);
}
}
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0)) || 1;
return vector.map((v) => v / norm);
}
/**
* Production embedding via OpenAI's text-embedding-3-small ($0.02 per 1M
* input tokens, 1536 dimensions, 8,192-token max input as of the official
* embeddings guide). Requires OPENAI_API_KEY.
*/
export async function embedWithOpenAI(text: string): Promise<number[]> {
const { default: OpenAI } = await import('openai');
const client = new OpenAI();
const result = await client.embeddings.create({
model: 'text-embedding-3-small',
input: text,
});
return result.data[0]!.embedding;
}
يكلف text-embedding-3-small حوالي 0.02 دولار لكل مليون رمز مدخل (tokens) في فئة OpenAI القياسية (أخوه الأكبر، text-embedding-3-large، يكلف 0.13 دولار لكل مليون) — وهو رخيص بما يكفي لجعل تضمين كل استعلام مجرد خطأ تقريبي مقارنة باستدعاء النموذج الذي قد يوفره لك.7 تعيد كلتا دالتي التضمين متجهاً بـ 1536 بعداً، لذا فإن استبدال واحدة بالأخرى هو تغيير في سطر واحد في أي مكان آخر في هذا البرنامج التعليمي.
الخطوة 5 — فئة SemanticCache
هذا هو جوهر البرنامج التعليمي. إنه يغلف ثلاثة أوامر لمجموعات المتجهات في Redis:
VADDيدرج تضمين الاستعلام في مجموعة المتجهات، مع إرفاق نص الاستجابة الفعلي كسمة JSON عبرSETATTR.8VSIM(عبر مساعدvSimWithScoresالخاص بالعميل) يجد أقرب متجه مخزن لتضمين الاستعلام ويعيده مع نتيجة تشابه جيب التمام من 1 (متطابق) إلى 0 (متضاد).9VGETATTRيجلب سمات JSON — بما في ذلك الاستجابة المخزنة — للعنصر المطابق.
// src/semanticCache.ts
import { randomUUID } from 'node:crypto';
export interface SemanticCacheOptions {
/** Vector set key holding the cached query embeddings. */
indexKey?: string;
/** Cosine similarity threshold in [0, 1]; 1 = identical. Default 0.92. */
threshold?: number;
/** How long a cached response stays valid, in seconds. Default 3600. */
ttlSeconds?: number;
}
export interface CacheHit {
response: string;
score: number;
element: string;
}
interface CachedAttributes {
response: string;
query: string;
cachedAt: number;
}
/** The slice of the Redis client SemanticCache actually needs - matches
* `Redis` v6's real vAdd/vSim/vGetAttr/zAdd typings structurally, so a real
* RedisClientType or a test double both satisfy it without casting. */
export interface VectorCacheStore {
vCard(key: string): Promise<number>;
vAdd(
key: string,
vector: Array<number>,
element: string,
options?: { SETATTR?: Record<string, any> },
): Promise<boolean>;
vSimWithScores(
key: string,
query: Array<number>,
options?: { COUNT?: number },
): Promise<Record<string, number>>;
vGetAttr(key: string, element: string): Promise<unknown>;
vRem(key: string, element: string): Promise<boolean>;
zAdd(key: string, member: { score: number; value: string }): Promise<number>;
zRangeByScore(key: string, min: number, max: number): Promise<Array<string>>;
zRemRangeByScore(key: string, min: number, max: number): Promise<number>;
}
/**
* A semantic cache backed by Redis 8 vector sets (VADD/VSIM). Looks up the
* nearest cached query by cosine similarity instead of an exact string
* match, so paraphrased questions can still hit the cache.
*/
export class SemanticCache {
private readonly indexKey: string;
private readonly threshold: number;
private readonly ttlSeconds: number;
constructor(
private readonly Redis: VectorCacheStore,
private readonly embed: (text: string) => number[] | Promise<number[]>,
options: SemanticCacheOptions = {},
) {
this.indexKey = options.indexKey ?? 'semcache:index';
this.threshold = options.threshold ?? 0.92;
this.ttlSeconds = options.ttlSeconds ?? 3600;
}
/** Returns the nearest cached response above the similarity threshold, or null on a miss. */
async get(query: string): Promise<CacheHit | null> {
const cardinality = await this.Redis.vCard(this.indexKey);
if (cardinality === 0) return null;
const vector = await this.embed(query);
const matches = await this.Redis.vSimWithScores(this.indexKey, vector, {
COUNT: 1,
});
const [element, score] = Object.entries(matches)[0] ?? [];
if (!element || score === undefined || score < this.threshold) {
return null;
}
const attrs = (await this.Redis.vGetAttr(
this.indexKey,
element,
)) as CachedAttributes | null;
if (!attrs) return null;
return { response: attrs.response, score, element };
}
/** Stores a query/response pair and schedules it for expiry after ttlSeconds. */
async set(query: string, response: string): Promise<string> {
const vector = await this.embed(query);
const element = randomUUID();
await this.Redis.vAdd(this.indexKey, vector, element, {
SETATTR: { response, query, cachedAt: Date.now() } satisfies CachedAttributes,
});
// Vector sets have no native per-element TTL, so expiry is tracked in a
// companion sorted set (score = expiry timestamp) and swept separately.
await this.Redis.zAdd(this.expiryKey, {
score: Date.now() + this.ttlSeconds * 1000,
value: element,
});
return element;
}
/** Removes cache entries whose TTL has elapsed. Call this on a schedule (e.g. a cron tick). */
async expireStaleEntries(): Promise<number> {
const stale = await this.Redis.zRangeByScore(this.expiryKey, 0, Date.now());
for (const element of stale) {
await this.Redis.vRem(this.indexKey, element);
}
if (stale.length > 0) {
await this.Redis.zRemRangeByScore(this.expiryKey, 0, Date.now());
}
return stale.length;
}
private get expiryKey(): string {
return `${this.indexKey}:expiry`;
}
}
واجهة VectorCacheStore تسرد فقط الطرق الثمانية التي تستدعيها الفئة بالفعل. هذا مهم لسببين: عميل Redis v6 الحقيقي يحقق هذه الواجهة هيكلياً دون الحاجة لتحويل الأنواع (تم التحقق من ذلك عن طريق تجميع هذا الملف باستخدام tsc --strict مقابل الحزمة المثبتة)، كما أنه يمنحك فاصلاً ضيقاً لاستبدال العميل ببديل للاختبار لاحقاً، وهو ما تستخدمه الخطوة 8.
هناك تفصيلان يستحقان الذكر في vAdd و vSimWithScores:
- يقبل
SETATTRكائن JavaScript بسيط — يقوم العميل بتحويله إلى JSON تلقائياً عند الإرسال، ويقومvGetAttrبإعادته ككائن تلقائياً. vSimWithScoresهو غلاف ملائم من جهة العميل حولVSIM ... WITHSCORES؛ فهو يعيد كائنًا بسيطًا يربط اسم العنصر بالدرجة بدلاً من مصفوفة خام، وهذا هو السبب في أنObject.entries(matches)[0]يمنحك النتيجة الأعلى عند ضبطCOUNT: 1.
الخطوة 6 — ربطه بمسار استدعاء LLM الخاص بك
// src/demo.ts
import { createClient } from 'Redis';
import { SemanticCache } from './semanticCache.js';
import { embedLocally } from './embed.js';
async function main() {
const Redis = createClient({ url: process.env.REDIS_URL ?? 'Redis://localhost:6379' });
Redis.on('error', (err) => console.error('Redis client error', err));
await Redis.connect();
const cache = new SemanticCache(Redis, embedLocally, {
// 0.75 suits the toy embedLocally() stand-in from Step 4, which scores
// paraphrases lower than a real model would. Raise this to 0.9+ once
// you swap in embedWithOpenAI (see Step 8).
threshold: 0.75,
ttlSeconds: 3600,
});
async function answer(question: string): Promise<string> {
const hit = await cache.get(question);
if (hit) {
console.log(`cache hit (score ${hit.score.toFixed(4))`);
return hit.response;
}
console.log('cache miss - calling the LLM');
const response = `[pretend LLM response for: ${question]`; // swap for a real chat completion call
await cache.set(question, response);
return response;
}
console.log(await answer('What is the capital of France?'));
console.log(await answer("What's the capital city of France?"));
await Redis.close();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
النمط هو: تحقق من ذاكرة التخزين المؤقت أولاً، واستدعِ النموذج فقط عند عدم العثور على النتيجة (miss)، ثم اكتب النتيجة مرة أخرى. مستمع Redis.on('error', ...) ليس اختياريًا — فحدث error غير معالج في Node.js EventEmitter يؤدي إلى تعطل العملية، ويقوم العميل بإصدار واحد عند كل خلل في الاتصال.
الخطوة 7 — إنهاء صلاحية المدخلات القديمة
لا تدعم مجموعات المتجهات في Redis خاصية EXPIRE لكل عنصر كما تفعل المفاتيح العادية — لا يوجد خيار VADD ... EX 3600 في بناء جملة الأمر.8 تتغلب SemanticCache.set() على ذلك عن طريق تسجيل الطابع الزمني لانتهاء صلاحية كل عنصر في مجموعة مرتبة مرافقة (semcache:index:expiry)، مصنفة حسب Date.now() + ttlSeconds * 1000. قم بتشغيل expireStaleEntries() على فترات زمنية منتظمة:
// somewhere in your app's startup
setInterval(() => {
cache.expireStaleEntries().then((removed) => {
if (removed > 0) console.log(`semantic cache: swept ${removed stale entries`);
});
}, 60_000);
هذا يمنع مجموعة المتجهات من النمو بلا حدود ويمنعها من تقديم إجابات قديمة لدرجة أنها قد تكون خاطئة (الأسعار، التواريخ، أي شيء حساس للوقت).
الخطوة 8 — ضبط عتبة التشابه
العتبة (threshold) في SemanticCacheOptions هي جوهر اللعبة: إذا كانت منخفضة جدًا، فستقدم إجابة مخزنة مؤقتًا لسؤال يشبه سؤالاً سابقًا بشكل سطحي فقط؛ وإذا كانت عالية جدًا، فنادرًا ما ستحصل على نتيجة مخزنة. تصف مواد التخزين المؤقت الدلالي الخاصة بـ Redis والتقارير المستقلة نطاقًا شائعًا يبدأ من حوالي 0.85–0.95 من تشابه جيب التمام (cosine similarity) لنماذج التضمين الحقيقية4 — لكن هذا الرقم يعتمد على النموذج ويحتاج إلى التحقق منه مقابل حركة استعلاماتك الخاصة، وليس نسخه حرفيًا. مع تضمين التجزئة البسيط من الخطوة 4، توقع درجات مطلقة أقل بكثير لإعادة الصياغة الحقيقية (تداخل جزئي للكلمات، لا يوجد فهم دلالي حقيقي)، لذا فإن العتبة الأقل مناسبة هناك؛ أعد المعايرة بمجرد التبديل إلى embedWithOpenAI.
هناك خياران إضافيان لـ VSIM يهمان الضبط بما يتجاوز العتبة الخام: EF (عامل استكشاف البحث، عادةً 50–1000) يقايض زمن انتقال البحث بالاستدعاء (recall)، و COUNT يحدد عدد المرشحين الذين يتم إرجاعهم — يمرر هذا البرنامج التعليمي دائمًا COUNT: 1 لأنه يريد فقط أفضل تطابق واحد.9
التحقق
تم نسخ كل أمر Redis في هذا البرنامج التعليمي (VADD، VSIM/vSimWithScores، VGETATTR، VCARD، VREM) حرفيًا من مرجع أوامر Redis الرسمي، وتم فحص كل سطر من TypeScript في semanticCache.ts و demo.ts باستخدام tsc --strict --noUncheckedIndexedAccess مقابل حزمة Redis@6.1.0 الحقيقية المثبتة عبر npm — صفر أخطاء. تم اختبار منطق اتخاذ القرار في الفئة (عدم وجود نتيجة في الذاكرة الباردة، تطابق دلالي عند إعادة الصياغة، عدم وجود نتيجة في استعلام غير ذي صلة، انتهاء الصلاحية بناءً على TTL) من البداية إلى النهاية باستخدام بديل اختبار في الذاكرة (test double) ينفذ نفس واجهة VectorCacheStore؛ نجحت جميع التأكيدات (انظر الخطوة 9). ما لم يستطع القيام به هذا البرنامج التعليمي هو تشغيل الكود بالضبط مقابل خادم Redis 8 مباشر — لم يكن هناك Docker أو وصول بصلاحيات root متاحًا أثناء كتابته. قبل أن تثق في هذا في بيئة الإنتاج، قم بتشغيله بنفسك:
Docker compose up -d
npx tsx src/demo.ts
المخرجات المتوقعة (باستخدام بديل embedLocally من الخطوة 4 و threshold: 0.75 المحدد في demo.ts — انظر الخطوة 8 لمعرفة سبب كون هذا الرقم أقل من عتبة الإنتاج):
cache miss - calling the LLM
[pretend LLM response for: What is the capital of France?]
cache hit (score 0.7701)
[pretend LLM response for: What is the capital of France?]
يعيد الاستدعاء الثاني استخدام الاستجابة الأولى لأن تشابه جيب التمام لإعادة الصياغة (0.7701 مقابل التضمين البسيط) تجاوز العتبة — استبدله بـ embedWithOpenAI وعتبة 0.9+ للمطابقة الدلالية الحقيقية في الإنتاج.
الخطوة 9 — الاختبار بدون Redis مباشر
نظرًا لأن VectorCacheStore يسرد فقط الطرق الثمانية التي تستدعيها SemanticCache، يمكنك اختبار منطق التفرع للفئة باستخدام بديل صغير في الذاكرة بدلاً من تشغيل Redis لكل اختبار:
// test double: brute-force cosine similarity over an in-memory Map
function makeFakeRedis(): VectorCacheStore {
const vectors = new Map<string, { vector: number[]; attrs: unknown }>();
const expiries = new Map<string, number>();
const cosine = (a: number[], b: number[]) =>
a.reduce((sum, v, i) => sum + v * b[i]!, 0); // vectors are pre-normalized
return {
async vCard() {
return vectors.size;
},
async vAdd(_key, vector, element, options) {
vectors.set(element, { vector, attrs: options?.SETATTR ?? null });
return true;
},
async vSimWithScores(_key, query, options) {
const scored = [...vectors.entries()]
.map(([element, e]) => ({ element, score: cosine(query, e.vector) }))
.sort((a, b) => b.score - a.score)
.slice(0, options?.COUNT ?? 10);
return Object.fromEntries(scored.map((s) => [s.element, s.score]));
},
async vGetAttr(_key, element) {
return vectors.get(element)?.attrs ?? null;
},
async vRem(_key, element) {
return vectors.delete(element);
},
async zAdd(_key, member) {
expiries.set(member.value, member.score);
return 1;
},
async zRangeByScore(_key, min, max) {
return [...expiries.entries()].filter(([, s]) => s >= min && s <= max).map(([v]) => v);
},
async zRemRangeByScore(_key min, max) {
let removed = 0;
for (const [v, s] of [...expiries.entries()]) {
if (s >= min && s <= max) {
expiries.delete(v);
removed++;
}
}
return removed;
},
};
}
وجه SemanticCache إلى هذا البديل بدلاً من عميل حقيقي، ويمكنك التأكد من نتائج التخزين المؤقت (hits) وحالات عدم الوجود (misses) وعمليات مسح TTL في أجزاء من الثانية، في بيئة CI، دون أي اعتماد على Redis على الإطلاق — واحتفظ باختبار التكامل الحقيقي المدعوم بـ Docker لمجموعة اختبارات أصغر وأبطأ يتم تشغيلها بشكل أقل تكرارًا.
الأخطاء الشائعة
ERR unknown command 'VADD'— خادم Redis الخاص بك أقدم من الإصدار 8.0. مجموعات المتجهات غير موجودة قبل Redis Open Source 8.0.0؛ تحقق باستخدامDocker compose exec Redis Redis-cli INFO server | grep redis_version.vSimWithScoresيعيد كائنًا فارغًا — مجموعة المتجهات فارغة (vCardيعيد 0) أو اسم المفتاح لا يتطابق مع ما استخدمهvAdd. تقومSemanticCacheبحماية الحالة الفارغة صراحةً فيget().vGetAttrيعيدnullحتى بعد نجاحvAdd— لقد نسيت تمريرSETATTRفي استدعاءvAddالمحدد، أو أنك تستعلم عن معرفelementخاطئ (المعرف الذي أعادهvSimWithScores، وليس معرفًا أنشأته بنفسك).- أخطاء عدم تطابق الأبعاد من
VADD— يجب أن تشترك جميع المتجهات المضافة إلى نفس المفتاح في نفس عدد الأبعاد. سيؤدي خلطembedLocally(1536 بعدًا) وembedWithOpenAI(أيضًا 1536 بعدًا حسب التصميم، ولكن لا تفترض أن هذا ينطبق على النماذج الأخرى) في نفس الفهرس إلى حدوث خطأ في اللحظة التي تقوم فيها بالتبديل دون مسح المفتاح. - حدث
errorغير معالج يؤدي إلى تعطل العملية — قم دائمًا بإرفاقRedis.on('error', ...)قبل استدعاء.connect()؛ يتبع node-Redis عقدEventEmitterالخاص بـ Node، ويؤدي حدثerrorبدون مستمع إلى إلقاء استثناء. - الدرجات تبدو منخفضة بشكل غير متوقع — تكميم
VADDالافتراضي هوQ8(8 بت)، والذي يقايض القليل من دقة الاستدعاء بالذاكرة. أضفQUANT: 'NOQUANT'إلى خياراتvAddفيsemanticCache.tsإذا كنت بحاجة إلى دقة float32 كاملة للاختبار، وارجع إلى الافتراضيQ8(أوBINالصريح لأقصى توفير في الذاكرة) بمجرد التحقق من العتبة الخاصة بك.
الخطوات التالية
استخدم هذا البرنامج التعليمي Redis كمخزن متجهات فقط؛ إذا كنت جديدًا على عميل Node.js نفسه، فإن دليل Valkey و Node.js للإنتاج يغطي تجميع الاتصالات، وإعادة المحاولة، و pub/sub مع نفس عائلة العملاء. إذا كنت تريد فهم رياضيات مقياس المسافة تحت تصنيف جيب التمام لـ VSIM، فراجع تشابه جيب التمام مقابل الضرب النقطي للتضمينات. وإذا كان هدفك هو تقليل إنفاق API بدلاً من زمن الانتقال تحديدًا، فقم بمقارنة هذا النهج مع التخزين المؤقت للمطالبات في Claude — يعيد التخزين المؤقت للمطالبات استخدام بادئة ثابتة في نفس شكل الطلب، بينما يعيد التخزين المؤقت الدلالي في هذا البرنامج التعليمي استخدام استجابة كاملة عبر طلبات مختلفة ولكن متشابهة؛ الاثنان يكملان بعضهما البعض وليسا متنافسين.
من هنا: أضف namespace لكل مستأجر إذا كنت تبني تخزينًا مؤقتًا متعدد المستأجرين (أضف بادئة لـ indexKey لكل عميل)، واستبدل مسح TTL للمجموعة المرتبة بـ cron أخف إذا كانت حركة المرور لديك متقطعة، وقم بتسجيل الـ score عند كل نتيجة (hit) حتى تتمكن من ضبط العتبة بناءً على بيانات الإنتاج الحقيقية بدلاً من التخمين.
Footnotes
-
صورة Docker Hub الرسمية لـ
Redis— تشمل تاجات Redis 8 الحالية8.8.0، و8.8، و8-alpine(إصدارات Debian "trixie" و Alpine). https://hub.Docker.com/_/Redis/tags ↩ -
حزمة package.json لـ
@Redis/client، مستودع node-Redis الرسمي (إصدار حزمة npm لـRedisهو 6.1.0،engines.node >= 20.0.0). https://GitHub.com/Redis/node-Redis ↩ ↩2 -
جدول إصدارات Node.js — إصدار Node 24 هو خط Active LTS الحالي (نهاية الدعم في 30 أبريل 2028)؛ وإصدار Node 22 في مرحلة Maintenance LTS (نهاية الدعم في 30 أبريل 2027). https://nodejs.org/en/about/previous-releases ↩ ↩2
-
Redis — "ما هو التخزين المؤقت الدلالي (Semantic Caching)؟ دليل لتطبيقات LLM أسرع وأذكى." يضيف البحث عن تشابه المتجهات (Vector similarity search) عبئاً يتراوح بين 5-20 مللي ثانية ولكنه يوفر من 1-5 ثوانٍ مقارنة باستدعاء LLM؛ وتكون إصابات ذاكرة التخزين المؤقت (Cache hits) عادةً أسرع بـ 2-4 مرات، وتصل إلى 50-100 مرة في الحالات المثالية؛ ويتراوح نطاق عتبة تشابه جيب التمام (Cosine similarity) الشائع بين 0.85-0.95. https://Redis.io/blog/what-is-semantic-caching/ ↩ ↩2
-
Redis — "Redis 8 متاح الآن بشكل عام (GA)، محملاً بميزات جديدة وأكثر من 30 تحسيناً في الأداء" (نُشرت المدونة في 1 مايو 2025؛ وقام مستودع
Redis/Redisعلى GitHub بوضع تاج لإصدار 8.0.0 في 2 مايو 2025). تم تقديم Vector set كنوع بيانات جديد، وتم تمييزه كنسخة تجريبية (Beta) ("قد نقوم بتغيير، أو حتى كسر، الميزات و API في الإصدارات المستقبلية"). https://Redis.io/blog/Redis-8-ga/ ; https://GitHub.com/Redis/Redis/releases/tag/8.0.0 ↩ ↩2 -
إصدار node-Redis 6.0 — أصبح RESP3 هو بروتوكول الاتصال الافتراضي بدءاً من هذا الإصدار الرئيسي. https://GitHub.com/Redis/node-Redis/releases ↩
-
تسعير OpenAI API —
text-embedding-3-smallبسعر 0.02 دولار لكل 1 مليون توكن، وtext-embedding-3-largeبسعر 0.13 دولار لكل 1 مليون توكن (صفحة النماذج الرسمية). https://developers.openai.com/API/docs/models/text-embedding-3-small ↩ -
مرجع أمر VADD — الصيغة، والخيارات (
REDUCE،CAS،NOQUANT/Q8/BIN،EF،SETATTR،M)، والقيم الافتراضية (EF200،M16، القيمة الافتراضية للتكميم هيQ8). متاح منذ إصدار Redis Open Source 8.0.0. https://Redis.io/docs/latest/commands/vadd/ ↩ ↩2 -
مرجع أمر VSIM — الصيغة، والخيارات (
WITHSCORES،WITHATTRIBS،COUNT،EPSILON،EF،FILTER،TRUTH،NOTHREAD)، نطاق الدرجة من 1 (متطابق) إلى 0 (متضاد). تمت إضافةWITHATTRIBSفي إصدار Redis 8.2.0. https://Redis.io/docs/latest/commands/vsim/ ↩ ↩2