Series Laravel & Elasticsearch Thực chiến #4: Quyền năng Aggregation – Vẽ biểu đồ doanh thu siêu tốc
Chào bạn và anh em!
Ở Bài 3, chúng ta đã biến Elasticsearch thành một cỗ máy tìm kiếm đa điều kiện siêu tốc. Tuy nhiên, sức mạnh thực sự khiến các tập đoàn lớn "phát cuồng" vì ES không chỉ nằm ở việc tìm kiếm văn bản, mà là khả năng Phân tích và Thống kê dữ liệu (Data Analytics).
Hãy tưởng tượng sếp yêu cầu bạn: "Xuất cho tôi báo cáo tổng doanh thu của từng nhà ga Metro trong tháng này để lên biểu đồ".
Nếu dùng MySQL với một bảng chứa 50 triệu dòng, câu lệnh SELECT station_id, SUM(amount) GROUP BY station_id sẽ tạo ra một cú "Full Table Scan", tạo bảng tạm (temp table) trên RAM và có thể khiến Database treo cứng trong vài phút. Nhưng với Elasticsearch, nhờ cấu trúc lưu trữ Doc Values, nó có thể nhai gọn 50 triệu dòng này và trả về kết quả trong chưa tới 50 mili-giây.
Hôm nay, chúng ta sẽ học cách dùng Aggregation để xây dựng API cấp số liệu cho biểu đồ doanh thu.
1. Bản chất của Aggregation: Buckets và Metrics
Trong SQL, chúng ta có GROUP BY và các hàm tính toán như SUM(), AVG(), COUNT(). Trong Elasticsearch Query DSL, khái niệm này được chia làm 2 loại rõ rệt:
- Buckets (Những cái xô): Tương đương với GROUP BY. ES sẽ tạo ra các "xô" dựa trên một tiêu chí nào đó (ví dụ: mỗi station_id là một xô).
- Metrics (Phép đo lường): Tương đương với SUM(), AVG(). Đây là phép toán được tính ở bên trong từng cái "xô" đó (ví dụ: tính tổng amount của các vé trong xô Ga Bến Thành).
2. Xây dựng Controller Thống kê (Analytics API)
Hãy mở Terminal và tạo một Controller mới chuyên lo việc báo cáo:
d-artisan make:controller Api/TransactionAnalyticsController
Mở file app/Http/Controllers/Api/TransactionAnalyticsController.php. Chúng ta sẽ kết hợp cả Query (Lọc thời gian) và Aggregation (Thống kê) trong cùng một request.
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Elastic\Elasticsearch\Client;
use Illuminate\Http\Request;
use Carbon\Carbon;
class TransactionAnalyticsController extends Controller
{
protected $elasticsearch;
public function __construct(Client $elasticsearch)
{
$this->elasticsearch = $elasticsearch;
}
public function revenueByStation(Request $request)
{
// Mặc định lấy dữ liệu của tháng hiện tại nếu không truyền params
$startDate = $request->input('start_date', Carbon::now()->startOfMonth()->toDateString());
$endDate = $request->input('end_date', Carbon::now()->endOfMonth()->toDateString());
$params = [
'index' => 'afc_transactions',
'body' => [
// QUAN TRỌNG: Đặt size = 0 vì chúng ta không cần lấy chi tiết từng giao dịch,
// chỉ quan tâm đến kết quả thống kê (tiết kiệm băng thông cực lớn).
'size' => 0,
// 1. Dùng Query Filter để khoanh vùng dữ liệu trước (chỉ tính giao dịch THÀNH CÔNG trong khoảng thời gian)
'query' => [
'bool' => [
'filter' => [
['term' => ['status' => 'success']],
['range' => [
'tapped_at' => [
'gte' => $startDate . ' 00:00:00',
'lte' => $endDate . ' 23:59:59'
]
]]
]
]
],
// 2. Định nghĩa Aggregations (Gom nhóm)
'aggs' => [
'group_by_station' => [
// Bucket: Nhóm theo station_id
'terms' => [
'field' => 'station_id',
'size' => 100 // Lấy tối đa 100 nhà ga
],
// Metric: Tính tổng trong từng Bucket
'aggs' => [
'total_revenue' => [
'sum' => [
'field' => 'amount'
]
]
]
]
]
]
];
try {
$response = $this->elasticsearch->search($params);
// Bóc tách kết quả từ mạng lưới JSON phức tạp của ES
$buckets = $response['aggregations']['group_by_station']['buckets'];
// Format lại dữ liệu cho Frontend dễ vẽ biểu đồ (VD: Chart.js, Recharts)
$chartData = array_map(function ($bucket) {
return [
'station_id' => $bucket['key'], // Tên/Mã Ga
'revenue' => $bucket['total_revenue']['value'], // Tổng doanh thu
'txn_count' => $bucket['doc_count'] // Số lượng giao dịch
];
}, $buckets);
return response()->json([
'success' => true,
'period' => "$startDate to $endDate",
'data' => $chartData
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Lỗi thống kê ES: ' . $e->getMessage()
], 500);
}
}
}
3. Đăng ký Route và Hưởng thành quả
Mở file routes/api.php và định nghĩa route cho API báo cáo này:
use App\Http\Controllers\Api\TransactionAnalyticsController;
Route::get('/analytics/revenue-by-station', [TransactionAnalyticsController::class, 'revenueByStation']);
Bây giờ, bạn hãy mở Postman hoặc trình duyệt gọi vào API: GET http://localhost:8000/api/analytics/revenue-by-station
Kết quả JSON trả về sẽ cực kỳ sạch sẽ và sẵn sàng để Frontend "đút" thẳng vào thư viện vẽ biểu đồ:
{
"success": true,
"period": "2026-07-01 to 2026-07-31",
"data": [
{
"station_id": "BEN_THANH",
"revenue": 45000000,
"txn_count": 3000
},
{
"station_id": "SUOI_TIEN",
"revenue": 28500000,
"txn_count": 1900
},
{
"station_id": "THAO_DIEN",
"revenue": 15000000,
"txn_count": 1000
}
]
}
4. Phân tích độ ngầu của đoạn code trên
- size => 0: Đây là bí quyết sống còn. Nếu bạn quên cái này, ES sẽ trả về toàn bộ mảng dữ liệu của hàng ngàn giao dịch làm nghẽn cổ chai mạng lưới. Bằng cách set size = 0, Payload trả về chỉ có vài KB chứa đúng các con số thống kê.
- filter đứng trước aggs: Chúng ta đã dùng bool -> filter để loại bỏ các giao dịch failed và nằm ngoài tháng hiện tại trước khi thực hiện tính tổng. Nhờ đó, Elasticsearch tốn ít sức lực hơn rất nhiều và được tận dụng Cache.
- Bản đồ terms lồng sum: Khái niệm lồng aggs vào trong aggs cho phép bạn tạo ra những báo cáo đa chiều khủng khiếp. Bạn thậm chí có thể lồng thêm một lớp date_histogram vào giữa để vẽ biểu đồ Doanh thu của từng nhà ga theo từng ngày trong tháng.
Tổng kết Bài 4
Bạn đã chính thức khai mở được quyền năng lớn nhất của Elasticsearch. Bằng cách sử dụng Aggregation, bạn đã giải phóng hoàn toàn gánh nặng cho MySQL Database. Các dashboard quản trị, báo cáo doanh thu real-time giờ đây hoạt động với độ trễ gần như bằng không.
Ở bài tiếp theo, chúng ta sẽ giải quyết một bài toán kiến trúc sống còn: Real-time Synchronization (Đồng bộ thời gian thực). Khi có một hành khách vừa quẹt thẻ và dữ liệu chui vào MySQL, làm cách nào để dữ liệu đó ngay lập tức "bắn" sang Elasticsearch mà không cần chờ chạy lại lệnh elastic:sync thủ công.
All rights reserved