Báo cáo Lựa chọn Tìm kiếm Ngữ nghĩa
1. Tổng quan
Báo cáo này đánh giá các giải pháp tìm kiếm ngữ nghĩa cho hệ thống BANA POS. Mục tiêu là nâng cấp tìm kiếm Typesense dựa trên từ khóa hiện có để hỗ trợ hiểu ngữ nghĩa — cho phép truy vấn dựa trên ý định, đối sánh từ đồng nghĩa, tìm kiếm đa ngôn ngữ (Tiếng Việt + Tiếng Anh), gợi ý sản phẩm và tính năng "tương tự sản phẩm này".
Hiện trạng: Typesense v2.1.0 với Kafka CDC (Debezium) đồng bộ từ PostgreSQL. 6 collection được index: products, merchants, organizers, categories, devices, sale-channels.
Mục tiêu: Hybrid search kết hợp tìm kiếm theo từ khóa (BM25) + ngữ nghĩa (vector/embedding) trên tất cả các collection với hỗ trợ Tiếng Việt + Tiếng Anh xuất sắc.
Ngày đánh giá: Tháng 02/2026
2. Kiến trúc Tìm kiếm Hiện tại
2.1. Luồng Dữ liệu
2.2. Các Collection được Index
| Collection | Entity | Query By | Đa ngôn ngữ |
|---|---|---|---|
| products | Product | identifier, info.name.en, info.name.vi, info.description.en, info.description.vi | Có |
| merchants | Merchant | name.en, name.vi, slug, identifier, status, type, currency | Có |
| organizers | Organizer | name.en, name.vi, description.en, description.vi | Có |
| categories | Category | name.en, name.vi | Có |
| devices | Device | name, identifier, status | Không |
| sale-channels | SaleChannel | name.en, name.vi | Có |
2.3. Bối cảnh Hạ tầng
| Thành phần | Phiên bản | Trạng thái | Liên quan đến tìm kiếm ngữ nghĩa |
|---|---|---|---|
| PostgreSQL | 18 Alpine | Đang chạy | Nguồn dữ liệu chính (source of truth). Có sẵn extension pgvector |
| Typesense | 2.1.0 | Đang chạy | Đã triển khai. Hỗ trợ vector search nguyên gốc |
| Redis | Mới nhất | Đang chạy | Cache (DB 0), BullMQ (DB 0), Pub/Sub (DB 0), WebSocket (DB 4). Redis 8+ hỗ trợ vector search |
| Kafka + Debezium | 3.x + 2.4 | Đang chạy | Pipeline CDC cho đồng bộ thời gian thực |
| Drizzle ORM | 0.45.1 | Đang chạy | Hỗ trợ pgvector nguyên gốc (vector(), cosineDistance()) |
| Bun | >= 1.3.2 | Đang chạy | Runtime. Tương thích gần 100% với Node.js |
| BullMQ | 5.14.3 | Đang chạy | Hạ tầng queue (finance, payment). Có thể tái sử dụng cho pipeline embedding |
2.4. Khoảng cách Năng lực
| Năng lực | Hiện tại (Từ khóa) | Mục tiêu (Ngữ nghĩa) |
|---|---|---|
| Đối sánh chính xác (SKU, mã vạch) | Có | Có (đường keyword) |
| Khả năng dung sai lỗi chính tả | Có | Có |
| Hiểu từ đồng nghĩa | Không | Có |
| Truy vấn theo ý định | Không | Có |
| "Tương tự sản phẩm này" | Không | Có |
| Truy vấn ngôn ngữ tự nhiên | Không | Có |
| Hiểu ngữ nghĩa Tiếng Việt | Không | Có |
| Gợi ý sản phẩm | Không | Có |
| Tìm kiếm xuyên ngôn ngữ (truy vấn EN → kết quả VI) | Không | Có |
3. Đánh giá Vector Database
3.1. Các Ứng viên
| # | Giải pháp | Loại | Ngôn ngữ | License | GitHub Stars | Trạng thái tại BANA |
|---|---|---|---|---|---|---|
| 1 | Typesense | Search engine + Vector | C++ | GPL-3 | 22k+ | Đã triển khai |
| 2 | PostgreSQL pgvector | Extension | C | PostgreSQL | 14k+ | DB đã triển khai |
| 3 | Redis Stack | In-memory DB + Vector | C | RSALv2 | 67k+ | Đã triển khai |
| 4 | Qdrant | Vector DB chuyên dụng | Rust | Apache-2 | 23k+ | Chưa triển khai |
| 5 | Meilisearch | Search engine + Vector | Rust | MIT | 49k+ | Chưa triển khai |
| 6 | Weaviate | Vector DB + Vectorizers | Go | BSD-3 | 14k+ | Chưa triển khai |
| 7 | LanceDB | Embedded vector DB | Rust | Apache-2 | 5k+ | Chưa triển khai |
| 8 | Milvus/Zilliz | Vector DB phân tán | Go/C++ | Apache-2 | 40k+ | Chưa triển khai |
| 9 | Elasticsearch | Search engine + Vector | Java | SSPL/ELv2 | 72k+ | Chưa triển khai |
| 10 | OpenSearch | Search engine + Vector | Java | Apache-2 | 10k+ | Chưa triển khai |
| 11 | Pinecone | Managed vector DB | - | Proprietary | N/A | Chưa triển khai |
| 12 | ChromaDB | Embedding DB | Python/Rust | Apache-2 | 18k+ | Chưa triển khai |
| 13 | Vespa | Search platform | Java/C++ | Apache-2 | 6k+ | Chưa triển khai |
3.2. Ma trận Tính năng
| Tính năng | Typesense | pgvector | Redis Stack | Qdrant | Meilisearch | Weaviate | Milvus | Elasticsearch |
|---|---|---|---|---|---|---|---|---|
| Vector Index (HNSW) | Có | Có | Có | Có | Có (Arroy) | Có | Có | Có (Lucene) |
| Hybrid Search | Native (rank fusion) | Manual (SQL) | KNN + filter | Dense+sparse fusion | semanticRatio | Alpha-blend | Native | Retriever API |
| Embedding tích hợp sẵn | Có (S-BERT, E5) | Không (BYO) | Không (BYO) | Không (BYO) | Có (OpenAI, HF) | Có (20+ modules) | Không (BYO) | Có (ELSER) |
| API Embedding bên ngoài | OpenAI, PaLM | N/A | N/A | N/A | OpenAI, Cohere, HF | OpenAI, Cohere, HF | N/A | OpenAI, HF |
| BYO Vectors | Có (float[]) | Có (vector) | Có (float32/64) | Có | Có | Có | Có | Có |
| Lọc Metadata | Đầy đủ | Đầy đủ (SQL) | Tối đa 10 attrs | Đầy đủ (payload) | Faceted | Đầy đủ (where) | Đầy đủ | Đầy đủ |
| Faceted Search | Có | Không | Không | Không | Có | Không | Không | Có |
| Multi-tenancy | Filter-based | RLS/WHERE | Tag-based | Tiered shards | Tenant tokens | Shard-per-tenant | DB/collection/partition | Index-per-tenant |
| TypeScript SDK | Tuyệt vời | Drizzle native | Client có sẵn | Official REST | Tốt | v3 client | Node.js SDK | Official |
| Tương thích Bun | Đã xác nhận | Đã xác nhận | Đã xác nhận | Dự kiến | Dự kiến | Dự kiến | Dự kiến | Dự kiến |
| Self-hosted Docker | Có | Extension | Có | Có | Có | Có (Compose) | Có (+ etcd) | Có |
| Chỉ Cloud | Không | Không | Không | Không | Không | Không | Không | Không |
3.3. Benchmark Hiệu năng
| Giải pháp | RPS (1M vectors) | P50 Latency | P99 Latency | Memory (100K x 1024d) | Tốc độ Insert |
|---|---|---|---|---|---|
| Typesense | ~5K-10K | <10ms | <50ms | ~600MB + 2-6GB model | Real-time (CDC) |
| pgvector | ~471 QPS (99% recall) | <50ms | <100ms | ~600MB (HNSW) | Real-time (SQL) |
| Redis Stack | ~3.4x Qdrant | ~3ms | <10ms | ~600MB (in-memory) | ~15K vec/sec |
| Qdrant | Cao | ~31ms | ~37ms | ~600MB (lượng tử hóa xuống ~60MB) | ~50K vec/sec |
| Meilisearch | ~5K-10K | <50ms | <100ms | ~35x kích thước dữ liệu (RAM!) | Batch |
| Weaviate | ~1.7x ít hơn Redis | ~10ms | <50ms | ~3GB cho 1M vectors | Trung bình |
| Milvus | Cao nhất trong open-source | <10ms | <30ms | ~600MB | ~50K vec/sec |
| Elasticsearch | ~5x nhanh hơn OpenSearch | <50ms | <100ms | Yêu cầu cluster 128-256GB RAM | Trung bình |
3.4. Phân tích Chi phí (Self-Hosted, Hàng tháng)
| Giải pháp | Hạ tầng mới | Overhead RAM | Độ phức tạp vận hành | Chi phí Hàng tháng |
|---|---|---|---|---|
| Typesense (nâng cấp) | Không | +2-6GB (embedding model) | Không (đã chạy) | $0 |
| pgvector | Không (extension) | +600MB (HNSW index) | Tối thiểu (ALTER TABLE) | $0 |
| Redis Stack (nâng cấp lên v8+) | Không | +600MB (vector index) | Tối thiểu (Redis có sẵn) | $0 |
| Qdrant | 1 Docker container | 2-8GB | Trung bình (service mới) | $50-150 |
| Meilisearch | 1 Docker container | 4-16GB (nặng!) | Trung bình | $50-200 |
| Weaviate | Docker Compose (multi-container) | 8-16GB | Cao (modules, config) | $100-300 |
| Milvus | Docker + etcd + MinIO | 8-16GB | Cao (phân tán) | $150-400 |
| Elasticsearch | Docker cluster (3+ nodes) | 128-256GB | Rất cao (JVM, ops) | $300-1000+ |
3.5. Các Lý do Loại trừ Quan trọng cho BANA
| Ứng viên | Lý do loại trừ | Mức độ nghiêm trọng |
|---|---|---|
| Elasticsearch | Yêu cầu cluster 128-256GB RAM. Dựa trên JVM. Overhead vận hành lớn. Quá mức cần thiết cho POS search. | NGHIÊM TRỌNG |
| OpenSearch | Tương tự Elasticsearch. Chậm hơn ES 5x cho vector search. | NGHIÊM TRỌNG |
| Vespa | Không có TypeScript/JavaScript SDK. Cấu hình dựa trên XML. Phức tạp ở quy mô internet, hoàn toàn quá mức. | NGHIÊM TRỌNG |
| Pinecone | Chỉ có cloud, không có lựa chọn self-hosted. Định giá theo mức tiêu thụ (đắt ở quy mô lớn). Khóa nhà cung cấp. | CAO |
| ChromaDB | Chỉ single-node. HNSW in-memory (10M x 1536d = 60GB RAM). Không có multi-tenancy. Chưa sẵn sàng production ở quy mô lớn. | CAO |
| Milvus | Yêu cầu etcd + MinIO cho chế độ phân tán. Phức tạp không tương xứng với quy mô POS (<1M sản phẩm). | TRUNG BÌNH |
| Weaviate | Multi-container Docker Compose. 8-16GB RAM. Cấu hình module phức tạp. Đường cong học tập dốc hơn. | TRUNG BÌNH |
| Meilisearch | Kiến trúc single-node. Sử dụng RAM = 35x kích thước dữ liệu. Không scale ngang. | TRUNG BÌNH |
3.6. Ma trận Chấm điểm
Chấm điểm có trọng số (1-10, cao hơn = tốt hơn cho BANA):
| Tiêu chí (Trọng số) | Typesense | pgvector | Redis Stack | Qdrant | Meilisearch | Weaviate | Milvus |
|---|---|---|---|---|---|---|---|
| Mức độ Tích hợp (25%) | 10 | 10 | 9 | 5 | 4 | 4 | 3 |
| Hybrid Search (20%) | 9 | 6 | 7 | 8 | 8 | 9 | 8 |
| TypeScript/Bun SDK (15%) | 10 | 10 | 9 | 7 | 8 | 7 | 6 |
| Chi phí Vận hành (10%) | 10 | 10 | 9 | 7 | 7 | 6 | 5 |
| Hiệu năng (10%) | 8 | 8 | 10 | 9 | 7 | 7 | 9 |
| Multi-tenancy (5%) | 7 | 9 | 6 | 8 | 7 | 9 | 10 |
| Khả năng mở rộng (5%) | 7 | 7 | 6 | 9 | 5 | 8 | 10 |
| Hỗ trợ Tiếng Việt (10%) | 7 | 7 | 7 | 7 | 7 | 7 | 7 |
| Tổng có Trọng số | 9.05 | 8.45 | 8.30 | 6.90 | 6.35 | 6.35 | 5.95 |
3.7. Phân tích Vòng Chung kết
Nâng cấp Vector cho Typesense (Điểm: 9.05) — NGƯỜI CHIẾN THẮNG
Vì sao #1: Đã được triển khai cùng đồng bộ CDC. Thêm tìm kiếm ngữ nghĩa = thêm trường embed vào schema collection hiện có. Không cần hạ tầng mới, không có service mới.
Điểm mạnh:
- Hybrid search rank fusion native với tham số
alphacấu hình được (0 = thuần keyword, 1 = thuần ngữ nghĩa) - Các mô hình embedding tích hợp sẵn:
ts/all-MiniLM-L12-v2,ts/e5-small,ts/distiluse-base-multilingual-cased-v2 - Hỗ trợ API bên ngoài: OpenAI, Google PaLM được hỗ trợ nguyên gốc
- BYO embedding tính sẵn qua trường
float[] - Pipeline Kafka CDC hiện có tiếp tục hoạt động không thay đổi
query_bycủa Typesense hỗ trợ trộn các trường text và trường embedding trong một truy vấnrerank_hybrid_matches: trueđể cải thiện chất lượng kết quả- Chi phí: $0 thêm
Điểm yếu:
- Mô hình đa ngôn ngữ tích hợp sẵn (
distiluse) đã cũ, chất lượng Tiếng Việt ở mức trung bình - Tải mô hình embedding cần thêm 2-6GB RAM
- Không được thiết kế cho quy mô tỷ vector (đủ cho POS với <10M sản phẩm)
Hybrid search hoạt động trong Typesense như thế nào:
{
"q": "laptop gaming",
"query_by": "name_en,name_vi,description_en,embedding",
"prefix": "true,true,true,false",
"vector_query": "embedding:([], id: *, alpha: 0.3)",
"filter_by": "status:=active && merchantId:=12345"
}PostgreSQL pgvector (Điểm: 8.45) — BỔ SUNG
Vì sao #2: Không cần hạ tầng mới. Drizzle ORM có hỗ trợ pgvector nguyên gốc với kiểu cột vector() và các hàm khoảng cách (cosineDistance(), l2Distance(), innerProduct()).
Điểm mạnh:
- Không có service mới — chỉ cần kích hoạt extension pgvector trên PostgreSQL hiện có
- Tích hợp Drizzle ORM mượt mà:typescript
import { vector, index } from 'drizzle-orm/pg-core'; const products = pgTable('products', { embedding: vector('embedding', { dimensions: 1024 }), }); // Query db.select().from(products).orderBy(cosineDistance(products.embedding, queryVector)).limit(10); - Lọc SQL đầy đủ kết hợp với vector search (merchantId, organizerId, category, v.v.)
- Kiểu
halfvectiết kiệm 50% lưu trữ mà không mất recall - HNSW index cho truy vấn dưới 100ms với <10M vectors
- pgvectorscale đạt 471 QPS với 99% recall trên 50M vectors
- Index real-time (vector được index ngay khi INSERT/UPDATE)
Điểm yếu:
- Không có rank fusion tích hợp — hybrid search (keyword + vector) phải triển khai ở tầng ứng dụng
- Không tạo embedding tích hợp (chỉ BYO)
- HNSW index tiêu thụ bộ nhớ đáng kể với dataset lớn
Phù hợp nhất cho: Gợi ý sản phẩm, "tương tự sản phẩm này", truy vấn tương tự dạng phân tích, tra cứu vector xuyên service.
Redis Stack Vector Search (Điểm: 8.30) — REAL-TIME
Vì sao #3: Đã chạy Redis cho cache, BullMQ và pub/sub. Redis 8+ bao gồm vector search nguyên gốc (trước đây yêu cầu Redis Stack module).
Điểm mạnh:
- Latency P50 3ms — vector search nhanh nhất trong tất cả các ứng viên
- Đã được tích hợp sâu (caching, queues, pub/sub, WebSocket)
- Sử dụng client Redis hiện có (
node-redis) - Hỗ trợ index HNSW, FLAT và SVS-VAMANA
- KNN + pre-filter trên các trường TAG, TEXT, NUMERIC, GEO
- Hiệu năng in-memory lý tưởng cho gợi ý checkout POS thời gian thực
Điểm yếu:
- Tối đa 10 thuộc tính cho mỗi vector index
- Tất cả trong bộ nhớ — chi phí RAM tăng tuyến tính
- Không có tạo embedding tích hợp
- Hybrid search hạn chế (không có rank fusion thực sự)
Phù hợp nhất cho: Tra cứu tương tự thời gian thực tại POS ("khách hàng cũng đã mua"), gợi ý checkout, cache tìm kiếm độ trễ cực thấp.
4. Đánh giá Mô hình Embedding
4.1. Yêu cầu Quan trọng: Tiếng Việt + Tiếng Anh
BANA sử dụng các trường i18n ({ en: string, vi: string }) cho products, merchants, organizers và categories. Mô hình embedding bắt buộc phải xử lý hiệu quả cả Tiếng Việt và Tiếng Anh. Hỗ trợ Tiếng Việt kém là lý do loại trừ.
4.2. Các Ứng viên
| # | Mô hình | Nhà cung cấp | Dimensions | Tham số | Max Tokens | Ngôn ngữ | Loại |
|---|---|---|---|---|---|---|---|
| 1 | BGE-M3 | BAAI | 1024 | 568M | 8,192 | 100+ | Open-source |
| 2 | jina-embeddings-v3 | Jina AI | 1024 (32-1024) | 570M | 8,192 | 89 | API + Self-host |
| 3 | Typesense distiluse-multilingual | Typesense built-in | 512 | ~135M | 512 | Đa ngôn ngữ | Tích hợp sẵn |
| 4 | Cohere Embed v4 | Cohere | 1536 (cấu hình được) | - | 128K | 100+ | Chỉ API |
| 5 | OpenAI text-embedding-3-small | OpenAI | 1536 (cấu hình được) | - | 8,191 | Đa ngôn ngữ | Chỉ API |
| 6 | multilingual-e5-large | Microsoft | 1024 | 560M | 512 | 100+ | Open-source |
| 7 | gte-multilingual-base | Alibaba | 768 | ~300M | 8,192 | 70+ | Open-source |
| 8 | Google gemini-embedding-001 | 3072 (cấu hình được) | - | 8,192 | 100+ | API | |
| 9 | nomic-embed-text-v2-moe | Nomic AI | 768 | 475M (305M active) | 8,192 | ~100 | Open-source |
| 10 | Voyage voyage-3.5 | Voyage AI | 1024 | - | 32K | Đa ngôn ngữ | API |
| 11 | OpenAI text-embedding-3-large | OpenAI | 3072 (cấu hình được) | - | 8,191 | Đa ngôn ngữ | Chỉ API |
| 12 | mxbai-embed-large | Mixedbread AI | 1024 | 335M | 512 | Tập trung Tiếng Anh | Open-source |
| 13 | all-MiniLM-L6-v2 | Sentence-Transformers | 384 | 22M | 512 | Chỉ Tiếng Anh | Open-source |
| 14 | AWS Titan V2 | AWS Bedrock | 1024 | - | 8,192 | Đa ngôn ngữ | API |
4.3. Chất lượng Tiếng Việt
| Mô hình | Hỗ trợ Tiếng Việt rõ ràng | MTEB Multilingual Rank | Đánh giá Chất lượng Tiếng Việt |
|---|---|---|---|
| jina-embeddings-v3 | Top 30 ngôn ngữ | Cao | Xuất sắc |
| BGE-M3 | 100+ ngôn ngữ (huấn luyện trên dữ liệu 170+ ngôn ngữ) | Cao | Xuất sắc |
| Cohere Embed v4 | 100+ ngôn ngữ | #1 đa ngôn ngữ | Tốt |
| multilingual-e5-large | Mạnh trên Mr. TyDi đa ngôn ngữ | Tốt | Tốt |
| gte-multilingual-base | 70+ ngôn ngữ | SOTA đa ngôn ngữ | Tốt |
| gemini-embedding-001 | 100+ ngôn ngữ | #1 MTEB đa ngôn ngữ | Tốt |
| nomic-embed-text-v2-moe | ~100 ngôn ngữ | Tốt | Trung bình |
| OpenAI text-embedding-3-small/large | Đa ngôn ngữ nhưng không tối ưu | Tốt | Trung bình |
| Typesense distiluse-multilingual | Đa ngôn ngữ (mô hình cũ hơn) | Trung bình | Trung bình |
| Voyage voyage-3.5 | Đa ngôn ngữ (không nổi bật) | Tốt | Trung bình |
| mxbai-embed-large | Tập trung Tiếng Anh | N/A | Kém |
| all-MiniLM-L6-v2 | Chỉ Tiếng Anh | N/A | Kém |
4.4. Phân tích Chi phí
| Mô hình | 100K docs (~50M tokens) | 1M docs (~500M tokens) | 10M docs (~5B tokens) | Lựa chọn Self-Hosted |
|---|---|---|---|---|
| BGE-M3 (self-hosted) | $0 (chỉ infra) | $0 (chỉ infra) | $0 (chỉ infra) | Có (Ollama, TEI) |
| Typesense built-in | $0 | $0 | $0 | Có (built-in) |
| multilingual-e5-large | $0 (chỉ infra) | $0 (chỉ infra) | $0 (chỉ infra) | Có (Ollama, TEI) |
| gte-multilingual-base | $0 (chỉ infra) | $0 (chỉ infra) | $0 (chỉ infra) | Có (HF, TEI) |
| OpenAI 3-small | $1.00 | $10.00 | $100.00 | Không |
| OpenAI 3-large | $6.50 | $65.00 | $650.00 | Không |
| Cohere Embed v4 | $6.00 | $60.00 | $600.00 | Không |
| gemini-embedding-001 | $7.50 (có gói miễn phí) | $75.00 | $750.00 | Không |
| Voyage voyage-3.5 | $3.00 (200M đầu tiên miễn phí) | $18.00 | $300.00 | Không |
| jina-embeddings-v3 | Theo token (dùng thử 10M) | Theo token | Theo token | Một phần |
Giả định ~500 token mỗi tài liệu sản phẩm (tiêu đề + mô tả + danh mục bằng cả en + vi).
Chi phí Hạ tầng Self-Hosted:
| Setup | Chi phí Hàng tháng | Throughput | Phù hợp nhất cho |
|---|---|---|---|
| Ollama trên CPU (4-core, 8GB) | ~$30-50/tháng VPS | ~50-100 docs/sec | Catalog nhỏ-trung |
| HuggingFace TEI trên CPU | ~$50-80/tháng VPS | ~100-200 docs/sec | Catalog trung bình |
| HuggingFace TEI trên GPU (T4) | ~$150-300/tháng | ~500-2,000 docs/sec | Catalog lớn |
4.5. Lựa chọn Self-Hosting cho TypeScript/Bun
| Runtime | Mô hình | Tương thích Bun | Latency | Phù hợp nhất cho |
|---|---|---|---|---|
| Ollama (Docker) | bge-m3, nomic-embed, mxbai-embed | Có (REST API) | 10-30ms/embed | Production. Docker đơn giản, quản lý mô hình, REST API |
| HuggingFace TEI (Docker) | Bất kỳ HF model nào (bge-m3, e5, gte) | Có (REST API) | 5-20ms/embed | Production. Token batching, Flash Attention, Prometheus metrics |
| Transformers.js (in-process) | Mô hình nhỏ hơn (<150M params) | Có (v4) | 20-50ms/embed | Prototype. Chỉ CPU, mô hình lớn chậm |
| ONNX Runtime (native addon) | Mô hình export ONNX | Một phần | <10ms/embed (int8) | Quan trọng về hiệu năng. Setup phức tạp |
4.6. Chiến lược Embedding cho Sản phẩm BANA
// Khuyến nghị: Nối các trường liên quan để tạo embedding
function createProductEmbeddingText(product: TProduct): string {
const parts = [
// Primary: product name (both languages, highest weight)
product.name?.en, product.name?.vi,
// Secondary: description
product.description?.en, product.description?.vi,
// Tertiary: structured attributes
product.category?.name?.en, product.category?.name?.vi,
product.merchant?.name?.en,
// Variant info
...product.variants?.map(v => `${v.name?.en || ''} ${v.name?.vi || ''}`),
// Identifiers (for exact match fallback)
product.sku, product.barcode,
].filter(Boolean);
return parts.join(' | ');
}Nguyên tắc cốt lõi:
- Trường quan trọng nhất (tên) đứng đầu — mô hình có trọng số cao hơn cho các token đầu
- Dấu phân tách (
|) giữa các trường để ngăn rò rỉ ngữ nghĩa - Cả hai phiên bản ngôn ngữ cho tìm kiếm xuyên ngôn ngữ
- Giữ tổng số dưới 8,192 tokens cho BGE-M3 (dữ liệu sản phẩm thường <200 tokens)
- Hash văn bản embedding; bỏ qua re-embed nếu hash không thay đổi
4.7. Ma trận Chấm điểm
| Tiêu chí (Trọng số) | BGE-M3 | jina-v3 | TS built-in | Cohere v4 | OpenAI 3-small | e5-multi | gte-multi |
|---|---|---|---|---|---|---|---|
| Chất lượng Tiếng Việt (25%) | 9 | 10 | 6 | 8 | 6 | 8 | 8 |
| Chất lượng Tiếng Anh (15%) | 8 | 8 | 6 | 9 | 8 | 7 | 8 |
| Lựa chọn Self-Hosted (15%) | 10 | 8 | 10 | 3 | 3 | 9 | 9 |
| Hiệu quả Chi phí (15%) | 10 | 7 | 10 | 6 | 9 | 10 | 10 |
| Tương thích Bun/TS (10%) | 8 | 8 | 10 | 8 | 9 | 7 | 7 |
| Dimensions/Lưu trữ (10%) | 8 | 9 | 7 | 9 | 9 | 8 | 7 |
| Độ dài Context (10%) | 9 | 9 | 7 | 10 | 8 | 6 | 9 |
| Tổng có Trọng số | 9.00 | 8.60 | 7.55 | 7.15 | 6.90 | 8.05 | 8.15 |
4.8. Phán quyết Mô hình Embedding
BGE-M3 (Điểm: 9.00) — NGƯỜI CHIẾN THẮNG
- 100+ ngôn ngữ bao gồm Tiếng Việt, huấn luyện trên dữ liệu 170+ ngôn ngữ
- Triple retrieval: Dense + Sparse (kiểu BM25) + ColBERT multi-vector trong một mô hình
- 1024 dimensions, 8,192 token context, 568M tham số
- Self-hosted qua Ollama:
ollama pull bge-m3(1.2GB), REST API tạilocalhost:11434 - Cũng qua HuggingFace TEI: Docker serving cấp production với Prometheus metrics
- Chi phí: $0 mỗi token (self-hosted), ~$30-80/tháng hạ tầng
jina-embeddings-v3 (Điểm: 8.60) — VỀ NHÌ
- Tiếng Việt nằm rõ ràng trong top 30 ngôn ngữ có hiệu năng tốt nhất (chất lượng Tiếng Việt tốt nhất)
- 89 ngôn ngữ, 570M tham số, 1024 dimensions (cấu hình 32-1024)
- LoRA adapters theo task: retrieval, text-matching, classification
- Lưu ý: Định giá API theo token; lựa chọn self-hosting hạn chế hơn
Typesense Built-in distiluse-multilingual (Điểm: 7.55) — KHỞI ĐỘNG NHANH
- Không cấu hình — chỉ cần thêm trường
embedvào schema collection - 512 dimensions, đa ngôn ngữ
- Chất lượng Tiếng Việt: Trung bình (mô hình cũ hơn, không tối ưu cụ thể)
- Phù hợp nhất cho: Phase 1 khởi động nhanh, xác thực hybrid search trước khi đầu tư vào mô hình tốt hơn
5. Đánh giá Kiến trúc
5.1. Các Kiến trúc Ứng viên
Phương án A: Chỉ Typesense (Đơn giản nhất)
| Khía cạnh | Đánh giá |
|---|---|
| Công sức | 1-2 tuần |
| Hạ tầng mới | Không |
| Chi phí hàng tháng | $0 thêm |
| Chất lượng Tiếng Việt | Trung bình (distiluse-multilingual) |
| Chất lượng Hybrid search | Xuất sắc (rank fusion native) |
| Latency tìm kiếm | <50ms |
Phương án B: Typesense + Ollama BGE-M3 (Cân bằng tốt nhất)
| Khía cạnh | Đánh giá |
|---|---|
| Công sức | 3-4 tuần |
| Hạ tầng mới | 1 Docker container (Ollama, ~2GB RAM) |
| Chi phí hàng tháng | ~$30-80 (Ollama trên server có sẵn hoặc VPS nhỏ) |
| Chất lượng Tiếng Việt | Xuất sắc (BGE-M3, 100+ ngôn ngữ) |
| Chất lượng Hybrid search | Xuất sắc (Typesense rank fusion với embedding chất lượng cao) |
| Latency tìm kiếm | <50ms search + 10-30ms embedding (cached: 5ms) |
Phương án C: Typesense + pgvector + Ollama (Toàn năng)
| Khía cạnh | Đánh giá |
|---|---|
| Công sức | 4-6 tuần |
| Hạ tầng mới | 1 Docker container (Ollama) + extension pgvector |
| Chi phí hàng tháng | ~$30-80 |
| Năng lực | Hybrid search + gợi ý + "tương tự" + phân tích SQL |
| Latency tìm kiếm | <50ms search, <100ms recommendations |
Phương án D: Chỉ Cloud API (Công sức thấp nhất)
| Khía cạnh | Đánh giá |
|---|---|
| Công sức | 1 tuần |
| Hạ tầng mới | Không |
| Chi phí hàng tháng | $5-50 chi phí API (theo khối lượng dữ liệu) |
| Chất lượng Tiếng Việt | Tốt (OpenAI) đến Xuất sắc (Cohere v4) |
| Phụ thuộc nhà cung cấp | Cao (API key, giới hạn rate, thay đổi giá) |
5.2. Chấm điểm Kiến trúc
| Tiêu chí (Trọng số) | Phương án A | Phương án B | Phương án C | Phương án D |
|---|---|---|---|---|
| Chất lượng Tiếng Việt (25%) | 6 | 10 | 10 | 7 |
| Công sức Tích hợp (20%) | 10 | 7 | 5 | 10 |
| Chi phí Vận hành (15%) | 10 | 8 | 8 | 6 |
| Đầy đủ Tính năng (15%) | 6 | 8 | 10 | 6 |
| Chủ quyền Dữ liệu (10%) | 10 | 10 | 10 | 3 |
| Chất lượng Tìm kiếm (10%) | 7 | 9 | 9 | 8 |
| Bảo trì (5%) | 10 | 7 | 6 | 9 |
| Tổng có Trọng số | 7.85 | 8.55 | 8.15 | 7.05 |
6. Thiết kế Hybrid Search
6.1. Cách Hybrid Search Hoạt động
Hybrid search chạy tìm kiếm theo từ khóa (BM25) và ngữ nghĩa (vector) song song, sau đó hợp nhất kết quả bằng Reciprocal Rank Fusion (RRF):
RRF_score(document) = SUM( 1 / (rank_i + k) )Trong đó rank_i là vị trí của tài liệu trong từng danh sách kết quả, và k là hằng số làm mịn (thường là 60).
Typesense triển khai điều này nguyên gốc qua tham số alpha:
alpha: 0.0= tìm kiếm thuần từ khóaalpha: 0.3= 70% từ khóa, 30% ngữ nghĩa (khuyến nghị bắt đầu)alpha: 0.5= pha trộn đềualpha: 1.0= tìm kiếm thuần ngữ nghĩa
6.2. Khi nào Tìm kiếm Ngữ nghĩa TỆ HƠN Từ khóa
Đây là yếu tố quan trọng cho hệ thống POS:
| Loại Truy vấn | Chế độ Tìm kiếm Tốt nhất | Vì sao |
|---|---|---|
Mã SKU (SKU-2847-B) | Chỉ từ khóa | Ngữ nghĩa có thể nhầm lẫn các mã tương tự |
Mã vạch (8935049001234) | Chỉ từ khóa | Yêu cầu đối sánh chính xác |
Số model (iPhone 15 Pro Max 256GB) | Chủ yếu từ khóa | Ngữ nghĩa có thể nhầm lẫn các model tương tự |
Tra cứu giá (price < 500000) | Chỉ filter | Không phải vấn đề tìm kiếm |
Ngôn ngữ tự nhiên (tai nghe không dây giá rẻ) | Hybrid | Ngữ nghĩa hiểu ý định |
Truy vấn từ đồng nghĩa (giày chạy bộ → sneakers) | Chủ yếu ngữ nghĩa | Từ khóa không khớp từ đồng nghĩa |
Truy vấn Tiếng Việt (tai nghe khong day) | Hybrid | Ngữ nghĩa xử lý Tiếng Việt tốt |
6.3. Chiến lược Phân loại Truy vấn
6.4. Re-ranking (Tùy chọn, Phase 3+)
Truy xuất hai giai đoạn cải thiện chất lượng 20-35% nhưng tăng latency 200-500ms:
- Giai đoạn 1 (Truy xuất): Hybrid search Typesense trả về top 50-100 ứng viên (~20ms)
- Giai đoạn 2 (Re-rank): Cross-encoder chấm điểm cặp truy vấn + ứng viên để có độ liên quan chính xác
| Re-ranker | Latency (top 20) | Chất lượng | Chi phí |
|---|---|---|---|
| Cohere Rerank 4 | ~200ms | Tốt nhất | $2/1K queries |
| BGE-reranker-v2-m3 | ~150ms | Rất tốt, đa ngôn ngữ | Miễn phí (self-hosted) |
| ms-marco-MiniLM-L-6-v2 | ~100ms | Tốt | Miễn phí (self-hosted) |
Khuyến nghị: Bỏ qua re-ranking ban đầu. Thêm vào trong Phase 3+ chỉ khi các chỉ số chất lượng tìm kiếm cho thấy cần thiết.
7. Thiết kế Pipeline Dữ liệu
7.1. Pipeline Embedding (Kiến trúc Phương án B)
7.2. Vô hiệu hóa Embedding (Invalidation)
Các trường kích hoạt re-embedding so với các trường không cần:
| Kích hoạt Re-embedding | Bỏ qua Re-embedding |
|---|---|
| Tên sản phẩm (en, vi) | Thay đổi giá |
| Mô tả sản phẩm (en, vi) | Số lượng tồn kho |
| Gán danh mục | Thứ tự sắp xếp |
| Thương hiệu / thuộc tính chính | Cờ nội bộ |
| Tên variant | Trạng thái (lọc, không embed) |
Triển khai: Hash SHA-256 của các trường liên quan đến embedding. So sánh hash khi có CDC event; bỏ qua re-embedding nếu không thay đổi.
7.3. Thiết kế Queue BullMQ
Theo pattern BullMQ hiện có của BANA (giống mq-pay và finance):
Queue: embedding:generation:P01, P02, P03 (3 partitions for load distribution)
Queue: embedding:indexing:P01, P02, P03
Concurrency: 10 jobs per partition (configurable)
Retry: Exponential backoff, max 3 retries
Dead Letter Queue: embedding:generation:failed8. Ước tính Bộ nhớ
8.1. Công thức Bộ nhớ HNSW Index
Memory = 1.1 * (4 * dimensions + 8 * m) * num_vectors bytesTrong đó m là số liên kết hai chiều tối đa của HNSW (thường là 16).
8.2. Dự báo cho BANA
| Quy mô | Sản phẩm | Dimensions | Bộ nhớ HNSW | Tổng Typesense (+ model) | Tổng pgvector |
|---|---|---|---|---|---|
| Nhỏ | 10,000 | 1024 (BGE-M3) | ~60 MB | ~2.1 GB (+ 2GB model) | ~60 MB |
| Trung bình | 100,000 | 1024 | ~600 MB | ~2.6 GB (+ 2GB model) | ~600 MB |
| Lớn | 1,000,000 | 1024 | ~6 GB | ~8 GB (+ 2GB model) | ~6 GB |
| Nhỏ (halfvec) | 100,000 | 1024 | ~300 MB | N/A | ~300 MB |
Lưu ý: Các ước tính này chỉ dành cho collection products. Nhân với ~1.3x cho tất cả 6 collections (các collection khác nhỏ hơn).
9. Quyết định
9.1. Lựa chọn: Phương án B — Typesense + Ollama BGE-M3 (Điểm: 8.55)
Vector Database được chọn: Typesense (nâng cấp vector cho triển khai hiện có) Mô hình Embedding được chọn: BGE-M3 qua Ollama (self-hosted) Kiến trúc: Typesense hybrid search với embedding BGE-M3 tính sẵn qua pipeline BullMQ
9.2. Lý do
- Tận dụng hạ tầng có sẵn — Typesense đã được triển khai cùng đồng bộ CDC. Không cần service database mới.
- Hỗ trợ Tiếng Việt + Tiếng Anh tốt nhất — BGE-M3 xử lý 100+ ngôn ngữ bao gồm Tiếng Việt với chất lượng xuất sắc. Self-hosted có nghĩa là không phụ thuộc API.
- Không có chi phí theo token — Tự sinh embedding self-hosted. Chi phí hạ tầng dự đoán được (~$30-80/tháng cho Ollama).
- Hybrid search đã được kiểm chứng production — Typesense rank fusion kết hợp tìm kiếm từ khóa + ngữ nghĩa nguyên gốc. Tham số
alphacấu hình được để tinh chỉnh. - Phù hợp pattern BullMQ hiện có — Pipeline embedding sử dụng cùng kiến trúc queue như package finance/payment (3 partitions, concurrency cấu hình được).
- Chủ quyền dữ liệu — Tất cả dữ liệu và mô hình ở lại trên server của bạn. Không có cuộc gọi API bên ngoài cho embedding.
- Migration tăng dần — Có thể bắt đầu với Phase 1 (mô hình built-in của Typesense) trong 1-2 tuần, sau đó nâng cấp lên BGE-M3 ở Phase 2.
9.3. Các Giai đoạn Triển khai
| Phase | Thời gian | Mô tả | Tác động |
|---|---|---|---|
| Phase 1 | Tuần 1-2 | Thêm embedding tích hợp Typesense (ts/distiluse-multilingual) vào collection products. Bật hybrid search với alpha: 0.3. Test với pipeline CDC hiện có. Phân loại truy vấn cho SKU vs ngôn ngữ tự nhiên. | Tìm kiếm ngữ nghĩa ngay lập tức. Chi phí $0. |
| Phase 2 | Tuần 3-4 | Triển khai Docker container Ollama + BGE-M3. Xây dựng pipeline embedding BullMQ (generation + indexing). Thay mô hình built-in bằng vector float[] BGE-M3 tính sẵn. Tinh chỉnh tham số alpha. | Chất lượng tìm kiếm Tiếng Việt vượt trội. |
| Phase 3 | Tuần 5-6 | Mở rộng cho tất cả 6 collections. Thêm cache embedding truy vấn (Redis). Triển khai search analytics (CTR, tỷ lệ zero-results). Thêm "sản phẩm liên quan" sử dụng độ tương tự vector. | Tìm kiếm ngữ nghĩa đầy đủ trên tất cả entity. |
| Phase 4 | Tương lai | Thêm pgvector cho gợi ý cấp SQL. Thêm cross-encoder re-ranking cho truy vấn giá trị cao. Triển khai gợi ý "ý bạn là?". Fine-tune mô hình embedding nếu cần. | Tính năng nâng cao. |
9.4. Tóm tắt Chi phí
| Thành phần | Trạng thái | Chi phí Hàng tháng |
|---|---|---|
| Typesense (có sẵn) | Đã chạy | $0 |
| Ollama BGE-M3 (Docker trên server có sẵn hoặc VPS $30) | Mới | $0-30 |
| Cache embedding Redis (Redis DB có sẵn) | Đã chạy | $0 |
| BullMQ workers (hạ tầng Bun có sẵn) | Tái sử dụng pattern | $0 |
| Pipeline Kafka CDC (có sẵn) | Đã chạy | $0 |
| Tổng | $0-30/tháng |
10. Phụ lục PostgreSQL Full-Text Search
10.1. Lý do Nghiên cứu
PostgreSQL cung cấp khả năng Full-Text Search (FTS) tích hợp sẵn, và một số extension hứa hẹn chấm điểm BM25 trong chính PostgreSQL. Phụ lục này đánh giá liệu PostgreSQL-native search có thể thay thế hoặc bổ sung cho kiến trúc Typesense + Ollama BGE-M3 đã chọn ở Phần 9.
Các câu hỏi chính được nghiên cứu:
- PostgreSQL có thể cung cấp BM25 scoring nguyên gốc không?
- PostgreSQL FTS xử lý văn bản Tiếng Việt tốt đến đâu?
- Drizzle ORM tích hợp với PostgreSQL FTS như thế nào?
- PostgreSQL FTS nên thay thế hay bổ sung cho Typesense?
10.2. So sánh Extension BM25
PostgreSQL FTS native sử dụng ts_rank (biến thể TF-IDF), không phải BM25. Một số extension thêm BM25 scoring:
| Extension | BM25 | License | An toàn WAL | Tiếng Việt | Drizzle ORM | Trạng thái Production |
|---|---|---|---|---|---|---|
| Native FTS | Không (TF-IDF) | PostgreSQL | Có | Custom config | Đầy đủ | Hàng thập kỷ |
| ParadeDB pg_search | Có | AGPL-3.0 | Không (community) | Chỉ ICU | Chỉ Raw SQL | Active ($12M tài trợ) |
| Timescale pg_textsearch | Có | PostgreSQL | Chưa biết | Chưa biết | Chỉ Raw SQL | Chỉ Preview |
| VectorChord-BM25 | Có | Apache-2.0 | Chưa biết | Chưa biết | Chỉ Raw SQL | Rất mới |
| PGroonga | Không (Groonga) | PostgreSQL | Có | Native | Chỉ Raw SQL | 10+ năm trưởng thành |
| pg_trgm | Không (similarity) | PostgreSQL | Có | Hoạt động với unaccent | Đầy đủ | Hàng thập kỷ |
10.2.1. ParadeDB pg_search — BỎ QUA
ParadeDB tích hợp BM25 search vào PostgreSQL qua engine Tantivy (Rust, lấy cảm hứng Lucene). Hiệu năng nhanh hơn 20-1000x so với FTS native.
Vấn đề quan trọng cho BANA:
- License AGPL-3.0 — Nếu bạn sửa đổi pg_search và cung cấp qua mạng (SaaS/POS), bạn có thể phải release các sửa đổi. Cần xem xét pháp lý cho mục đích thương mại.
- Không hỗ trợ WAL trong phiên bản community — PostgreSQL crash = phải rebuild toàn bộ BM25 index. Phiên bản enterprise thêm WAL nhưng yêu cầu license trả phí.
- Một BM25 index mỗi bảng — Không thể tạo nhiều BM25 index với các cấu hình khác nhau trên cùng một bảng.
- Không có tokenizer Tiếng Việt — Tantivy cung cấp ICU tokenization (ranh giới từ Unicode) nhưng không có phân đoạn từ ghép Tiếng Việt theo ngôn ngữ học.
- Không có tích hợp Drizzle ORM — Tất cả query qua raw SQL (
db.execute(sql\...`)`).
10.2.2. Timescale pg_textsearch — THEO DÕI
Đây là extension được hỏi ban đầu. Phát triển bởi Timescale với license tương thích PostgreSQL (permissive). Tuy nhiên, đang ở trạng thái preview — chưa phù hợp triển khai production. Đáng theo dõi cho phiên bản GA tương lai.
10.2.3. PGroonga — XEM XÉT
PGroonga (dựa trên Groonga, v4.0.5) là extension duy nhất hỗ trợ Tiếng Việt nguyên gốc ngay lập tức, xử lý tất cả các ngôn ngữ bao gồm CJK và Tiếng Việt mà không cần cấu hình tùy chỉnh. Có sẵn trên Supabase như extension built-in. Đánh đổi: thêm Groonga như dependency bên ngoài.
10.3. Tìm kiếm Tiếng Việt trong PostgreSQL
10.3.1. Thách thức Tiếng Việt
Văn bản Tiếng Việt đặt ra những thách thức đặc biệt cho tìm kiếm:
| Thách thức | Ví dụ | Tác động |
|---|---|---|
| Dấu thanh | à á ả ã ạ đều là biến thể của a | Người dùng có thể tìm kiếm không có dấu |
| Ký tự kép | ế = mũ + sắc (2 dấu) | Cần chuẩn hóa ký tự |
| Từ ghép | "Hà Nội" = 2 âm tiết, 1 từ | Tokenization theo khoảng trắng phá vỡ từ ghép |
| Không có cấu hình PG sẵn | Tiếng Việt không có trong stemmer Snowball | Cần cấu hình text search tùy chỉnh |
10.3.2. Giải pháp unaccent
Từ PostgreSQL 11, unaccent.rules mặc định bao gồm tất cả ký tự Tiếng Việt:
CREATE EXTENSION IF NOT EXISTS unaccent;
-- Test: all Vietnamese diacritics handled
SELECT unaccent('cà phê điện thoại'); -- → 'ca phe dien thoai'
SELECT unaccent('Hà Nội thủ đô'); -- → 'Ha Noi thu do'Các ký tự được xử lý: ắ ằ ẳ ẵ ặ → a, ế ề ể ễ ệ → e, ớ ờ ở ỡ ợ → o, ứ ừ ử ữ ự → u, đ → d, và tất cả các kết hợp dấu thanh.
Quan trọng: unaccent() là STABLE (không phải IMMUTABLE), do đó cần wrapper immutable cho indexes và generated columns:
CREATE OR REPLACE FUNCTION f_unaccent(text)
RETURNS text AS $$
SELECT public.unaccent('public.unaccent', $1);
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT;10.3.3. Cấu hình Text Search Tiếng Việt Tùy chỉnh
CREATE TEXT SEARCH CONFIGURATION vietnamese (COPY = simple);
ALTER TEXT SEARCH CONFIGURATION vietnamese
ALTER MAPPING FOR asciiword, asciihword, hword_asciipart,
word, hword, hword_part
WITH unaccent, simple;Điều này kích hoạt FTS Tiếng Việt nơi "ca phe" khớp "cà phê" qua chuẩn hóa dấu.
10.3.4. Công cụ Phân đoạn Từ Tiếng Việt
| Công cụ | Ngôn ngữ | Tốc độ | Extension PostgreSQL? |
|---|---|---|---|
| CocCoc Tokenizer | C++ | Rất nhanh | Không |
| Underthesea | Python | Tốt | Không |
| VnCoreNLP | Java | Tốt | Không |
| PGroonga/Groonga | C | Nhanh | Có |
Không có công cụ NLP Tiếng Việt nào có extension PostgreSQL nguyên gốc (trừ PGroonga). Đối với phân đoạn từ ghép, cần tiền xử lý ở tầng ứng dụng.
10.3.5. ICU Accent-Insensitive Collation (PostgreSQL 12+)
CREATE COLLATION vi_accent_insensitive (
provider = icu,
deterministic = false,
locale = 'vi-u-ks-level1'
);Điều này cung cấp so sánh không nhạy với dấu ở cấp database nhưng không thể sử dụng với pattern LIKE (chỉ với =) và có chi phí hiệu năng.
10.4. Tích hợp Drizzle ORM
10.4.1. Kiểu tsvector Tùy chỉnh
Drizzle ORM không có hỗ trợ tsvector nguyên gốc. Cần định nghĩa kiểu tùy chỉnh:
import { customType } from 'drizzle-orm/pg-core';
export const tsvector = customType<{ data: string }>({
dataType() {
return 'tsvector';
},
});10.4.2. Schema với Generated Column + GIN Index
import { SQL, sql } from 'drizzle-orm';
import { index, pgTable, text, bigint } from 'drizzle-orm/pg-core';
export const products = pgTable(
'products',
{
id: bigint('id', { mode: 'bigint' }).primaryKey(),
nameVi: text('name_vi').notNull(),
nameEn: text('name_en'),
nameSearch: tsvector('name_search')
.generatedAlwaysAs(
(): SQL =>
sql`setweight(to_tsvector('vietnamese',
f_unaccent(coalesce(${products.nameVi}, ''))), 'A') ||
setweight(to_tsvector('english',
coalesce(${products.nameEn}, '')), 'B')`,
),
},
(t) => [
// GIN index for full-text search
index('idx_products_name_search').using('gin', t.nameSearch),
// GIN trigram index for fuzzy search
index('idx_products_name_vi_trgm').using(
'gin',
sql`f_unaccent(lower(${t.nameVi}))`.op('gin_trgm_ops'),
),
],
);Ràng buộc quan trọng: Biểu thức generation chỉ được sử dụng các hàm IMMUTABLE. Cả to_tsvector() (với regconfig rõ ràng 'vietnamese') và f_unaccent() (wrapper tùy chỉnh của chúng ta) đều là immutable.
10.4.3. Pattern Truy vấn với Drizzle
// Full-text search with ranking
const query = sql`plainto_tsquery('vietnamese', f_unaccent(${searchTerm}))`;
const results = await db
.select({
id: products.id,
nameVi: products.nameVi,
rank: sql<number>`ts_rank(${products.nameSearch}, ${query})`.as('rank'),
})
.from(products)
.where(sql`${products.nameSearch} @@ ${query}`)
.orderBy(desc(sql`ts_rank(${products.nameSearch}, ${query})`));
// Fuzzy trigram search (typo-tolerant fallback)
const fuzzy = await db
.select({
id: products.id,
sim: sql<number>`similarity(
f_unaccent(lower(${products.nameVi})),
f_unaccent(lower(${term}))
)`.as('sim'),
})
.from(products)
.where(sql`f_unaccent(lower(${products.nameVi})) % f_unaccent(lower(${term}))`)
.orderBy(desc(sql`similarity(...)`));10.4.4. Migration trong BANA (Pattern IGNIS)
// src/migrations/processes/search-0001-setup-vietnamese-fts.ts
export const process: TMigrationProcess = {
name: 'search-0001-setup-vietnamese-fts',
handler: async ({ connector }) => {
await connector.execute(sql`CREATE EXTENSION IF NOT EXISTS unaccent`);
await connector.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`);
await connector.execute(sql`
CREATE OR REPLACE FUNCTION f_unaccent(text)
RETURNS text AS $$
SELECT public.unaccent('public.unaccent', $1);
$$ LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
`);
await connector.execute(sql`
CREATE TEXT SEARCH CONFIGURATION IF NOT EXISTS vietnamese (COPY = simple)
`);
await connector.execute(sql`
ALTER TEXT SEARCH CONFIGURATION vietnamese
ALTER MAPPING FOR asciiword, asciihword, hword_asciipart,
word, hword, hword_part
WITH unaccent, simple
`);
},
};10.5. Kiến trúc Thực tế: Tìm kiếm Đa lớp
Đối với BANA, cách tiếp cận khuyến nghị là tìm kiếm đa lớp với suy giảm duyên dáng (graceful degradation):
| Lớp | Engine | Phù hợp nhất cho | Latency |
|---|---|---|---|
| Chính | Typesense (hybrid) | Catalog sản phẩm, tìm kiếm khối lượng cao, faceted search | <50ms |
| Bổ sung | PostgreSQL FTS | Truy vấn nhất quán thời gian thực, tìm kiếm CRUD inline, collection nhỏ | 10-50ms |
| Dự phòng | PostgreSQL pg_trgm | Tìm kiếm fuzzy/dung sai lỗi, không nhạy dấu Tiếng Việt | 10-50ms |
10.6. Tác động lên Quyết định Ban đầu
Khuyến nghị ban đầu (Phương án B: Typesense + Ollama BGE-M3) vẫn không thay đổi. PostgreSQL FTS là một bổ sung có giá trị, không phải thay thế.
| Khía cạnh | Typesense (Chính) | PostgreSQL FTS (Bổ sung) |
|---|---|---|
| Ranking BM25 | Tích hợp sẵn | Không (chỉ TF-IDF qua ts_rank) |
| Tìm kiếm Ngữ nghĩa/Vector | Tích hợp sẵn (với BGE-M3 embeddings) | Không (cần pgvector riêng) |
| Dấu thanh Tiếng Việt | Tokenization ICU | Extension unaccent (hỗ trợ đầy đủ) |
| Dung sai lỗi chính tả | Tích hợp sẵn | Qua extension pg_trgm |
| Faceted Search | Native | Không |
| Tích hợp Drizzle ORM | N/A (service riêng) | Đầy đủ (generated columns, GIN indexes) |
| Nhất quán thời gian thực | Trễ CDC (~1-5s) | Tức thì (cùng transaction) |
| Chi phí vận hành | Đã triển khai ($0) | Đã triển khai ($0) |
Khi nào nên dùng PostgreSQL FTS thay vì Typesense:
- Tìm kiếm sale order trong context một merchant duy nhất (nhất quán thời gian thực quan trọng)
- Tìm kiếm finance transaction (dataset nhỏ, truy vấn dạng giao dịch)
- Bộ lọc tìm kiếm inline trong các CRUD controller có sẵn
- Dự phòng khi Typesense tạm thời không khả dụng
Phán quyết các extension BM25:
- ParadeDB: Bỏ qua (license AGPL + không có WAL trong phiên bản community)
- pg_textsearch: Theo dõi (license PostgreSQL hứa hẹn, nhưng vẫn ở preview)
- PGroonga: Xem xét cho hỗ trợ Tiếng Việt tốt nhất (nếu cần phân đoạn từ ghép)
- Native FTS + pg_trgm: Sử dụng ngay (chi phí $0, tích hợp Drizzle đầy đủ)
11. So sánh với các Lựa chọn không được Chọn
| Lựa chọn | Lý do không chọn |
|---|---|
| Elasticsearch/OpenSearch | Cluster 128-256GB RAM. Overhead vận hành JVM. Quá mức cho quy mô POS. |
| Vespa | Không có TypeScript SDK. Cấu hình dựa trên XML. Phức tạp ở quy mô internet. |
| Pinecone | Chỉ có cloud. Không có self-hosted. Định giá theo mức tiêu thụ tăng theo sử dụng. Khóa nhà cung cấp. |
| ChromaDB | Single-node. 60GB RAM cho 10M vectors. Không có multi-tenancy. Chưa sẵn sàng production. |
| Milvus | Yêu cầu etcd + MinIO. Phức tạp không tương xứng cho <1M sản phẩm. |
| Weaviate | Multi-container Compose. Tối thiểu 8-16GB RAM. Đường cong học tập dốc hơn. |
| Meilisearch | Single-node. RAM = 35x kích thước dữ liệu. Không scale ngang. |
| Pure Cloud API (OpenAI/Cohere) | Chi phí theo token liên tục. Phụ thuộc API. Giới hạn rate. Ít kiểm soát. |
12. Tài liệu Liên quan
| Tài liệu | Mô tả |
|---|---|
| Search | Thẻ định danh thư viện search + catalog |
| Search — Architecture | CDC pipeline + query path |
| Search — Domain Model | Typesense collections |
| Commerce Package | Tích hợp tìm kiếm sản phẩm (host) |
| Core Database | Schema và model PostgreSQL |
13. Tham khảo
Vector Databases
- Typesense Vector Search Documentation
- Typesense Semantic Search Guide
- pgvector GitHub
- Drizzle ORM pgvector Integration
- pgvector 0.8.0 on Aurora PostgreSQL
- Redis Vector Search
- Redis Benchmarking Results
- Qdrant Vector Database
- Milvus Multi-Tenancy
- Weaviate Hybrid Search
- Meilisearch AI-Powered Search
Embedding Models
- BGE-M3 on HuggingFace
- Jina Embeddings v3
- OpenAI Embeddings
- Cohere Embed v4
- Gemini Embedding
- Voyage AI Embeddings
- VN-MTEB Vietnamese Benchmark
- Ollama Embedding Models
- HuggingFace TEI
- Transformers.js v4
PostgreSQL Full-Text Search
- PostgreSQL Documentation: Full Text Search
- PostgreSQL Documentation: unaccent
- PostgreSQL Documentation: pg_trgm
- ParadeDB: BM25 in PostgreSQL
- ParadeDB Documentation
- PGroonga: Multilingual Full Text Search
- PGroonga on Supabase
- Vietnamese FTS on PostgreSQL (blog.tuando.me)
- Vietnamese PostgreSQL FTS Configuration (GitHub Gist)
- CocCoc Vietnamese Tokenizer
- Drizzle ORM: PostgreSQL Full-Text Search
- Drizzle ORM: Full-Text Search with Generated Columns
- Drizzle ORM: Indexes & Constraints
- PostgreSQL Accent-Insensitive Collations
- Postgres Text Search: Full Text vs Trigram