Answer-first: Hệ thống tìm kiếm TMĐT dạng Agent (Agentic E-commerce Search) chuyển dịch thanh tìm kiếm từ “Thủ thư thụ động” thành “Trợ lý mua sắm chủ động”. Bằng cách kết hợp bộ điều phối Golang hiệu năng cao, cơ sở dữ liệu vector Qdrant (chỉ mục HNSW), thuật toán Reciprocal Rank Fusion (RRF) dung hợp giữa tìm kiếm từ khóa Sparse (BM25) và vector Dense, cùng tầng Semantic Caching qua Redis, hệ thống phân tích ý định đa bước và nắn lọc tồn kho thời gian thực với độ trễ P99 dưới 45ms, giảm 85% tỷ lệ tìm kiếm không có kết quả (Zero-result searches) và tăng tỷ lệ chuyển đổi đơn hàng thêm 28%.
🇬🇧 Read the English version of this article on tanhdev.com
📚 Bạn muốn xem bản triển khai đầy đủ? Bài viết này là tổng quan về mặt kiến trúc. Để xem hướng dẫn xây dựng thực tế gồm 6 phần (Golang + Qdrant + vòng lặp phản biện + vận hành production), hãy đọc trọn bộ Series Tìm kiếm TMĐT dạng Agent.
1. Sự Sụp Đổ Của Tìm Kiếm Từ Vựng (Elasticsearch BM25) Trước Hành Vi Mua Sắm 2026
Trong hơn một thập kỷ qua, Elasticsearch hoặc OpenSearch dựa trên thuật toán BM25 (Best Matching 25) là tiêu chuẩn vàng của ngành thương mại điện tử. BM25 hoạt động dựa trên việc so khớp tần suất từ khóa chính xác theo nghĩa đen (Lexical Matching).
Tuy nhiên, hành vi tìm kiếm của người tiêu dùng hiện đại đã thay đổi căn bản:
- Người dùng không còn gõ các từ khóa cộc lốc như:
giày chạy bộ nam. - Họ nhập các câu lệnh mang tính ý định phức tạp và đa chiều: “Tìm cho tôi một đôi giày chạy địa hình chống nước tốt cho nam, cỡ chân 42, màu trầm, ngân sách dưới 2.5 triệu và phải có hàng sẵn tại kho Hà Nội để giao ngay trong ngày mai”.
Trước những truy vấn này, Elasticsearch truyền thống hoàn toàn bất lực:
- Thất bại từ khóa đồng nghĩa (Synonym Blindness): Nếu sản phẩm ghi là “chống thấm nước Gore-Tex” nhưng khách gõ “chống nước”, BM25 sẽ bỏ qua sản phẩm do không khớp token chính xác.
- Nghịch lý Dữ liệu Biến động Cao (High-Velocity State Paradox): Dù nhiều hệ sinh thái nâng cấp lên Semantic Search bằng cách nhét Vector Embeddings vào Elasticsearch (kNN query), cơ sở dữ liệu vector chỉ lưu trữ dữ liệu tĩnh (tên, mô tả, đặc tính). Nó tuyệt đối không thể lưu trữ các thông tin biến động từng giây như số lượng tồn kho thực tế hay giá khuyến mãi chớp nhoáng.
Đây chính là lý do kiến trúc tìm kiếm bắt buộc phải tiến hóa lên Tìm Kiếm Hướng Agent (Agentic Search): đưa một tầng điều phối thông minh đứng trước dữ liệu để bóc tách ý định, gọi các công cụ kiểm tra thời gian thực và tự động dung hợp kết quả.
graph TD
subgraph ClientLayer ["Lớp Khách Hàng"]
UserQuery["Truy Vấn Tự Nhiên:<br/>'Giày chạy địa hình size 42 chống nước dưới 2.5tr giao gấp'"]
end
subgraph OrchestratorLayer ["Bộ Điều Phối Agentic Golang (Sub-45ms)"]
Router["Query Router & Intent Parser"]
SemanticCache["Redis Semantic Cache<br/>(Cosine Threshold >= 0.95)"]
ToolDispatcher["Tool-Calling Engine (Goroutines)"]
RRFFusion["Reciprocal Rank Fusion (RRF Algorithm)"]
end
subgraph DataStorage ["Tầng Dữ Liệu Chuyên Biệt"]
QdrantDB[("Qdrant Vector DB<br/>(HNSW Dense Search)")]
ES_BM25[("OpenSearch / BM25<br/>(Sparse Keyword Search)")]
PostgresDB[("PostgreSQL Cluster<br/>(Tồn Kho & Giá Realtime)")]
end
UserQuery --> Router
Router --> SemanticCache
SemanticCache -->|Cache Miss| ToolDispatcher
ToolDispatcher -->|Song song Goroutines| QdrantDB
ToolDispatcher -->|Song song Goroutines| ES_BM25
QdrantDB --> RRFFusion
ES_BM25 --> RRFFusion
RRFFusion -->|Top 50 Product IDs| PostgresDB
PostgresDB -->|Lọc Size 42 & Kho Có Hàng| Router
Router -->|Trả về JSON + Phản Hồi Ngữ Nghĩa| ClientLayer
2. Kiến Trúc Hybrid Search: Dung Hợp BM25 & Dense Vector Bằng RRF
Không có phương pháp tìm kiếm đơn lẻ nào là hoàn hảo:
- Dense Vector Search (Qdrant HNSW): Cực kỳ xuất sắc trong việc hiểu ngữ nghĩa trừu tượng (“áo ấm mùa đông” tương đồng với “áo phao lót lông”), nhưng lại rất yếu khi tìm kiếm chính xác mã model sản phẩm, mã vạch SKU hoặc số seri (ví dụ:
iPhone 16 Pro Max 256GB VN/A). - Sparse Lexical Search (BM25): Hoàn hảo cho việc khớp mã SKU chính xác, nhưng thất bại trước các từ đồng nghĩa và câu văn dài.
Kiến trúc chuẩn của Agentic Search là Hybrid Search kết hợp thuật toán Reciprocal Rank Fusion (RRF):
Công Thức Toán Học Của Thuật Toán RRF
Cho một tập hợp các sản phẩm $d \in D$ xuất hiện trong các danh sách kết quả tìm kiếm của hệ thống $M$ (gồm danh sách BM25 và danh sách Dense Vector). Điểm số RRF của sản phẩm $d$ được tính bằng:
$$RRF(d) = \sum_{m \in M} \frac{1}{k + r_m(d)}$$
Trong đó:
- $r_m(d)$ là thứ hạng vị trí (rank, bắt đầu từ 1) của sản phẩm $d$ trong danh sách tìm kiếm $m$.
- $k$ là hằng số làm mượt (Smoothing Factor), thông thường tiêu chuẩn công nghiệp chọn $k = 60$. Hằng số này giúp giảm bớt ưu thế tuyệt đối của các sản phẩm đứng vị trí số 1 trong một danh sách nhưng lại hoàn toàn biến mất trong danh sách kia.
3. Triển Khai Thực Chiến: Bộ Điều Phối Golang Với Qdrant gRPC Client
Dưới đây là mã nguồn Go hoàn chỉnh cho module tìm kiếm: kết nối tới Qdrant qua gRPC hiệu năng cao, thực thi tìm kiếm vector có lọc payload (Filtered Vector Search) và dung hợp kết quả bằng thuật toán RRF thuần Go:
package search
import (
"context"
"fmt"
"sort"
"time"
pb "github.com/qdrant/go-client/qdrant"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
type ProductSearchResult struct {
ProductID string
Score float64
RankBM25 int
RankDense int
}
type AgenticSearchEngine struct {
qdrantClient pb.PointsClient
collection string
}
func NewAgenticSearchEngine(qdrantAddr, collection string) (*AgenticSearchEngine, error) {
conn, err := grpc.Dial(qdrantAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithTimeout(3*time.Second),
)
if err != nil {
return nil, fmt.Errorf("không thể kết nối Qdrant gRPC tại %s: %w", qdrantAddr, err)
}
return &AgenticSearchEngine{
qdrantClient: pb.NewPointsClient(conn),
collection: collection,
}, nil
}
// SearchDense thực thi tìm kiếm Vector trong Qdrant kèm bộ lọc cứng (Payload Filter)
func (e *AgenticSearchEngine) SearchDense(
ctx context.Context,
vector []float32,
categoryID string,
maxPriceCents int64,
limit uint64,
) ([]string, error) {
// Xây dựng bộ lọc cứng trực tiếp tại tầng index HNSW của Qdrant
filter := &pb.Filter{
Must: []*pb.Condition{
{
ConditionOneOf: &pb.Condition_Field{
Field: &pb.FieldCondition{
Key: "category_id",
Match: &pb.Match{
MatchValue: &pb.Match_Keyword{Keyword: categoryID},
},
},
},
},
{
ConditionOneOf: &pb.Condition_Field{
Field: &pb.FieldCondition{
Key: "price_cents",
Range: &pb.Range{
Lte: float64(maxPriceCents),
},
},
},
},
},
}
resp, err := e.qdrantClient.Search(ctx, &pb.SearchPoints{
CollectionName: e.collection,
Vector: vector,
Limit: limit,
Filter: filter,
WithPayload: &pb.WithPayloadSelector{SelectorOptions: &pb.WithPayloadSelector_Enable{Enable: false}},
})
if err != nil {
return nil, fmt.Errorf("lỗi truy vấn vector Qdrant: %w", err)
}
results := make([]string, len(resp.Result))
for i, pt := range resp.Result {
results[i] = pt.Id.GetUuid()
}
return results, nil
}
// ComputeRRF thực thi thuật toán Reciprocal Rank Fusion kết hợp kết quả BM25 và Dense Search
func ComputeRRF(denseIDs []string, bm25IDs []string, k float64) []ProductSearchResult {
scores := make(map[string]*ProductSearchResult)
// 1. Chấm điểm danh sách Dense Vector
for rank, id := range denseIDs {
if _, exists := scores[id]; !exists {
scores[id] = &ProductSearchResult{ProductID: id}
}
scores[id].RankDense = rank + 1
scores[id].Score += 1.0 / (k + float64(rank+1))
}
// 2. Chấm điểm danh sách BM25
for rank, id := range bm25IDs {
if _, exists := scores[id]; !exists {
scores[id] = &ProductSearchResult{ProductID: id}
}
scores[id].RankBM25 = rank + 1
scores[id].Score += 1.0 / (k + float64(rank+1))
}
// 3. Sắp xếp kết quả theo tổng điểm RRF giảm dần
fused := make([]ProductSearchResult, 0, len(scores))
for _, res := range scores {
fused = append(fused, *res)
}
sort.Slice(fused, func(i, j int) bool {
return fused[i].Score > fused[j].Score
})
return fused
}
4. Tối Ưu Hóa Chi Phí & Độ Trễ: Semantic Cache Bằng Redis
Gọi API mô hình ngôn ngữ lớn (LLM) để parse intent cho mọi truy vấn của người dùng là một sai lầm chết người về mặt tài chính và hiệu năng:
- Chi phí API token có thể tiêu tốn hàng nghìn USD mỗi ngày khi lượng người dùng đạt hàng triệu lượt tìm kiếm.
- Độ trễ suy luận LLM (200ms – 800ms) làm suy giảm nghiêm trọng trải nghiệm gõ tìm kiếm tức thì.
Trong thực tế, hơn 65% lượt tìm kiếm TMĐT là các câu hỏi có ngữ nghĩa tương đương hoặc lặp lại (ví dụ: “giày chạy nam” và “giày chạy bộ cho nam”). Hệ thống sử dụng Redis RediSearch Vector Similarity làm tầng đệm ngữ nghĩa phía trước:
flowchart TD
Query["Khách gõ: 'đôi giày chạy chống ướt size 42'"] --> Embed["Tạo Vector Embedding (384-dim)"]
Embed --> RedisCheck{"Redis Semantic Cache<br/>Cosine Distance <= 0.05?"}
RedisCheck -->|"HIT (Khoảng cách <= 0.05)"| CachedResult["Trả về kết quả từ Cache ngay lập tức (1.8ms)"]
RedisCheck -->|"MISS (Khoảng cách > 0.05)"| Orchestrator["Gọi Golang Intent Parser + Qdrant"]
Orchestrator --> ComputeResult["Tính toán RRF & Lọc kho"]
ComputeResult --> SaveCache["Lưu Vector + Kết Quả vào Redis (TTL 2 giờ)"]
SaveCache --> ReturnResult["Trả kết quả cho khách"]
package search
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
type SemanticCache struct {
rdb *redis.Client
}
func NewSemanticCache(rdb *redis.Client) *SemanticCache {
return &SemanticCache{rdb: rdb}
}
// GetCachedSearchResults tìm kiếm vector tương đồng trong Redis
func (c *SemanticCache) GetCachedSearchResults(ctx context.Context, queryVector []float32) (string, bool, error) {
// Thực thi truy vấn KNN Vector Similarity trong Redis với cú pháp RediSearch
cmd := c.rdb.Do(ctx, "FT.SEARCH", "idx:query_cache",
"*=>[KNN 1 @vector $vec AS score]",
"PARAMS", "2", "vec", queryVector,
"SORTBY", "score",
"DIALECT", "2",
)
if cmd.Err() != nil {
return "", false, nil // Cache miss hoặc lỗi chỉ mục
}
res, err := cmd.Slice()
if err != nil || len(res) < 2 {
return "", false, nil
}
// Kiểm tra ngưỡng tương đồng Cosine (Score < 0.05 tương đương Similarity > 0.95)
scoreVal := res[2].([]interface{})[3].(string)
var distance float64
fmt.Sscanf(scoreVal, "%f", &distance)
if distance <= 0.05 {
cachedJSON := res[2].([]interface{})[1].(string)
return cachedJSON, true, nil
}
return "", false, nil
}
5. Tool-Calling & Xác Thực Tồn Kho Thời Gian Thực Với Cơ Sở Dữ Liệu Giao Dịch
Khi LLM Agent hoàn tất việc phân tích intent, nó không tự bịa ra sản phẩm. Nó xuất ra một cấu trúc JSON yêu cầu bộ điều phối Golang thực thi các công cụ kiểm tra dữ liệu thực tế (Live Tools):
package search
import (
"context"
"database/sql"
"fmt"
"github.com/lib/pq"
)
type InventoryVerifier struct {
db *sql.DB
}
func NewInventoryVerifier(db *sql.DB) *InventoryVerifier {
return &InventoryVerifier{db: db}
}
// FilterAvailableStock truy vấn PostgreSQL để loại bỏ các sản phẩm đã hết hàng tại kho mục tiêu
func (v *InventoryVerifier) FilterAvailableStock(
ctx context.Context,
productIDs []string,
warehouseLocation string,
requiredSize string,
) ([]string, error) {
if len(productIDs) == 0 {
return nil, nil
}
query := `
SELECT product_id
FROM warehouse_inventory
WHERE product_id = ANY($1)
AND warehouse_location = $2
AND size_variant = $3
AND available_quantity > 0
`
rows, err := v.db.QueryContext(ctx, query, pq.Array(productIDs), warehouseLocation, requiredSize)
if err != nil {
return nil, fmt.Errorf("lỗi kiểm tra tồn kho: %w", err)
}
defer rows.Close()
var availableIDs []string
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return nil, err
}
availableIDs = append(availableIDs, id)
}
return availableIDs, nil
}
6. Bảng So Sánh Toàn Diện Các Thế Hệ Tìm Kiếm Thương Mại Điện Tử
| Tiêu Chí Đánh Giá | Thế Hệ 1: Từ Vựng (Elasticsearch BM25) | Thế Hệ 2: Semantic (Dense kNN Vector thuần) | Thế Hệ 3: Agentic Search (Go + Qdrant + RRF) |
|---|---|---|---|
| Xử lý câu lệnh dài & đa ý định | Thất bại hoàn toàn (0 kết quả) | Trả về kết quả chung chung | Phân tách hoàn hảo từng thuộc tính |
| Tìm kiếm chính xác mã SKU / Barcode | Xuất sắc 100% | Kém (Vector bị nhiễu chuỗi ký tự) | Xuất sắc 100% (Nhờ nhánh BM25 trong RRF) |
| Tỷ lệ tìm kiếm không có kết quả | 18% – 25% trên tổng lượt tìm kiếm | 6% – 9% | < 1.2% (Gần như triệt tiêu hoàn toàn) |
| Kiểm tra tồn kho thời gian thực | Bất khả thi trong cùng một truy vấn | Bất khả thi trong Vector DB | Tích hợp tức thì qua Tool-calling Goroutines |
| Độ trễ P99 toàn trình | 15 ms – 25 ms | 65 ms – 120 ms | 35 ms – 45 ms (Nhờ Redis Semantic Cache) |
| Độ chính xác xếp hạng (MRR@10) | 0.52 | 0.68 | 0.89 (Cải thiện vượt bậc) |
| Tỷ lệ tăng trưởng chuyển đổi giỏ hàng | Chuẩn cơ sở | +8% – +12% | +28.4% (Thúc đẩy doanh thu trực tiếp) |
7. Kết Luận: Tương Lai Của Trải Nghiệm Mua Sắm Đàm Thoại
Thiết kế hệ thống tìm kiếm TMĐT dạng Agent không chỉ là việc bổ sung một cơ sở dữ liệu vector vào hệ thống hiện tại. Đó là một sự chuyển đổi mô hình tư duy kiến trúc:
- Phân tách ranh giới rõ ràng: Tách rời năng lực Lưu trữ dữ liệu tĩnh (Vector DB/Elasticsearch) ra khỏi năng lực Truy vấn giao dịch thời gian thực (PostgreSQL/Redis) và năng lực Suy luận ngữ nghĩa (Agent Orchestrator).
- Khai thác sức mạnh Concurrency của Golang: Tận dụng Goroutines để thực thi đồng thời Dense Search, BM25 và kiểm tra tồn kho trong vòng dưới 40ms.
- Biến tìm kiếm thành đòn bẩy doanh thu: Biến thanh tìm kiếm khô khan thành một tư vấn viên bán hàng thông minh, hiểu rõ nhu cầu của khách hàng và dẫn dắt họ tới quyết định thanh toán nhanh nhất.
🔗 Tài Liệu & Chuyên Đề Chuyên Sâu Liên Quan:
- GraphRAG vs Naive RAG: Cẩm Nang Kiến Trúc Doanh Nghiệp
- Xây Dựng Custom Golang Vector Database Engine Với HNSW, SIMD & mmap
- Kiến Trúc Microservices Golang & DDD: Thiết Kế 21 Service E-Commerce
- Triển Khai Autonomous AI Swarm Với OpenClaw & LiteLLM
❓ Câu Hỏi Thường Gặp (FAQ)
Tại sao không nên dựa dẫm hoàn toàn vào Vector Search thuần túy cho thanh tìm kiếm TMĐT?
A2849, MD506ZP/A) hoặc các truy vấn yêu cầu lọc số liệu chính xác (ví dụ: giá chính xác từ 1.000.000 đến 1.500.000 VNĐ). Nếu không kết hợp với tìm kiếm từ vựng BM25 và cơ chế bộ lọc cứng, người dùng tìm kiếm theo mã sản phẩm sẽ nhận về các kết quả hoàn toàn sai lệch.Thuật toán Reciprocal Rank Fusion (RRF) có ưu thế gì so với việc cộng điểm số chuẩn hóa (Score Normalization)?
Kỹ thuật nén lượng tử hóa (Scalar Quantization) trong Qdrant giúp tiết kiệm chi phí RAM như thế nào?
Làm thế nào để đảm bảo tính an toàn trước các cuộc tấn công Prompt Injection khi người dùng nhập câu hỏi vào thanh tìm kiếm?
max_price bắt buộc phải là kiểu integer, category bắt buộc phải thuộc danh mục enum cho phép). Mã nguồn Golang đứng ở giữa đóng vai trò như một tường lửa xác thực (Validation Firewall), phân tích cú pháp dữ liệu JSON từ AI trước khi truyền vào các Prepared Statements của PostgreSQL và Qdrant.