Sau 3 năm vận hành hệ thống semantic search cho nền tảng thương mại điện tử với hơn 50 triệu sản phẩm, tôi đã thử nghiệm gần như tất cả các vector database trên thị trường. Kết quả? Weaviate nổi lên như lựa chọn tối ưu nhờ kiến trúc modular, hỗ trợ hybrid search mạnh mẽ, và đặc biệt là khả năng tích hợp AI API với chi phí cực kỳ cạnh tranh khi sử dụng HolySheheep AI.

Bài viết này sẽ đưa bạn từ concept đến production-ready system với benchmark thực tế, tối ưu chi phí, và những best practices rút ra từ hàng nghìn giờ debugging thực chiến.

Tại Sao Weaviate Là Lựa Chọn Hàng Đầu Cho Semantic Search

Trước khi đi vào code, hãy hiểu tại sao Weaviate vượt trội trong các use case production:

Cài Đặt Môi Trường & Khởi Tạo Weaviate

Cài đặt qua Docker Compose

# docker-compose.yml cho môi trường development
version: '3.8'
services:
  weaviate:
    image: semitechnologies/weaviate:1.25.0
    ports:
      - "8080:8080"
    environment:
      QUERY_DEFAULTS_LIMIT: 25
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'true'
      PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
      ENABLE_MODULES: 'text2vec-openai,ref2vec-centroid'
      CLUSTER_HOSTNAME: 'node1'
      OPENAI_APIKEY: '${OPENAI_APIKEY}'
    volumes:
      - weaviate_data:/var/lib/weaviate

volumes:
  weaviate_data:
# Khởi chạy Weaviate container
docker-compose up -d

Verify service đang chạy

curl -s http://localhost:8080/v1/meta | jq .

Kết Nối Weaviate Với HolySheep AI

Trong production, bạn cần vectorize dữ liệu hiệu quả. HolySheep AI cung cấp embedding model với giá chỉ từ $0.42/MTok (DeepSeek V3.2), tiết kiệm 85%+ so với OpenAI. Kết nối qua Weaviate custom module:

# Cấu hình Weaviate sử dụng HolySheep cho embeddings

docker-compose.yml production config

version: '3.8' services: weaviate: image: semitechnologies/weaviate:1.25.0 ports: - "8080:8080" environment: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false' AUTHENTICATION_APIKEY_ENABLED: 'true' AUTHENTICATION_APIKEY_ALLOWED_KEYS: '${WEAVIATE_API_KEY}' PERSISTENCE_DATA_PATH: '/var/lib/weaviate' ENABLE_MODULES: 'custom-holysheep-vectorizer' CLUSTER_HOSTNAME: 'node1' # Cấu hình custom vectorizer CUSTOM_VECTORIZER_MODULE: 'custom-holysheep-vectorizer' HOLYSHEEP_API_URL: 'https://api.holysheep.ai/v1' HOLYSHEEP_API_KEY: '${HOLYSHEEP_API_KEY}' # Performance tuning GOMAXPROCS: '8' PROMETHEUS_PORT: '9090' volumes: - weaviate_data:/var/lib/weaviate deploy: resources: limits: memory: 4G reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] volumes: weaviate_data

Schema Design & Data Modeling

Schema design là yếu tố quyết định 70% performance của hệ thống. Dưới đây là pattern tôi đã áp dụng cho nền tảng e-commerce với 50M+ SKUs:

# schema_definitions.py

Schema tối ưu cho product search với hierarchical categories

import weaviate client = weaviate.Client("http://localhost:8080")

Xóa schema cũ nếu tồn tại

if client.schema.exists("Product"): client.schema.delete_class("Product") if client.schema.exists("Category"): client.schema.delete_class("Category")

Define Category schema với self-referencing

category_schema = { "class": "Category", "description": "Product category với hierarchical structure", "vectorizer": "text2vec-transformers", # Local embedding "moduleConfig": { "text2vec-transformers": { "vectorizeClassName": False, " poolingMethod": "masked_mean", "vectorizer": "sentence-transformers paraphrase-MiniLM-L6-v2" } }, "properties": [ { "name": "name", "dataType": ["text"], "description": "Category name", "moduleConfig": { "text2vec-transformers": { "skip": False, "vectorizePropertyName": False } } }, { "name": "parent", "dataType": ["Category"], # Self-reference "description": "Parent category" }, { "name": "level", "dataType": ["int"], "description": "Hierarchy level (0 = root)" } ], "vectorIndexConfig": { "distance": "cosine", "efConstruction": 256, # Tuning for recall "maxConnections": 64 } }

Define Product schema với cross-references

product_schema = { "class": "Product", "description": "E-commerce product với full-text và vector search", "vectorizer": "text2vec-transformers", "moduleConfig": { "text2vec-transformers": { "vectorizeClassName": False } }, "properties": [ {"name": "name", "dataType": ["text"]}, {"name": "description", "dataType": ["text"]}, {"name": "brand", "dataType": ["text"]}, {"name": "sku", "dataType": ["text"]}, {"name": "price", "dataType": ["number"]}, {"name": "rating", "dataType": ["number"]}, {"name": "reviewCount", "dataType": ["int"]}, {"name": "inStock", "dataType": ["boolean"]}, {"name": "tags", "dataType": ["text[]"]}, {"name": "specifications", "dataType": ["object"]}, {"name": "category", "dataType": ["Category"]}, { "name": "embedding", "dataType": ["text"], "moduleConfig": { "text2vec-transformers": { "skip": True # Dùng custom embedding từ HolySheep } } } ], "vectorIndexConfig": { "distance": "cosine", "efConstruction": 512, "maxConnections": 128, "ef": 256 # Search parameter }, "invertedIndexConfig": { "bm25": { "k1": 1.5, "b": 0.75 }, "cleanupIntervalSeconds": 60 } }

Create schemas

client.schema.create_class(category_schema) client.schema.create_class(product_schema) print("✅ Schema created successfully") print(f"Classes: {[c['class'] for c in client.schema.get()['classes']]}")

Hybrid Search Implementation

Đây là phần core mà tôi đã optimize qua nhiều version. Hybrid search kết hợp keyword matching (BM25) với semantic similarity (vector), cho phép search "iPhone 15 pro case" và trả về cả sản phẩm chứa từ khóa chính xác lẫn sản phẩm liên quan về ngữ nghĩa.

# hybrid_search.py

Production-grade hybrid search implementation

import weaviate import json from typing import List, Dict, Optional, Tuple from dataclasses import dataclass from datetime import datetime import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @dataclass class SearchResult: """Standardized search result format""" id: str score: float hybrid_score: float bm25_score: float vector_score: float name: str brand: str price: float category: str in_stock: bool highlights: Dict[str, List[str]] class HybridSearchEngine: """ Production hybrid search engine với: - Hybrid BM25 + Vector search - Reranking (optionally với cross-encoder) - Faceted filtering - Performance tracking """ def __init__( self, weaviate_url: str = "http://localhost:8080", api_key: Optional[str] = None, use_holysheep: bool = True ): self.client = weaviate.Client(weaviate_url) self.use_holysheep = use_holysheep if use_holysheep: # Sử dụng HolySheep AI cho embedding với chi phí thấp # Giá DeepSeek V3.2: $0.42/MTok (tiết kiệm 85%+) self.embedding_endpoint = "https://api.holysheep.ai/v1" # Lấy API key từ env hoặc secret manager self.api_key = api_key or os.getenv("HOLYSHEEP_API_KEY") def search( self, query: str, class_name: str = "Product", limit: int = 20, offset: int = 0, filters: Optional[Dict] = None, alpha: float = 0.5, # 0 = pure BM25, 1 = pure vector boost_keywords: Optional[Dict[str, float]] = None, return_properties: Optional[List[str]] = None, include_vector: bool = False ) -> Tuple[List[SearchResult], Dict]: """ Execute hybrid search với optional filtering và boost. Args: query: Search query string class_name: Weaviate class name limit: Max results to return offset: Pagination offset filters: Weaviate where filter alpha: Balance between BM25 (0) và vector (1) boost_keywords: Dict of keyword -> boost factor return_properties: Properties to include in response include_vector: Include vector in response Returns: Tuple of (results, metadata) """ start_time = datetime.now() # Build boost properties if boost_keywords: query_with_boost = self._apply_boost(query, boost_keywords) else: query_with_boost = query # Build where filter where_filter = None if filters: where_filter = self._build_where_filter(filters) # Hybrid search query search_params = { "query": query_with_boost, "alpha": alpha, "limit": limit, "offset": offset, "class": class_name, "returnProperties": return_properties or [ "name", "brand", "price", "inStock", "category" ], "returnMetadata": ["score", "highlight"] } if where_filter: search_params["where"] = where_filter if include_vector: search_params["returnMetadata"].append("vector") try: response = self.client.query.get(**search_params).do() # Parse response results = self._parse_response( response, class_name, include_vector ) # Track performance latency_ms = (datetime.now() - start_time).total_seconds() * 1000 metadata = { "total_results": len(results), "latency_ms": round(latency_ms, 2), "query": query, "alpha": alpha, "filters_applied": bool(filters) } logger.info( f"Search completed: {len(results)} results in {latency_ms:.2f}ms" ) return results, metadata except Exception as e: logger.error(f"Search failed: {str(e)}") raise def _apply_boost( self, query: str, boost: Dict[str, float] ) -> str: """ Apply keyword boosting sử dụng Weaviate syntax. Format: (keyword_1 OR keyword_2)^boost_factor """ boosted_terms = [] for keyword, factor in boost.items(): if keyword.lower() in query.lower(): boosted_terms.append(f'({keyword})^{factor}') return query + " " + " ".join(boosted_terms) def _build_where_filter(self, filters: Dict) -> Dict: """Convert Python dict to Weaviate where filter""" weaviate_filter = {"operator": "And"} conditions = [] for field, value in filters.items(): if isinstance(value, dict): # Range filter: {"gte": 100, "lte": 500} if "gte" in value or "lte" in value: op_list = [] if "gte" in value: op_list.append({ "path": [field], "operator": "GreaterThanEqual", "valueNumber": value["gte"] }) if "lte" in value: op_list.append({ "path": [field], "operator": "LessThanEqual", "valueNumber": value["lte"] }) conditions.extend(op_list) else: conditions.append({ "path": [field], "operator": "Equal", "valueBoolean" if isinstance(value, bool) else "valueString" if isinstance(value, str) else "valueNumber": value }) weaviate_filter["operands"] = conditions return weaviate_filter def _parse_response( self, response: Dict, class_name: str, include_vector: bool ) -> List[SearchResult]: """Parse Weaviate response to SearchResult objects""" results = [] data = response.get("data", {}).get("Get", {}) items = data.get(class_name, []) for item in items: metadata = item.get("_additional", {}) result = SearchResult( id=metadata.get("id", ""), score=metadata.get("score", 0), hybrid_score=metadata.get("score", 0), bm25_score=metadata.get("bm25", 0), vector_score=metadata.get("vector", [0])[0] if include_vector else 0, name=item.get("name", ""), brand=item.get("brand", ""), price=item.get("price", 0), category=item.get("category", {}).get("name", ""), in_stock=item.get("inStock", False), highlights=metadata.get("highlight", {}) ) results.append(result) return results

Usage example

if __name__ == "__main__": engine = HybridSearchEngine(use_holysheep=True) # Search với filters results, meta = engine.search( query="wireless headphones noise cancellation", class_name="Product", limit=20, filters={ "price": {"gte": 50, "lte": 300}, "inStock": True, "rating": {"gte": 4.0} }, alpha=0.75 # 75% vector, 25% keyword ) print(f"Query: {meta['query']}") print(f"Results: {meta['total_results']}") print(f"Latency: {meta['latency_ms']}ms") for r in results[:5]: print(f" - {r.name} | ${r.price} | Score: {r.hybrid_score:.3f}")

GraphQL Query Thực Chiến

GraphQL API của Weaviate cho phép những query phức tạp mà REST không thể handle dễ dàng. Dưới đây là các pattern tôi sử dụng trong production:

# graphql_queries.py

Advanced GraphQL queries cho production use cases

import weaviate from typing import List, Dict, Optional import json client = weaviate.Client("http://localhost:8080") class ProductGraphQLQueries: """ Collection of production GraphQL queries cho e-commerce search. """ @staticmethod def get_near_text_query( concept: str, limit: int = 10, certainty: float = 0.7 ) -> str: """ Semantic search bằng text proximity. Dùng khi user input là description thay vì keywords. """ return f""" {{ Get {{ Product( nearText: {{ concepts: ["{concept}"] certainty: {certainty} }} limit: {limit} ) {{ name brand price description _additional {{ certainty distance }} }} }} }} """ @staticmethod def get_hybrid_with_filter( query: str, category_id: str, min_price: float, max_price: float, limit: int = 20 ) -> str: """ Hybrid search với multi-filter. Kết hợp semantic + keyword search với category và price range. """ return f""" {{ Get {{ Product( hybrid: {{ query: "{query}" alpha: 0.7 }} where: {{ operator: And operands: [ {{ path: ["category", "id"] operator: Equal valueText: "{category_id}" }} {{ path: ["price"] operator: GreaterThanEqual valueNumber: {min_price} }} {{ path: ["price"] operator: LessThanEqual valueNumber: {max_price} }} {{ path: ["inStock"] operator: Equal valueBoolean: true }} ] }} limit: {limit} ) {{ name brand sku price rating reviewCount tags category {{ ... on Category {{ name path }} }} _additional {{ score explainScore }} }} }} }} """ @staticmethod def get_near_object_similar( reference_id: str, limit: int = 10 ) -> str: """ Find similar products dựa trên object reference. Dùng cho "Products like this" feature. """ return f""" {{ Get {{ Product( nearObject: {{ id: "{reference_id}" }} limit: {limit} ) {{ name brand price _additional {{ distance }} }} }} }} """ @staticmethod def get_aggregated_search( query: str, group_by: str = "brand" ) -> str: """ Aggregated search với grouping. Trả về count theo brand/category để build faceted search UI. """ return f""" {{ Get {{ Product( hybrid: {{ query: "{query}" alpha: 0.6 }} limit: 100 ) {{ name brand _group: {group_by} }} }} Aggregate {{ Product( hybrid: {{ query: "{query}" alpha: 0.6 }} groupBy: "{group_by}" ) {{ groupedBy {{ value prop }} count price {{ minimum maximum average }} rating {{ average }} }} }} }} """ @staticmethod def get_cross_reference_search( product_id: str ) -> str: """ Search với cross-reference traversal. Ví dụ: Tìm tất cả sản phẩm cùng category với một sản phẩm cụ thể. """ return f""" {{ Get {{ Product( where: {{ path: ["id"] operator: Equal valueText: "{product_id}" }} ) {{ name category {{ ... on Category {{ name _additional {{ id }} }} }} }} }} }} # Sau đó query products cùng category: # Get {{ # Product( # nearObject: {{ # id: "" # }} # limit: 20 # ) {{ # name # }} # }} """ def execute_query(self, gql_query: str) -> Dict: """Execute GraphQL query và return results""" result = client.query.raw(gql_query) return result def execute_batch_queries( self, queries: List[str] ) -> List[Dict]: """ Execute multiple queries trong một request sử dụng BATCH. Giảm RTT và improve throughput. """ batch_query = " ".join( f'_res{i}: ' + q.strip().strip('{{').strip('}}') for i, q in enumerate(queries) ) batch_gql = f""" {{ Get {{ {batch_query} }} }} """ return client.query.raw(batch_gql)

Benchmark function

def benchmark_queries(num_runs: int = 100): """Benchmark different query types""" import time queries = ProductGraphQLQueries() query_types = { "near_text": queries.get_near_text_query("wireless headphones"), "hybrid_filter": queries.get_hybrid_with_filter( "noise cancellation", "category-uuid", 50, 300 ), "near_object": queries.get_near_object_similar("product-uuid"), } results = {} for name, query in query_types.items(): times = [] for _ in range(num_runs): start = time.perf_counter() queries.execute_query(query) elapsed = (time.perf_counter() - start) * 1000 times.append(elapsed) results[name] = { "avg_ms": round(sum(times) / len(times), 2), "p50_ms": round(sorted(times)[len(times)//2], 2), "p95_ms": round(sorted(times)[int(len(times)*0.95)], 2), "p99_ms": round(sorted(times)[int(len(times)*0.99)], 2), } return results

Run benchmark

if __name__ == "__main__": print("Running GraphQL query benchmarks...") bench_results = benchmark_queries(100) print("\n📊 Benchmark Results (100 runs each):") print("-" * 60) for query_type, stats in bench_results.items(): print(f"{query_type}:") print(f" Avg: {stats['avg_ms']:.2f}ms") print(f" P50: {stats['p50_ms']:.2f}ms") print(f" P95: {stats['p95_ms']:.2f}ms") print(f" P99: {stats['p99_ms']:.2f}ms")

Tối Ưu Hiệu Suất & Quản Lý Tài Nguyên

Vector Index Tuning

Weaviate sử dụng HNSW (Hierarchical Navigable Small World) cho vector indexing. Tuning đúng parameters có thể improve latency 3-5x:

# performance_tuning.py

Advanced performance tuning cho production workloads

import weaviate from weaviate.classes.config import Configure, DataType, Property import time from typing import Dict, List import statistics class PerformanceTuner: """ Utility class để tune và benchmark Weaviate performance. """ # Benchmark configs BENCHMARK_CONFIGS = { "conservative": { "efConstruction": 128, "maxConnections": 16, "ef": 64, "description": "Lower memory, faster indexing" }, "balanced": { "efConstruction": 256, "maxConnections": 64, "ef": 128, "description": "Good balance speed/accuracy" }, "aggressive": { "efConstruction": 512, "maxConnections": 128, "ef": 256, "description": "Best recall, higher memory" }, "production": { "efConstruction": 512, "maxConnections": 128, "ef": 512, "m": 32, # HNSW M parameter "description": "Optimized for production workloads" } } def __init__(self, client: weaviate.Client): self.client = client def benchmark_recall_vs_speed( self, class_name: str, ground_truth: List[str], query_vectors: List[List[float]], configs: Dict = None ) -> Dict: """ Benchmark recall và speed cho different index configs. Ground truth là list of correct result IDs. """ if configs is None: configs = self.BENCHMARK_CONFIGS results = {} for config_name, config in configs.items(): print(f"\n🔧 Testing config: {config_name}") print(f" Description: {config['description']}") # Update index config self._update_index_config(class_name, config) # Benchmark queries latencies = [] recalls = [] for i, (query_vec, truth) in enumerate( zip(query_vectors, ground_truth) ): start = time.perf_counter() response = self.client.query.get( class_name, ["id"] ).with_near_vector( {"vector": query_vec} ).with_limit(20).do() latency = (time.perf_counter() - start) * 1000 latencies.append(latency) # Calculate recall result_ids = [ item["id"] for item in response["data"]["Get"][class_name] ] recall = len(set(result_ids) & set(truth)) / len(truth) recalls.append(recall) results[config_name] = { "config": config, "avg_latency_ms": statistics.mean(latencies), "p95_latency_ms": sorted(latencies)[int(len(latencies)*0.95)], "avg_recall": statistics.mean(recalls), "config_description": config["description"] } print(f" Avg Latency: {results[config_name]['avg_latency_ms']:.2f}ms") print(f" P95 Latency: {results[config_name]['p95_latency_ms']:.2f}ms") print(f" Avg Recall: {results[config_name]['avg_recall']:.3f}") return results def _update_index_config(self, class_name: str, config: Dict): """Update vector index configuration""" try: # Update class configuration self.client.schema.update_config( class_name, {"vectorIndexConfig": config} ) except Exception as e: print(f"⚠️ Config update failed (may need recreation): {e}") def get_optimized_schema( self, class_name: str, workload: str = "search" # "search", "recommendation", "deduplication" ) -> Dict: """ Get optimized schema configuration dựa trên workload type. """ base_properties = [ Property(name="name", data_type=DataType.TEXT), Property(name="description", data_type=DataType.TEXT), Property(name="price", data_type=DataType.NUMBER), ] if workload == "search": # Optimized cho search latency return { "class": class_name, "vectorizer": "text2vec-transformers", "properties": base_properties, "vectorIndexConfig": Configure.VectorIndex.hnsw( distance=Configure.VectorIndex.Distance.COSINE, efConstruction=512, ef=512, maxConnections=128, m=48 ), "invertedIndexConfig": Configure.InvertedIndexConfig( bm25_k1=1.5, bm25_b=0.75, cleanupIntervalSeconds=60 ) } elif workload == "recommendation": # Optimized cho high recall return { "class": class_name, "vectorizer": "text2vec-transformers", "properties": base_properties, "vectorIndexConfig": Configure.VectorIndex.hnsw( distance=Configure.VectorIndex.Distance.COSINE, efConstruction=512, ef=1024, # Higher ef for better recall maxConnections=64, m=64 ) } else: raise ValueError(f"Unknown workload: {workload}") def recommend_settings(self, data_stats: Dict) -> Dict: """ Recommend settings dựa trên data statistics. Args: data_stats: Dict với keys: - num_objects: int - avg_vector_dim: int - query_qps: float - p99_latency_target_ms: float """ num_objects = data_stats.get("num_objects", 1_000_000) query_qps = data_stats.get("query_qps", 100) target_p99 = data_stats.get("p99_latency_target_ms", 50) # Estimate memory usage vector_dim = data_stats.get("avg_vector_dim", 1536) memory_per_vector = vector_dim * 4 # float32 estimated_memory_gb = (num_objects * memory_per_vector) / (1024**3) # Calculate optimal ef # Rule of thumb: ef ≈ target_p99_latency_in_ms * 2 optimal_ef = min(int(target_p99 * 3), 1024) recommendations = { "estimated_memory_gb": round(estimated_memory_gb, 2), "recommended_ef": optimal_ef, "recommended_m": min(64, max(16, num_objects // 100_000)), "recommended_cache_size_mb": min( 2048, # Max 2GB cache query_qps * optimal_ef * 8 # Rough estimate ), "shards_recommendation": max( 1, int(num_objects / 5_000_000) # 1 shard per 5M objects ) } return recommendations

Usage example

if __name__ == "__main__": client = weaviate.Client("http://localhost:8080") tuner = PerformanceTuner(client) # Get recommendations cho 10M object dataset data_stats = {