Pengantar: Dari Angka Menjadi Pencarian Cerdas

Halo developer Laravel! Selamat datang kembali di seri tutorial membangun fitur AI Search. Pada Part 1 sebelumnya, kita telah membahas kelemahan sistem pencarian tradisional berbasis kata kunci (keyword-matching) dan mulai membangun fondasi untuk platform e-learning fiktif kita, LaraCourse.

Kita juga sudah berhasil membuat Artisan Command untuk mengirimkan data kursus ke API OpenAI, mengubah teks tersebut menjadi vector embeddings (deretan angka desimal), dan menyimpannya ke dalam kolom JSON di database kita. Namun, deretan angka tersebut belum ada gunanya jika kita tidak tahu cara mencarinya.

Di Part 2 ini, kita akan menjawab pertanyaan krusial: Bagaimana cara kita membandingkan teks yang diketik oleh user dengan ribuan angka di database kita? Jawabannya ada pada sebuah konsep matematika bernama Cosine Similarity. Mari kita mulai keajaibannya!

Memahami Konsep Cosine Similarity

Sebelum masuk ke kode, mari kita pahami sedikit teorinya. Bayangkan vector embedding sebagai sebuah koordinat garis di dalam ruang multidimensi. Setiap teks yang kita proses ke OpenAI diubah menjadi garis tersebut.

Cosine Similarity adalah metode untuk mengukur kemiripan antara dua garis (vector) dengan cara menghitung sudut di antara keduanya. Semakin kecil sudutnya, semakin mirip makna dari kedua teks tersebut, terlepas dari panjang pendeknya teks. Nilai Cosine Similarity berkisar antara -1 hingga 1:

  • Nilai 1: Teks memiliki makna atau konteks yang sangat identik.
  • Nilai 0: Teks tidak memiliki hubungan konteks sama sekali.
  • Nilai -1: Teks memiliki makna yang sangat berlawanan.

Tugas kita di aplikasi Laravel adalah: mengubah kata kunci pencarian user menjadi vector, lalu menghitung nilai Cosine Similarity-nya terhadap semua vector kursus di database, dan mengurutkan hasilnya dari nilai yang paling mendekati angka 1.

Langkah 1: Membuat Helper Cosine Similarity di PHP

Karena pada Part 1 kita menyimpan data vector di dalam kolom JSON standar (bukan menggunakan database khusus vector), kita akan melakukan proses kalkulasi matematis ini di level aplikasi (PHP). Pendekatan ini sangat baik untuk pembelajaran dan masih sangat mumpuni untuk data dalam skala ribuan.

Mari kita buat sebuah class Helper sederhana. Buat file baru di app/Helpers/VectorHelper.php (Anda mungkin perlu membuat folder Helpers terlebih dahulu):

<?php

namespace App\Helpers;

class VectorHelper
{
    /**
     * Menghitung nilai Cosine Similarity antara dua array vector.
     */
    public static function cosineSimilarity(array $vec1, array $vec2): float
    {
        $dotProduct = 0;
        $magnitude1 = 0;
        $magnitude2 = 0;

        $count = count($vec1);
        for ($i = 0; $i < $count; $i++) {
            $dotProduct += $vec1[$i] * $vec2[$i];
            $magnitude1 += pow($vec1[$i], 2);
            $magnitude2 += pow($vec2[$i], 2);
        }

        if ($magnitude1 == 0 || $magnitude2 == 0) {
            return 0;
        }

        return $dotProduct / (sqrt($magnitude1) * sqrt($magnitude2));
    }
}

Fungsi di atas akan menerima dua array (vector dari OpenAI selalu memiliki panjang dimensi yang sama, yaitu 1536 untuk model text-embedding-3-small) dan mengembalikan skor kemiripan (float).

Langkah 2: Memperbarui Controller Pencarian

Sekarang, mari kita ubah CourseSearchController yang sebelumnya menggunakan klausa LIKE menjadi sistem pencarian semantik penuh.

Buka file app/Http/Controllers/CourseSearchController.php dan modifikasi menjadi seperti berikut:

<?php

namespace App\Http\Controllers;

use App\Models\Course;
use Illuminate\Http\Request;
use OpenAI\Laravel\Facades\OpenAI;
use App\Helpers\VectorHelper;

class CourseSearchController extends Controller
{
    public function search(Request $request)
    {
        $query = $request->input('q');
        
        if (!$query) {
            return response()->json(['message' => 'Query pencarian kosong'], 400);
        }

        // 1. Ubah input pencarian user menjadi Vector Embedding menggunakan OpenAI
        $response = OpenAI::embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $query,
        ]);

        $queryVector = $response->embeddings[0]->embedding;

        // 2. Ambil semua kursus yang sudah memiliki embedding
        // Catatan: Untuk data besar, metode in-memory ini perlu dioptimasi (dibahas di Part 3)
        $courses = Course::whereNotNull('embedding')->get();

        // 3. Kalkulasi Cosine Similarity untuk setiap kursus
        $results = $courses->map(function ($course) use ($queryVector) {
            $score = VectorHelper::cosineSimilarity($course->embedding, $queryVector);
            
            // Tambahkan property score ke object course
            $course->similarity_score = $score;
            
            // Sembunyikan array embedding yang panjang dari response JSON
            $course->makeHidden('embedding');
            
            return $course;
        });

        // 4. Urutkan berdasarkan skor tertinggi dan ambil 5 teratas
        $topMatches = $results->sortByDesc('similarity_score')
                              ->take(5)
                              ->values();

        return response()->json([
            'query' => $query,
            'results' => $topMatches
        ]);
    }
}

Penjelasan Alur Kode:

  1. Embed Input User: Saat user mengetikkan kata kunci (misalnya "cara bikin web responsif"), kita tidak langsung mencarinya di database. Kita mengirim teks itu ke OpenAI untuk diubah menjadi array berisi 1536 angka.
  2. Fetch Database: Kita mengambil semua data kursus dari database (pastikan hanya yang kolom embedding-nya tidak null).
  3. Proses Kalkulasi: Menggunakan map() dari Laravel Collection, kita bandingkan vector input user dengan vector masing-masing kursus menggunakan fungsi cosineSimilarity yang sudah kita buat.
  4. Sorting: Hasil akhirnya kita urutkan dari skor tertinggi (menurun/descending), lalu memotongnya hanya untuk 5 hasil terbaik menggunakan take(5).

Langkah 3: Menguji Keajaiban Semantic Search

Mari kita buktikan perbedaan sistem baru kita ini. Jika Anda menjalankan server dan mencoba melakukan request ke endpoint pencarian kita dengan kata kunci:

Query: "belajar desain antarmuka aplikasi"

Dengan keyword-matching konvensional (LIKE), jika judul kursus Anda adalah "Mastering UI/UX in Figma", sistem akan gagal menemukannya karena kata "desain", "antarmuka", dan "aplikasi" tidak ada di dalam teks tersebut secara persis.

Namun, dengan AI Search kita, API OpenAI memahami bahwa "desain antarmuka" secara semantik sama dengan "UI/UX". Hasil response JSON Anda akan terlihat seperti ini:

{
  "query": "belajar desain antarmuka aplikasi",
  "results": [
    {
      "id": 12,
      "title": "Mastering UI/UX in Figma",
      "description": "Panduan komprehensif membuat user interface dan pengalaman pengguna yang menarik.",
      "category": "Design",
      "similarity_score": 0.824513
    },
    {
      "id": 45,
      "title": "Fundamental Web Design",
      "description": "Pelajari dasar-dasar tata letak visual untuk website modern.",
      "category": "Design",
      "similarity_score": 0.612098
    }
  ]
}

Luar biasa, bukan? Skor 0.824513 menunjukkan tingkat kemiripan konteks yang sangat tinggi, meskipun struktur bahasanya sama sekali berbeda.

Kesimpulan Sementara dan Limitasi

Selamat! Di Part 2 ini, Anda telah berhasil merangkai puzzle AI Search secara utuh. Anda tidak lagi bergantung pada pencocokan kata secara kaku. Aplikasi Laravel Anda kini memiliki kemampuan pencarian semantik cerdas layaknya mesin pencari modern.

Namun, ada sebuah tantangan besar menanti.

Pendekatan menghitung Cosine Similarity di level memori PHP dengan Laravel Collection (seperti kode di atas) berjalan sangat mulus untuk ratusan hingga ribuan baris data. Tetapi bagaimana jika platform LaraCourse Anda berkembang pesat dan memiliki puluhan ribu hingga jutaan data kursus dan modul?

Menarik jutaan baris data ke dalam RAM (memori PHP) setiap kali user melakukan pencarian akan membuat server Anda kehabisan memori (Out of Memory) dan proses pencarian memakan waktu berdetik-detik lamanya.

Teaser Part 3: Skalabilitas dengan Vector Database

Untuk menyelesaikan masalah performa pada skala besar (Enterprise Scale), kita perlu memindahkan beban kalkulasi matematika dari aplikasi PHP langsung ke level Database.

Di Part 3 (Terakhir) dari seri ini, kita akan melakukan refactoring besar-besaran. Kita akan membahas integrasi dengan Vector Database yang sesungguhnya (seperti ekstensi pgvector pada PostgreSQL atau layanan cloud seperti Pinecone/Meilisearch). Kita akan belajar bagaimana melakukan indexing vector agar pencarian dari puluhan ribu data dapat dieksekusi dalam hitungan milidetik!

Pastikan Anda sudah memahami konsep dasar di Part 2 ini, karena di artikel selanjutnya, kita akan membawa aplikasi Laravel Anda ke level arsitektur tingkat lanjut. Sampai jumpa di Part 3!