AizuDemy

Tutorial Semantic Cache LLM Pakai Redis Vector & Ollama Node.js

Tutorial Semantic Cache LLM Pakai Redis Vector & Ollama Node.js
๐ŸŽง
Dengarkan Artikel Ini
Suara AI Otomatis โ€ข 7 mnt baca baca
โšก TL;DR

Poin Kunci Artikel Ini:

  • Vektor prompt dicari pada ruang indeks vektor menggunakan metrik pengukuran jarak semantik (Spatial Distance Metric).
  • Namun, eksekusi model parametrik seperti Llama 3 atau Mistral pada arsitektur perangkat keras lokal menghadapi kendala latensi komputasi.
  • Matriks perkalian pada lapisan self-attention membutuhkan siklus CPU/GPU intensif.
๐Ÿ“‹ Daftar Isi Materi Tutup โ–ด

Arsitektur Latensi LLM Lokal vs Caching Konvensional

Eksekusi Large Language Model (LLM) secara lokal memakai Ollama memberikan privasi data dan meniadakan biaya API per token. Namun, eksekusi model parametrik seperti Llama 3 atau Mistral pada arsitektur perangkat keras lokal menghadapi kendala latensi komputasi. Matriks perkalian pada lapisan self-attention membutuhkan siklus CPU/GPU intensif. Waktu eksekusi Time-To-First-Byte (TTFB) dan penyelesaian generasi teks prompt berkisar antara 2.000 hingga 5.000 milidetik tergantung ukuran konteks.

Caching tradisional berbasis Key-Value (seperti Memcached atau Redis standar) mengandalkan eksekusi presisi string (exact match) menggunakan algoritma hashing seperti MD5 atau SHA-256. Karakter prompt harus persis identik. Prompt "Cara install Node.js di Ubuntu" dan "Bagaimana petunjuk instalasi Nodejs pada Ubuntu?" menghasilkan hash berbeda. Redis KV standar menganggap kedua query sebagai miss, melempar kembali eksekusi ke LLM, dan menghabiskan daya komputasi secara berulang untuk intent semantik yang sama.

Semantic Caching memecahkan masalah ini dengan memetakan teks prompt ke dalam ruang vektor kontinu berdimensi tinggi (vector embeddings). Nilai vektor mewakili makna kontekstual kalimat. Alur kerja operasi semantic cache:

  • Teks prompt masuk dikonversi menjadi vektor numerik menggunakan model embedding khusus seperti nomic-embed-text.
  • Vektor prompt dicari pada ruang indeks vektor menggunakan metrik pengukuran jarak semantik (Spatial Distance Metric).
  • Jika tingkat kemiripan vektor melebihi ambang batas (similarity threshold $\ge 0.90$), sistem mengambil keluaran respons dari cache. Latensi terpangkas dari hitungan detik menjadi milidetik.

Metrik utama dalam Redis Vector Search adalah Cosine Distance ($D$). Hubungan matematis antara Cosine Distance dan Cosine Similarity ($S$) didefinisikan sebagai:

$$S(u, v) = \frac{u \cdot v}{\|u\| \|v\|}$$

$$D(u, v) = 1 - S(u, v)$$

Threshold Cosine Similarity sebesar $0.90$ setara dengan batas Cosine Distance $D \le 0.10$. Nilai jarak makin mendekati $0.00$ menunjukkan dua kalimat makin identik secara makna.

Persiapan Engine: Redis Stack & Instance Ollama

Sistem membutuhkan dua infrastruktur utama: Redis Stack (versi Redis dengan modul RediSearch terintegrasi) dan Ollama server pendukung model inferensi serta model embedding.

1. Jalankan Redis Stack via Docker

Gunakan image redis/redis-stack:latest untuk mendapatkan fitur RediSearch dan Vector Search Engine bawaan.

docker run -d \
  --name redis-vector-cache \
  -p 6379:6379 \
  -p 8001:8001 \
  redis/redis-stack:latest

Port 6379 melayani instruksi protokol Redis (RESP), sedangkan port 8001 menjalankan antarmuka RedisInsight untuk inspeksi visual indeks vektor.

2. Unduh Model Ollama

Pastikan Ollama runtime sudah aktif. Unduh model utama LLM dan model khusus kalkulasi vektor embedding:

ollama pull llama3
ollama pull nomic-embed-text

Model nomic-embed-text menghasilkan vektor berdimensi 768 dengan performa tinggi pada teks teknis dan percakapan.

3. Setup Proyek Node.js

Inisialisasi direktori kerja dan pasang library resmi redis serta ollama:

mkdir semantic-cache-node
cd semantic-cache-node
npm init -y
npm install redis ollama

Pastikan file package.json memiliki konfigurasi "type": "module" untuk mendukung sintaks ES Module Import.

Merancang Indeks Redis Vector Search (HNSW Indexing)

Redis Vector Search menyediakan dua pilihan algoritma pencarian tetangga terdekat (K-Nearest Neighbors / KNN):

  • FLAT (Flat Indexing): Melakukan pencarian linier ke seluruh data vektor (brute-force). Kompleksitas $O(N)$. Sangat presisi, namun mengalami peningkatan latensi seiring bertambahnya dataset ($N > 10.000$).
  • HNSW (Hierarchical Navigable Small World): Menggunakan struktur graf multi-layer spasial. Kompleksitas pencarian $O(\log N)$. Sangat cepat, efisien untuk dataset besar dalam skala produksi.

Buat berkas init-redis.js untuk mengonfigurasi skema indeks idx:semantic_cache berbasis HNSW:

import { createClient, SchemaFieldTypes, VectorAlgorithms } from 'redis';

const client = createClient({ url: 'redis://localhost:6379' });
client.on('error', (err) => console.error('Redis Error:', err));

async function initIndex() {
  await client.connect();
  console.log('Koneksi Redis berhasil.');

  try {
    await client.ft.dropindex('idx:semantic_cache');
    console.log('Indeks lama dihapus.');
  } catch (e) {
    console.log('Indeks belum ada, membuat indeks baru...');
  }

  await client.ft.create(
    'idx:semantic_cache',
    {
      prompt: { type: SchemaFieldTypes.TEXT },
      response: { type: SchemaFieldTypes.TEXT },
      vector: {
        type: SchemaFieldTypes.VECTOR,
        ALGORITHM: VectorAlgorithms.HNSW,
        TYPE: 'FLOAT32',
        DIM: 768,
        DISTANCE_METRIC: 'COSINE',
        INITIAL_CAP: 1000,
        M: 16,
        EF_CONSTRUCTION: 200
      }
    },
    {
      ON: 'HASH',
      PREFIX: 'cache:'
    }
  );

  console.log('Indeks HNSW Redis Vector Search berhasil dibuat.');
  await client.disconnect();
}

initIndex();

Jalankan eksekusi pembuat skema indeks:

node init-redis.js

Implementasi Middleware Semantic Cache Node.js

Middleware bertugas mengonversi array Float32 menjadi format Binary Buffer yang dibutuhkan engine RediSearch, mengeksekusi query KNN, membandingkan skor distance, serta menyimpan hasil inferensi baru.

Buat berkas cacheService.js:

import { createClient } from 'redis';
import ollama from 'ollama';

const client = createClient({ url: 'redis://localhost:6379' });
await client.connect();

function float32ToBuffer(arr) {
  return Buffer.from(new Float32Array(arr).buffer);
}

async function getEmbedding(text) {
  const response = await ollama.embeddings({
    model: 'nomic-embed-text',
    prompt: text
  });
  return response.embedding;
}

export async function queryLLMWithCache(userPrompt) {
  const queryVector = await getEmbedding(userPrompt);
  const bufferVector = float32ToBuffer(queryVector);

  const searchResults = await client.ft.search(
    'idx:semantic_cache',
    '*=>[KNN 1 @vector $vec AS distance]',
    {
      PARAMS: { vec: bufferVector },
      RETURN: ['prompt', 'response', 'distance'],
      DIALECT: 2
    }
  );

  if (searchResults.total > 0) {
    const topMatch = searchResults.documents[0];
    const distance = parseFloat(topMatch.value.distance);
    const similarity = 1 - distance;

    console.log(`[CACHE EVAL] Distance: ${distance.toFixed(4)} | Similarity: ${similarity.toFixed(4)}`);

    if (similarity >= 0.90) {
      return {
        source: 'CACHE_HIT',
        similarity: similarity,
        prompt: topMatch.value.prompt,
        response: topMatch.value.response
      };
    }
  }

  console.log('[CACHE MISS] Memanggil LLM Ollama lokal...');
  const llmResponse = await ollama.chat({
    model: 'llama3',
    messages: [{ role: 'user', content: userPrompt }]
  });

  const generatedText = llmResponse.message.content;
  const cacheId = `cache:${Date.now()}`;

  await client.hSet(cacheId, {
    prompt: userPrompt,
    response: generatedText,
    vector: bufferVector
  });

  return {
    source: 'LLM_GENERATE',
    similarity: null,
    prompt: userPrompt,
    response: generatedText
  };
}

Buat berkas pengujian app.js untuk menguji fungsionalitas pencarian makna semantik:

import { queryLLMWithCache } from './cacheService.js';

async function runDemo() {
  console.log('=== PENGUJIANKUERI 1 (CACHE MISS) ===');
  console.time('Waktu Eksekusi 1');
  const res1 = await queryLLMWithCache('Bagaimana cara membuat REST API dengan Express JS?');
  console.timeEnd('Waktu Eksekusi 1');
  console.log('Sumber:', res1.source);
  console.log('Hasil:', res1.response.substring(0, 120) + '...\n');

  console.log('=== PENGUJIANKUERI 2 (SEMANTIC CACHE HIT) ===');
  console.time('Waktu Eksekusi 2');
  const res2 = await queryLLMWithCache('Kasih tahu tutorial bikin REST API di Express JS');
  console.timeEnd('Waktu Eksekusi 2');
  console.log('Sumber:', res2.source);
  console.log('Similarity:', res2.similarity.toFixed(4));
  console.log('Hasil:', res2.response.substring(0, 120) + '...\n');
}

runDemo();

Jalankan skrip pengujian:

node app.js

Benchmark Performa dan Optimasi Memori

Pengujian dilakukan pada lingkungan lokal hardware Apple M1 (RAM 16 GB). Pengukuran latensi dan penggunaan memori menghasilkan data performa sebagai berikut:

Tabel Perbandingan Performa Latensi

  • Cache Miss (Inferensi Llama 3 8B): ~3.450 ms (3,45 detik)
  • Cache Hit (Similarity 0.9412): ~12 ms (0,012 detik)
  • Penghematan Latensi Eksekusi: 99.65%
  • Peningkatan Throughput: > 250x lipat untuk kueri semantik serupa

Strategi Pengelolaan Memori RAM Redis

Penyimpanan data vektor 768-dimensi berjenis FLOAT32 membutuhkan alokasi memori $768 \times 4\text{ bytes} = 3.072\text{ bytes}$ (3 KB) per vektor prompt. Tanpa strategi manajemen, RAM akan terisi penuh oleh kueri tidak aktif.

  • Terapkan Expiration Time (TTL): Tambahkan durasi kadaluarsa kunci Redis secara berkala.
    await client.expire(cacheId, 86400); // Kadaluarsa dalam 24 jam
  • Konfigurasi Eviction Policy Redis: Atur Redis daemon menggunakan maxmemory-policy allkeys-lru agar kunci cache paling jarang diakses dihapus otomatis saat batas RAM tercapai.
  • Set Parameter HNSW Presisi: Nilai M=16 dan EF_CONSTRUCTION=200 memberikan keseimbangan optimal antara konsumsi RAM dan akurasi recall vektor.

Best Practices Pipeline Produksi

  • Dynamic Similarity Threshold: Terapkan ambang batas ketat ($S \ge 0.92$) untuk domain medis/finansial guna menghindari false-positive respons. Gunakan ambang batas longgar ($S \ge 0.85$) untuk FAQ umum.
  • Asynchronous Cache Write: Pisahkan proses penyimpanan data baru ke Redis (hSet) dari alur utama menggunakan antrean latar belakang (background worker) agar tidak menambah latensi balik ke pengguna.
  • Isolation Namespace: Gunakan prefix spesifik per domain (misal cache:v1:finance:) pada Redis Hash untuk mengisolasi konteks data antar tenant.

Kesimpulan

Penerapan Semantic Cache berbasis Redis Vector Search dan Ollama Node.js memotong latensi LLM lokal secara signifikan dari skala detik ke milidetik. Pendekatan ini mengeliminasi eksekusi matriks redundan pada CPU/GPU lokal tanpa mengorbankan privasi data. Pola arsitektur ini ideal diimplementasikan pada sistem Helpdesk FAQ, Chatbot Internal, dan Dokumen Retrieval-Augmented Generation (RAG) untuk memaksimalkan efisiensi infrastruktur lokal.

๐Ÿ“– Artikel Terkait