hisham-cmd commited on
Commit
5c90fa4
·
verified ·
1 Parent(s): db0d886

Upload app/api/ai_search.py

Browse files
Files changed (1) hide show
  1. app/api/ai_search.py +141 -0
app/api/ai_search.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, Query, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+ from typing import List, Optional
4
+
5
+ from app.db.base import get_db
6
+ from app.models.product import Product
7
+ from app.schemas.product import ProductListItem, PaginatedResponse
8
+ from app.api.auth import get_current_user
9
+ from app.services.ai_service import ai_service
10
+ from app.schemas.auth import AuthResponse
11
+
12
+ router = APIRouter(prefix="/ai", tags=["AI Integration"])
13
+
14
+ @router.post("/products/{product_id}/embed", response_model=AuthResponse)
15
+ def generate_product_embedding(
16
+ product_id: int,
17
+ current_user_id: int = Depends(get_current_user), # Real app needs Admin check
18
+ db: Session = Depends(get_db)
19
+ ):
20
+ """
21
+ Generate or regenerate the AI semantic vector embedding for a specific product.
22
+ Typically called after a product is created or updated.
23
+ """
24
+ product = db.query(Product).filter(Product.id == product_id).first()
25
+ if not product:
26
+ raise HTTPException(status_code=404, detail="Product not found")
27
+
28
+ # Create a rich text representation of the product for the model
29
+ text_to_embed = f"{product.name_en} {product.name_ar} {product.description_en} {product.description_ar}"
30
+
31
+ embedding = ai_service.generate_embedding(text_to_embed)
32
+ if not embedding:
33
+ raise HTTPException(status_code=500, detail="Failed to generate embedding")
34
+
35
+ product.embedding = embedding
36
+ db.commit()
37
+
38
+ return AuthResponse(
39
+ isSuccess=True,
40
+ value={"message": "Embedding generated successfully"},
41
+ statusCode=200
42
+ )
43
+
44
+
45
+ @router.post("/products/embed-all", response_model=AuthResponse)
46
+ def generate_all_embeddings(
47
+ current_user_id: int = Depends(get_current_user), # Real app needs Admin check
48
+ db: Session = Depends(get_db)
49
+ ):
50
+ """
51
+ Bulk generate embeddings for all active products that don't have one.
52
+ """
53
+ products = db.query(Product).filter(Product.is_active == True, Product.embedding == None).all()
54
+ count = 0
55
+
56
+ for product in products:
57
+ text_to_embed = f"{product.name_en} {product.name_ar} {product.description_en} {product.description_ar}"
58
+ embedding = ai_service.generate_embedding(text_to_embed)
59
+ if embedding:
60
+ product.embedding = embedding
61
+ count += 1
62
+
63
+ db.commit()
64
+
65
+ return AuthResponse(
66
+ isSuccess=True,
67
+ value={"message": f"Generated embeddings for {count} products"},
68
+ statusCode=200
69
+ )
70
+
71
+
72
+ @router.get("/search", response_model=PaginatedResponse)
73
+ def semantic_search(
74
+ q: str = Query(..., min_length=2, description="The search query"),
75
+ limit: int = Query(10, ge=1, le=50),
76
+ threshold: float = Query(0.3, ge=0.0, le=1.0, description="Minimum similarity score"),
77
+ db: Session = Depends(get_db)
78
+ ):
79
+ """
80
+ Perform a semantic search across products using AI embeddings.
81
+ """
82
+ query_embedding = ai_service.generate_embedding(q)
83
+ if not query_embedding:
84
+ raise HTTPException(status_code=500, detail="Failed to process search query")
85
+
86
+ # Fetch all active products with embeddings
87
+ # In a real heavy-duty app, we'd use pgvector or dedicated vector DB like Pinecone/Milvus
88
+ # For small/medium datasets, memory scan works fine.
89
+ products = db.query(Product).filter(Product.is_active == True, Product.embedding != None).all()
90
+
91
+ results = []
92
+ for p in products:
93
+ similarity = ai_service.calculate_similarity(query_embedding, p.embedding)
94
+ if similarity >= threshold:
95
+ results.append((similarity, p))
96
+
97
+ # Sort by similarity descending
98
+ results.sort(key=lambda x: x[0], reverse=True)
99
+
100
+ # Take top limit
101
+ top_results = results[:limit]
102
+
103
+ # Map to schema
104
+ items = []
105
+ for sim, p in top_results:
106
+ primary_image = None
107
+ if p.images:
108
+ sorted_imgs = sorted(p.images, key=lambda x: x.sort_order)
109
+ primary_image = sorted_imgs[0].image_url if sorted_imgs else None
110
+
111
+ item_dict = ProductListItem(
112
+ id=p.id,
113
+ name_ar=p.name_ar,
114
+ name_en=p.name_en,
115
+ price=p.price,
116
+ compare_price=p.compare_price,
117
+ stock=p.stock,
118
+ category_id=p.category_id,
119
+ rating=p.rating,
120
+ rating_count=p.rating_count,
121
+ is_featured=p.is_featured,
122
+ image_url=primary_image,
123
+ created_at=p.created_at,
124
+ ).model_dump()
125
+
126
+ item_dict["created_at"] = item_dict["created_at"].isoformat()
127
+ item_dict["similarity_score"] = round(sim, 4) # Add match score
128
+
129
+ items.append(item_dict)
130
+
131
+ return PaginatedResponse(
132
+ isSuccess=True,
133
+ value={
134
+ "items": items,
135
+ "total": len(items),
136
+ "page": 1,
137
+ "page_size": limit,
138
+ "total_pages": 1
139
+ },
140
+ statusCode=200
141
+ )