Product Traceability System - Developer Guide
⚠️ CRITICAL ARCHITECTURE NOTICE
Hash-Only Mode (Default since 2024)
The Product Trace API now uses Hash-Only Mode by default, which provides deterministic, millisecond-level matching based on barcode and OCR text hashes:
Hash-Only Mode Pipeline (Default)
├── 🔴 Stage 1: Barcode Exact Match (<10ms)
│ └── Match success → Direct confirm, terminate early
├── 🟡 Stage 2: Text Hash Match (<10ms)
│ └── Text hash 100% match → Direct confirm
└── ❌ No match → Return verified=falseTo enable legacy visual similarity matching, set environment variable SCAN_HASH_ONLY_MODE=false.
Legacy Mode (Visual Similarity)
Deprecation Notice
Legacy mode with DINOv2 visual features is deprecated and should only be used when:
- Products have no barcode or OCR text
- Visual similarity matching is specifically required
DINOv2 global features CANNOT be used for final product verification alone.
Test Results (Synthetic Products):
| Metric | Value | Production Requirement |
|---|---|---|
| False Positive Rate | 100% | < 0.1% |
| Same Product Similarity | 0.877 | > 0.9 |
| Different Product Similarity | 0.811 | < 0.5 |
| Classification Margin | 0.066 | > 0.3 |
Root Cause:
DINOv2 extracts global semantic features - products with similar colors/patterns produce highly overlapping feature vectors. The 0.066 margin is insufficient for reliable discrimination.
Legacy Architecture:
Product Verification Pipeline (Legacy Mode)
├── 🔴 Stage 1: Barcode Exact Match (0.1ms, 99.99% accuracy)
│ └── Match success → Direct confirm, terminate early
├── 🟡 Stage 2: DINOv2 Coarse Filtering (50ms, 85% recall)
│ └── Similarity < 0.3 → Quick reject
│ └── Similarity ≥ 0.85 → High confidence candidate
│ └── 0.3 ~ 0.85 → Proceed to fine verification
├── 🟢 Stage 3: OCR Text Semantic Match (2-6s, 95% accuracy)
│ └── Text fingerprint 100% match → Direct confirm
│ └── Keyword subset match ≥ 0.8 → High confidence
│ └── TF-IDF vector similarity fusion
└── ⚖️ Final Decision: Weighted fusion + manual review recommendationDevelopment Guide
This guide provides detailed information about the architecture, design patterns, and implementation details of the Alaikis BI API platform, following the openspec development specification.
System Architecture
Overview
The Alaikis BI API is built on a microservices-inspired architecture using FastAPI with the following core components:
┌─────────────────────────────────────────────────────────┐
│ FastAPI Application │
├─────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Auth │ │ Routes │ │ Middleware │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Packing │ │ Product │ │ Address │ │
│ │ Service │ │ Trace Svc │ │ Parser │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Classif- │ │ Async │ │
│ │ ication │ │ Analysis │ │
│ │ Service │ │ Pool │ │
│ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Database │ │ Redis │ │ Model │ │
│ │ (PostgreSQL)│ │ Cache │ │ Registry │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘Tech Stack
| Component | Technology | Version |
|---|---|---|
| Web Framework | FastAPI | 0.100+ |
| Database | PostgreSQL | 14+ |
| ORM | SQLAlchemy | 2.0+ |
| Cache | Redis | 7.0+ |
| ML Framework | PyTorch | 2.0+ |
| Optimization | OR-Tools | 9.0+ |
| Address Parsing | libpostal | 1.1+ |
| Auth | Casdoor OAuth2 + JWT | - |
| Test Framework | pytest | 7.0+ |
Core Services Architecture
Packing Service - 3-Layer Hybrid Architecture
The packing optimization service uses a unique three-layer architecture for optimal performance and correctness:
┌─────────────────────────────────────────────────────────┐
│ Layer 3: OR-Tools Mathematical Solver │
│ - Formal mathematical guarantees │
│ - Constraint Programming │
│ - Integer Linear Programming │
└─────────────────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────────────────┐
│ Layer 2: AI Model Prediction │
│ - SKU-pallet compatibility scoring │
│ - Placement sequence optimization │
│ - Pattern recognition from historical data │
└─────────────────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Rule Engine │
│ - Pre-assigned pallet handling │
│ - Unavailable pallet filtering │
│ - Dangerous goods isolation │
│ - Stacking constraints │
└─────────────────────────────────────────────────────────┘Implementation Details
# api/service/intelligent_packing_service.py
class IntelligentPackingService:
def __init__(self, use_ai: bool = True):
self.use_ai = use_ai
self.rule_engine = RuleEngine()
self.ai_model = AIMatchingModel() if use_ai else None
self.or_solver = ORToolsSolver()
def pack(self, skus, pallets, **kwargs):
# 1. Apply rules first
processed = self.rule_engine.process(skus, pallets, **kwargs)
# 2. AI prediction if enabled
if self.use_ai:
scored_pallets = self.ai_model.predict_compatibility(
processed['skus'],
processed['pallets']
)
else:
scored_pallets = processed['pallets']
# 3. OR-Tools mathematical optimization
result = self.or_solver.solve(
skus=processed['skus'],
pallets=scored_pallets,
time_limit_ms=kwargs.get('time_limit_ms', 30000)
)
return resultProduct Trace Service - Multi-Modal Pipeline
The product trace service uses a multi-modal feature extraction and verification pipeline:
┌─────────────────────┐
│ Input Image(s) │
└──────────┬──────────┘
│
┌──────────────────┼──────────────────┐
↓ ↓ ↓
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Barcode Detector│ │ OCR Extractor │ │ Feature Extractor│
│ (ZBar + OpenCV) │ │ (Tesseract) │ │ (ResNet + CLIP) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└────────────────────┼────────────────────┘
↓
┌─────────────────────┐
│ Feature Fusion │
│ - Vector averaging │
│ - Confidence weight│
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Database Storage │
│ - pgvector for ANN │
│ - JSON for raw data│
└─────────────────────┘Three-Stage Verification Pipeline with Early Termination
Following the openspec specification, the verification pipeline implements early termination at each stage for performance optimization:
# Verification Flow
def verify_product(query_image, sku, identification_code):
"""Three-stage verification pipeline with early termination
Stage 1: Fast barcode matching (< 10ms) - RETURN immediately if found
Stage 2: Global feature quick match (< 100ms) - RETURN early if confident
Stage 3: Fine-grained verification (2-6 seconds) - only if needed
"""
# Stage 1: Fast barcode matching - EARLY TERMINATION
barcodes = barcode_service.detect(query_image)
for barcode in barcodes:
if matches_criteria(barcode, identification_code):
return VERIFIED, 1.0, "barcode_match" # ← EARLY RETURN
# Stage 2: Global feature quick match - EARLY TERMINATION POINTS
query_features = feature_service.extract_global(query_image)
candidates = vector_db.search(query_features, top_k=50)
# Early termination: No candidates found
if len(candidates) == 0:
return NOT_VERIFIED, 0.0, "no_global_candidates" # ← EARLY RETURN
# Early termination: Exactly one candidate
if len(candidates) == 1:
return VERIFIED, candidates[0].similarity, "single_candidate" # ← EARLY RETURN
# Early termination: Single high-confidence match above threshold
best_match = find_best_candidate(candidates, sku, identification_code)
if best_match and best_match.similarity > fast_accept_threshold:
return VERIFIED, best_match.similarity, "fast_feature_match" # ← EARLY RETURN
# Stage 3: Fine-grained verification - only reached if necessary
fine_similarity = fine_grained_service.compare(query_image, best_match.images)
ocr_match = ocr_service.compare_text(query_image, best_match.ocr_data)
final_score = weighted_combination(fine_similarity, ocr_match)
if final_score > final_threshold:
return VERIFIED, final_score, "fine_grained_match"
else:
return NOT_VERIFIED, final_score, "no_match"DINOv2 Lightweight Feature Extraction Architecture
Overview
The product trace service now supports an end-to-end DINOv2 feature extraction pipeline that replaces the traditional dual-channel (global + fine-grained) architecture with a single unified vision transformer model.
Architecture Comparison
| Aspect | Traditional Architecture | DINOv2 Architecture |
|---|---|---|
| Feature Dimension | 2304D (2048D global + 256D local) | 384D unified |
| Inference Speed | ~250ms/image | ~50ms/image (5x faster) |
| Peak Memory | ~1.2GB | ~500MB (60% saving) |
| Model Count | 2+ (ResNet + SuperPoint) | 1 (ViT-S/14) |
| Accuracy Trade-off | Highest accuracy | Slightly lower (-1-2%) but sufficient for most use cases |
| Batch Processing | Limited | Native support (matrix multiplication) |
DINOv2 Feature Pipeline
┌─────────────────────┐
│ Input Image(s) │
└──────────┬──────────┘
│
↓
┌─────────────────────┐
│ Image Preprocessor │
│ - Resize (224x224) │
│ - Normalization │
│ - Batch support │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ DINOv2 ViT-S/14 │
│ - Self-supervised │
│ - End-to-end │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ L2 Normalization │
│ - Unit vector │
│ - Cosine = dot │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ 384D Feature Vector│
└─────────────────────┘Implementation Details
# api/service/dinov2_feature_service.py
class DINOv2FeatureService:
"""DINOv2-based end-to-end feature extraction service
Replaces traditional global + fine-grained dual-channel architecture
with single unified ViT model for 5x faster inference.
"""
def __init__(self, model_name: str = "dinov2_vits14",
use_onnx: bool = True, device: str = "auto"):
self.model_name = model_name
self.use_onnx = use_onnx
self.device = self._get_device(device)
self.model = None
self.transform = None
self._model_lock = threading.Lock()
self._model_loaded = False
def extract_features(self, image: Image.Image,
normalize: bool = True) -> np.ndarray:
"""Extract DINOv2 feature vector from single image
Args:
image: PIL Image (any size)
normalize: Whether to L2 normalize the output vector
Returns:
384D numpy array of features
"""
self._ensure_model_loaded()
# Preprocess
img_tensor = self.transform(image).unsqueeze(0).to(self.device)
# Inference
with torch.no_grad():
features = self.model(img_tensor)
# Convert to numpy
features_np = features.cpu().numpy().squeeze()
# Normalize for cosine similarity
if normalize:
norm = np.linalg.norm(features_np)
if norm > 0:
features_np = features_np / norm
return features_np
def extract_batch_features(self, images: List[Image.Image],
normalize: bool = True) -> np.ndarray:
"""Extract features from batch of images (N x 384D matrix)
Optimized for database scanning operations - 10x faster than loop processing.
"""
self._ensure_model_loaded()
batch_tensors = torch.stack([
self.transform(img) for img in images
]).to(self.device)
with torch.no_grad():
features = self.model(batch_tensors)
features_np = features.cpu().numpy()
if normalize:
norms = np.linalg.norm(features_np, axis=1, keepdims=True)
features_np = features_np / np.maximum(norms, 1e-8)
return features_np
def compute_similarity(self, query_vector: np.ndarray,
product_vectors: np.ndarray) -> np.ndarray:
"""Compute cosine similarity using matrix multiplication
For normalized vectors, cosine similarity equals dot product.
Query: (384,) vector
Products: (N, 384) matrix
Result: (N,) similarity scores
"""
# Matrix multiplication for batch similarity - O(N) operation
similarities = product_vectors @ query_vector
return similaritiesONNX Deployment & Quantization
For production deployment, export the DINOv2 model to ONNX format with INT8 quantization:
# scripts/export_dinov2_onnx.py
def export_to_onnx(model_name: str = "dinov2_vits14",
output_path: str = "models/dinov2_vits14.onnx",
quantize: bool = True):
"""Export DINOv2 model to ONNX with optional INT8 quantization
Args:
quantize: Apply INT8 dynamic range quantization for CPU optimization
- Reduces model size ~75%
- Improves inference speed ~2-3x
- Minimal accuracy loss (< 0.5%)
"""
model = torch.hub.load('facebookresearch/dinov2', model_name)
model.eval()
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy_input,
output_path,
export_params=True,
opset_version=14,
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
if quantize:
quantized_path = output_path.replace('.onnx', '_int8.onnx')
onnx_model = onnx.load(output_path)
quantized_model = quantize_dynamic(
onnx_model,
weight_type=QuantType.QInt8
)
onnx.save(quantized_model, quantized_path)DINOv2 Integrated Verification Pipeline
The main VerificationService now uses DINOv2 as its primary feature extraction backend, fully integrated with the cascaded filtering pipeline and all 9 early termination checkpoints.
Initialization & Architecture
# api/service/verification_service.py
class VerificationService:
"""Unified verification service using DINOv2 feature extraction
Replaces traditional global + fine-grained dual-channel architecture
with single DINOv2 ViT model for 5x faster inference while maintaining
full cascaded filtering and early termination support.
Architecture:
- Barcode detection (ZBar + OpenCV) - EARLY TERMINATION
- DINOv2 feature extraction (ViT-S/14) - 384D unified vector
- OCR text extraction (Tesseract)
- Weighted fusion (DINOv2 weight: 0.8, OCR weight: 0.2)
"""
def __init__(self,
dinov2_feature_service,
ocr_service,
barcode_service,
db_session):
self.dinov2_feature_service = dinov2_feature_service
self.ocr_service = ocr_service
self.barcode_service = barcode_service
self.db = db_session
self.logger = logging.getLogger(__name__)DINOv2 Scan Pipeline with Early Termination
def scan_top_k(self, query_image: Image.Image,
top_k: int = 10,
similarity_threshold: float = 0.7,
enable_early_termination: bool = True) -> dict:
"""Scan using DINOv2 with all 9 early termination checkpoints
Follows openspec performance optimization standards.
"""
start_time = time.time()
stages_completed = []
# ========== STAGE 1: BARCODE DETECTION ==========
# Checkpoint 1: Non-background barcode exact match
barcodes = self.barcode_service.detect(query_image)
stages_completed.append("barcode_detection")
for barcode in barcodes:
matches = self._find_products_by_barcode(barcode.data)
if len(matches) >= 1 and enable_early_termination:
return {
"matches": matches[:top_k],
"total_count": len(matches),
"exit_reason": "barcode_exact",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# Checkpoint 2: Background barcode mode matched >= top_k
if len(barcodes) >= top_k and enable_early_termination:
background_matches = self._find_background_barcode_matches(barcodes)
if len(background_matches) >= top_k:
return {
"matches": background_matches[:top_k],
"total_count": len(background_matches),
"exit_reason": "background_barcode",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# Checkpoint 3: Exactly one barcode match remaining
barcode_matches = self._find_all_barcode_matches(barcodes)
if len(barcode_matches) == 1 and enable_early_termination:
return {
"matches": barcode_matches,
"total_count": 1,
"exit_reason": "barcode_single_candidate",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# ========== STAGE 2: DINOv2 GLOBAL FEATURE FILTERING ==========
stages_completed.append("dinov2_feature_extraction")
# Extract DINOv2 feature vector - single forward pass (~50ms)
query_dinov2 = self.dinov2_feature_service.extract_features(query_image)
# Load all product DINOv2 vectors from database
all_products = self._load_all_product_features()
if not all_products:
return {
"matches": [],
"total_count": 0,
"exit_reason": "no_products",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# BATCH SIMILARITY: Matrix multiplication optimization
# O(N) operation - 10x faster than loop-based calculation
product_matrix = np.vstack([p["dinov2_vector"] for p in all_products])
similarities = product_matrix @ query_dinov2
# Filter by similarity threshold
filtered_by_dinov2 = []
for i, product in enumerate(all_products):
sim = float(similarities[i])
if sim >= similarity_threshold:
filtered_by_dinov2.append({
"product": product,
"dinov2_similarity": sim
})
stages_completed.append("dinov2_filtering")
# Checkpoint 4: No candidates after DINOv2 filtering
if len(filtered_by_dinov2) == 0 and enable_early_termination:
return {
"matches": [],
"total_count": 0,
"exit_reason": "no_candidates",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# Checkpoint 5: Exactly one candidate after DINOv2 filtering
if len(filtered_by_dinov2) == 1 and enable_early_termination:
return {
"matches": [{
"sku": filtered_by_dinov2[0]["product"]["sku"],
"similarity_score": filtered_by_dinov2[0]["dinov2_similarity"],
"match_method": "dinov2_single_candidate"
}],
"total_count": 1,
"exit_reason": "global_single_candidate",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# Checkpoint 6: Filtered candidates count <= top_k
if len(filtered_by_dinov2) <= top_k and enable_early_termination:
matches = [{
"sku": item["product"]["sku"],
"similarity_score": item["dinov2_similarity"],
"match_method": "dinov2_filtered"
} for item in filtered_by_dinov2]
return {
"matches": matches,
"total_count": len(matches),
"exit_reason": "candidates_within_k",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# ========== STAGE 3: OCR TEXT FINGERPRINT MATCHING ==========
stages_completed.append("ocr_extraction")
query_ocr_text = self.ocr_service.extract_text(query_image)
query_text_hash = self._compute_text_fingerprint(query_ocr_text)
# Checkpoint 7: Exact OCR text hash match
text_hash_matches = []
for item in filtered_by_dinov2:
product_text_hash = item["product"].get("text_hash")
if product_text_hash and product_text_hash == query_text_hash:
text_hash_matches.append(item)
if len(text_hash_matches) >= 1 and enable_early_termination:
matches = [{
"sku": item["product"]["sku"],
"similarity_score": 1.0,
"match_method": "text_hash_exact"
} for item in text_hash_matches[:top_k]]
return {
"matches": matches,
"total_count": len(matches),
"exit_reason": "text_hash_exact",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# ========== STAGE 4: HIGH-CONFIDENCE VISUAL MATCH ==========
stages_completed.append("high_confidence_check")
# Checkpoint 8: Single high-confidence visual match (>= 0.9)
high_conf_matches = [
item for item in filtered_by_dinov2
if item["dinov2_similarity"] >= 0.9
]
if len(high_conf_matches) == 1 and enable_early_termination:
return {
"matches": [{
"sku": high_conf_matches[0]["product"]["sku"],
"similarity_score": high_conf_matches[0]["dinov2_similarity"],
"match_method": "visual_high_confidence"
}],
"total_count": 1,
"exit_reason": "visual_high_confidence",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": True,
"stages_completed": stages_completed
}
# ========== STAGE 5: FULL VERIFICATION LOOP ==========
stages_completed.append("full_verification")
# Compute final similarity with OCR fusion
final_results = []
for item in filtered_by_dinov2:
product = item["product"]
# Weighted fusion: DINOv2 (0.8) + OCR (0.2)
product_ocr_data = product.get("ocr_data", "")
ocr_similarity = self.ocr_service.compare_text(
query_ocr_text, product_ocr_data
)
final_similarity = (
item["dinov2_similarity"] * 0.8 +
ocr_similarity * 0.2
)
final_results.append({
"sku": product["sku"],
"similarity_score": final_similarity,
"match_method": "dinov2_ocr_fusion",
"dinov2_similarity": item["dinov2_similarity"],
"ocr_similarity": ocr_similarity
})
# Sort and take top K
final_results.sort(key=lambda x: x["similarity_score"], reverse=True)
top_results = final_results[:top_k]
# Checkpoint 9: Loop early break - collected enough matches
early_break = len(top_results) >= top_k and enable_early_termination
return {
"matches": top_results,
"total_count": len(final_results),
"exit_reason": "loop_early_break" if early_break else "full_pipeline",
"processing_time_ms": (time.time() - start_time) * 1000,
"early_termination": early_break,
"stages_completed": stages_completed
}Migration Guide from Traditional Features
Step 1: Export DINOv2 ONNX Model
python scripts/export_dinov2_onnx.py --model dinov2_vits14 --quantizeStep 2: Migrate Database Schema
-- Add DINOv2 vector column
ALTER TABLE product_features
ADD COLUMN dinov2_vector vector(384);
-- Create vector index
CREATE INDEX product_features_dinov2_vector_idx
ON product_features
USING ivfflat (dinov2_vector vector_cosine_ops)
WITH (lists = 100);Step 3: Backfill Existing Features
# scripts/backfill_dinov2_features.py
def backfill_features():
"""Backfill DINOv2 features for existing product images"""
service = DINOv2FeatureService(use_onnx=True)
products = db.query(ProductFeature).filter(
ProductFeature.dinov2_vector.is_(None)
).all()
for product in products:
image = download_image(product.image_url)
features = service.extract_features(image)
product.dinov2_vector = features
db.commit()Step 4: Switch Verification Pipeline
# In verification service initialization
if os.getenv('USE_DINOV2', 'false').lower() == 'true':
dinov2_service = DINOv2FeatureService()
verification_service = DINOv2VerificationService(
dinov2_service, db, ocr_service, barcode_service
)
else:
# Fallback to traditional pipeline
feature_service = FeatureExtractionService()
verification_service = VerificationService(
feature_service, fine_grained_service, ocr_service, barcode_service
)Performance Optimization Techniques
Matrix Multiplication Batch Similarity:
- Traditional loop: O(N * D) time complexity
- Matrix multiplication: O(N * D) with highly optimized BLAS
- Practical speedup: 10x faster for large N
ONNX Runtime Optimization:
- CPU: MKL-DNN / OpenVINO backend
- GPU: CUDA / TensorRT backend
- Quantization: INT8 for CPU, FP16 for GPU
Lazy Loading with Double-Checked Locking:
pythondef _ensure_model_loaded(self): if not self._model_loaded: with self._model_lock: if not self._model_loaded: self._load_model() self._model_loaded = TrueVector Normalization Pre-computation:
- Normalize at feature extraction time, not at comparison
- Cosine similarity = dot product for normalized vectors
- Saves sqrt operations during verification
Cascaded Filtering & Early Termination Architecture
Overview
Following the openspec "spec-driven development" philosophy, the product trace scan API implements a cascaded filtering pipeline with 9 early termination checkpoints to maximize performance while maintaining accuracy.
Pipeline Stages
┌─────────────────────────┐
│ Input Query Image │
└────────────┬────────────┘
│
┌───────────────────────┼───────────────────────┐
↓ ↓
┌────────────────────┐ ┌────────────────────┐
│ Barcode Matching │←─── STAGE 1 │ 0 or 1 match? │
│ (< 10ms) │ │ → RETURN NOW │
└────────┬───────────┘ └────────────────────┘
│
├───────────────────────────────────────────────────────┐
↓ ↓
┌────────────────────┐ ┌────────────────────┐
│ Global Feature │←─── STAGE 2 │ 0 candidates? │
│ Filtering │ │ → RETURN NOW │
│ (< 100ms) │ └────────────────────┘
└────────┬───────────┘
│
├───────────────────────────────────────────────────────┐
↓ ↓
┌────────────────────┐ ┌────────────────────┐
│ 1 Candidate Left? │←─── CHECKPOINT 3 │ ≤ top_k? │
│ → RETURN NOW │ │ → RETURN NOW │
└────────────────────┘ └────────────────────┘
│
├───────────────────────────────────────────────────────┐
↓ ↓
┌────────────────────┐ ┌────────────────────┐
│ OCR Text Hash │←─── STAGE 3 │ ≥1 hash match? │
│ Matching │ │ → RETURN NOW │
└────────┬───────────┘ └────────────────────┘
│
├───────────────────────────────────────────────────────┐
↓ ↓
┌────────────────────┐ ┌────────────────────┐
│ High-Confidence │←─── STAGE 4 │ 1 high-confidence? │
│ Visual Matching │ │ → RETURN NOW │
└────────┬───────────┘ └────────────────────┘
│
↓
┌────────────────────┐
│ Fine-Grained Loop │←─── STAGE 5
│ with Early Break │ → Break when matches ≥ top_k
└────────┬───────────┘
│
↓
┌────────────────────┐
│ Final Results │
│ (Only reached if │
│ no early exit) │
└────────────────────┘Early Termination Checkpoints
Following openspec's performance-first design principle, the verification service implements 9 distinct early termination checkpoints to minimize unnecessary computation:
| Checkpoint | Location | Condition | Performance Impact |
|---|---|---|---|
| 1 | Barcode stage | Non-background barcode matching found ≥ 1 product | Fastest path (< 10ms), highest priority |
| 2 | Background barcode stage | Background barcode matching ≥ top_k products | Fast return for ambiguous barcode scenarios |
| 3 | Background barcode stage | Exactly 1 barcode match remaining | Single candidate after barcode detection |
| 4 | Global filter stage | 0 candidates remaining after filtering | Saves all downstream processing |
| 5 | Global filter stage | Exactly 1 candidate remaining | Saves OCR + fine-grained stages entirely |
| 6 | Global filter stage | Candidates ≤ top_k after filtering | Direct return without heavy computation |
| 7 | OCR hash stage | ≥1 matching text hash found | Skips fine-grained visual comparison stage |
| 8 | High-confidence stage | Exactly 1 match with similarity ≥ 0.9 | Fast verification for unambiguous visual matches |
| 9 | Fine-grained loop | Matches collected ≥ top_k | Breaks loop early, saves unnecessary iterations |
Async Analysis Pool Architecture
Overview
The Async Analysis Pool manages background processing for feature extraction with multiple executor pools and timeout protection:
┌─────────────────────────────────────────────────────────┐
│ AsyncAnalysisPool │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Barcode │ │ OCR │ │ Feature │ │
│ │ Executor │ │ Executor │ │ Executor │ │
│ │ (8 workers) │ │ (4 workers) │ │ (2 workers) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ↓ │
│ ┌───────────────────────────┐ │
│ │ Task Queue │ │
│ │ (asyncio.Queue) │ │
│ └─────────────┬─────────────┘ │
│ ↓ │
│ ┌───────────────────────────┐ │
│ │ Status Tracker │ │
│ │ (progress, sub-tasks) │ │
│ └───────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────┐ │
│ │ Timeout Monitor │ │
│ │ (300s max processing) │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────────────────────┘Implementation Details
# api/service/async_analysis_pool.py
class AsyncAnalysisPool:
def __init__(self, feature_service, fine_grained_service,
ocr_service, barcode_service):
self.feature_service = feature_service
self.fine_grained_service = fine_grained_service
self.ocr_service = ocr_service
self.barcode_service = barcode_service
# Executor pools
self.barcode_executor = ThreadPoolExecutor(max_workers=8)
self.ocr_executor = ThreadPoolExecutor(max_workers=4)
self.feature_executor = ThreadPoolExecutor(max_workers=2)
# Concurrency limiting - follows openspec degradation strategy
self.max_concurrent_no_barcode = int(os.getenv(
'MAX_CONCURRENT_NO_BARCODE', '3'
))
self.current_no_barcode_count = 0
self._concurrency_lock = asyncio.Lock()
# Task tracking
self._task_status = {} # record_id -> ProcessingStatus
self._task_futures = {} # record_id -> Future
self._lock = asyncio.Lock()
self._timeout_monitor_task = None
async def submit_task(self, record_id, images):
"""Submit a new analysis task with concurrency control"""
async with self._lock:
self._task_status[record_id] = ProcessingStatus(
status="pending",
progress=0,
message="Task queued"
)
# Apply degradation strategy for no-barcode scenarios
if await self._should_degrade():
raise ServiceUnavailable(
"High concurrency detected, please try again later"
)
# Submit to thread pool
future = self._loop.run_in_executor(
None,
self._process_task,
record_id,
images
)
self._task_futures[record_id] = future
def _process_task(self, record_id, images):
"""Process in thread pool - parallel feature extraction"""
loop = asyncio.new_event_loop()
# Run all extractions in parallel
results = loop.run_until_complete(asyncio.gather(
self._extract_barcodes(images),
self._extract_ocr(images),
self._extract_features(images),
self._extract_fine_grained(images)
))
barcode_result, ocr_result, feature_result, fine_result = results
# Fuse features
fused = self._fuse_features(*results)
# Update database
self._save_results(record_id, fused)
# Update status
self._update_status(record_id, "completed", progress=100)
async def get_status(self, record_id):
"""Get current processing status"""
async with self._lock:
return self._task_status.get(record_id)Database Schema
Core Tables
-- Users table
CREATE TABLE users (
id SERIAL PRIMARY KEY,
casdoor_id VARCHAR(255) UNIQUE,
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
full_name VARCHAR(255),
password_hash VARCHAR(255),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- API Tokens
CREATE TABLE tokens (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
name VARCHAR(255),
token VARCHAR(255) UNIQUE NOT NULL,
scopes JSONB,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP
);
-- Product Features (with vector support)
CREATE TABLE product_features (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
sku VARCHAR(255) NOT NULL,
identification_code VARCHAR(255) NOT NULL,
image_url VARCHAR(2048),
image_hash VARCHAR(64),
product_name VARCHAR(255),
description TEXT,
category VARCHAR(100),
-- Feature vectors
global_vector vector(512),
fine_grained_vector vector(1024),
-- Extracted data
barcode_data JSONB,
ocr_data JSONB,
text_hash VARCHAR(64),
-- Processing flags
has_barcode BOOLEAN DEFAULT FALSE,
has_ocr BOOLEAN DEFAULT FALSE,
has_fusion BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, sku, identification_code)
);
-- Verification History
CREATE TABLE verification_history (
id SERIAL PRIMARY KEY,
product_feature_id INTEGER REFERENCES product_features(id),
query_image_url VARCHAR(2048),
similarity_score FLOAT,
threshold FLOAT,
is_verified BOOLEAN,
verification_method VARCHAR(50), -- barcode, global, fine_grained
match_details JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Vector index for ANN search
CREATE INDEX product_features_global_vector_idx
ON product_features
USING ivfflat (global_vector vector_cosine_ops)
WITH (lists = 100);Database Migrations
The system uses automatic migration for missing fields:
# api/core/entity/auto_migration.py
def auto_migrate_missing_fields(engine, base):
"""Automatically detect and add missing columns"""
inspector = inspect(engine)
for table_name, table in base.metadata.tables.items():
existing_columns = {
col['name'] for col in inspector.get_columns(table_name)
}
for column in table.columns:
if column.name not in existing_columns:
# Add missing column
with engine.connect() as conn:
conn.execute(text(f"""
ALTER TABLE {table_name}
ADD COLUMN {column.name} {column.type.compile(engine.dialect)}
"""))
conn.commit()
# Also create missing indexes
for table_name, table in base.metadata.tables.items():
existing_indexes = {
idx['name'] for idx in inspector.get_indexes(table_name)
}
for index in table.indexes:
if index.name not in existing_indexes:
index.create(engine)Authentication System
Dual Authentication Methods
# api/utils/auth.py
async def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> User:
"""
Unified authentication supporting:
1. JWT Bearer tokens (from Casdoor)
2. API Key tokens (from tokens table)
"""
token = credentials.credentials
# Try JWT first
try:
casdoor_id = verify_token(token)
if casdoor_id:
user = db.query(User).filter(
User.casdoor_id == casdoor_id
).first()
if user and user.is_active:
return user
except JWTError:
pass
# Try API Key
api_token = db.query(Token).filter(
Token.token == token,
Token.is_active == True
).first()
if api_token:
user = db.query(User).get(api_token.user_id)
if user and user.is_active:
return user
raise HTTPException(
status_code=401,
detail="Invalid authentication credentials"
)JWT Token Flow
# api/utils/jwt.py
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode,
SECRET_KEY,
algorithm=ALGORITHM
)
return encoded_jwt
def verify_token(token: str) -> Optional[str]:
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM]
)
casdoor_id: str = payload.get("sub")
if casdoor_id is None:
return None
return casdoor_id
except JWTError:
return Noneopenspec Test Organization Specification
Following the Superpowers microservices architecture principles, the project adheres to a strict test organization convention.
Directory Structure
tests/
├── unit/ # Unit tests for individual components
│ ├── test_packing_algorithm.py
│ ├── test_feature_extraction.py
│ ├── test_barcode_service.py
│ └── test_ocr_service.py
├── integration/ # Integration tests for API endpoints and workflows
│ ├── test_comprehensive_validation.py
│ ├── test_product_trace_flow.py
│ └── test_cascaded_filtering.py
├── api/ # API-specific tests using TestClient or requests
│ ├── test_api_availability.py
│ ├── test_api_response_structure.py
│ └── test_endpoints.py
├── e2e/ # End-to-end tests for complete user flows
│ └── test_user_workflow.py
├── data/ # Test data fixtures and JSON files
│ ├── test_skus.json
│ ├── test_images/
│ └── user_test_request.json
├── reports/ # Generated test reports and benchmark results
│ ├── packing_api_comprehensive_analysis.md
│ └── intelligent_packing_test_report.md
├── static/ # Static test resources (logs, images, etc.)
│ └── logs/
└── archive/ # Deprecated legacy test files for reference
└── old_test_files/
scripts/
├── testing/ # Debug scripts, stress tests, and development utilities
│ ├── stress_test.py
│ ├── debug_simple.py
│ └── test_with_testjson.py
└── deployment/ # Deployment validation and health check scripts
└── deploy_check.pyFile Naming Conventions
| File Type | Pattern | Example |
|---|---|---|
| Formal test files | test_*.py | test_packing_calculate.py |
| Development/debug scripts | debug_*.py | debug_feature_extraction.py |
| Performance/stress tests | stress_*.py | stress_scan_api.py |
File Classification Rules
Formal Tests (tests/):
- Packing algorithm tests →
tests/unit/ - API integration tests →
tests/integration/ - Product trace tests →
tests/integration/ - Feature extraction tests →
tests/unit/ - Database tests →
tests/integration/
Debug Utilities (scripts/testing/):
- All
debug_*.pyfiles - Simple verification scripts
- Model weight inspection tools
Legacy Archive (tests/archive/):
- Deprecated test files
- Obsolete debug scripts
Test Execution Commands
# Unit tests only
pytest tests/unit/ -v
# Integration tests with timeout
pytest tests/integration/ -v --timeout=60
# API-specific tests
pytest tests/api/ -v
# All tests with short tracebacks
pytest tests/ -v --tb=short
# Generate test report
pytest tests/ --json=tests/reports/test_results.jsonSuperpowers Test Conventions
- Hermetic Tests: All tests must be self-contained and independent
- FastAPI Integration: Use TestClient for FastAPI integration tests
- Performance Benchmarks: Include performance benchmarks for critical paths
- Machine-Readable Reports: Generate JSON reports for CI/CD pipelines
- Test Data Management: Maintain test data fixtures in
tests/data/ - Archive Policy: Archive deprecated tests instead of deleting
- Separation of Concerns: Keep debug utilities separate from formal test suites
Development Workflow
Local Development Setup
# 1. Clone the repository
git clone <repository-url>
cd api/backend
# 2. Create virtual environment
python -m venv .venv
source .venv/bin/activate # Linux/Mac
.venv\Scripts\activate # Windows
# 3. Install dependencies
pip install -r requirements.txt
# 4. Set up environment variables
cp .env.example .env
# Edit .env with your configuration
# 5. Start PostgreSQL and Redis
docker-compose up -d postgres redis
# 6. Run the application
python main.pyCode Quality Standards
# Run type checking
mypy api/
# Run linting
flake8 api/ --max-line-length=120
# Format code
black api/
# Run tests before commit
pytest tests/unit/ -vCI/CD Pipeline
The project follows a standard CI/CD workflow:
- Push: Trigger CI pipeline
- Lint: Code quality checks
- Test: Run unit and integration tests
- Build: Docker image creation
- Deploy: Staging → Production
Feature Branch Workflow
main (production)
↑
develop (integration)
↑
feature/xxx (feature branches)
↓
Submit PR → Code Review → Merge to develop