Commit 06c6f102 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

perf: optimize SQL queries and fix bugs

- Replace description_text_full with description_text in vector search (reduce token bloat)
- Add description_text to CASE 1 (code search) and CASE 3 (vector search)
- Fix generic word fallback: 'quần bán chạy' now filters correctly
- Derive GENERIC_WORDS from PRODUCT_LINE_MAP (auto-sync)
- Fix duplicate if include_product_ids in conversation_manager.py
- Add product_mapping.py with PRODUCT_LINE_MAP and resolve functions
parent 6e66cea0
......@@ -137,7 +137,7 @@ def format_product_results(products: list[dict]) -> list[dict]:
"sale_price": int(sale_price) if sale_price else int(original_price),
"url": web_url, # First color's URL
"thumbnail_image_url": thumb_url, # First color's thumbnail
"description": p.get("description_text_full", ""),
"description": p.get("description_text", ""),
}
# Include quantity_sold if available (for best seller)
qty_sold = p.get("quantity_sold")
......
"""
Product Line Mapping
Key = DB product_line_vn (chính xác)
Value = list các từ khách hàng hay dùng (synonym)
"""
# DB value → [các từ khách hàng hay gọi]
PRODUCT_LINE_MAP: dict[str, list[str]] = {
"Áo Sơ mi": ["áo sơ mi"],
"Áo Polo": ["áo polo", "áo cổ bẻ"],
"Áo phông": ["áo phông", "áo thun", "áo thun ngắn tay", "áo cổ v", "áo cổ tym"],
"Áo nỉ có mũ": ["áo nỉ có mũ"],
"Áo nỉ": ["áo nỉ"],
"Áo mặc nhà": ["áo mặc nhà"],
"Áo lót": ["áo lót", "áo bra", "áo ngực", "áo quây"],
"Áo len gilet": ["áo len gilet"],
"Áo len": ["áo len"],
"Áo kiểu": ["áo kiểu"],
"Áo khoác sợi": ["áo khoác sợi"],
"Áo khoác nỉ không mũ": ["áo khoác nỉ không mũ"],
"Áo khoác nỉ có mũ": ["áo khoác nỉ có mũ"],
"Áo khoác lông vũ": ["áo khoác lông vũ"],
"Áo khoác gió": ["áo khoác gió", "áo gió", "áo khoác mỏng"],
"Áo khoác gilet chần bông": ["áo khoác gilet chần bông"],
"Áo khoác gilet": ["áo khoác gilet"],
"Áo khoác dạ": ["áo khoác dạ"],
"Áo khoác dáng ngắn": ["áo khoác dáng ngắn"],
"Áo khoác chống nắng": ["áo khoác chống nắng"],
"Áo khoác chần bông": ["áo khoác chần bông"],
"Áo khoác": ["áo khoác"],
"Áo giữ nhiệt": ["áo giữ nhiệt"],
"Áo bra active": ["áo bra active"],
"Áo Body": ["áo body", "áo croptop", "croptop", "baby tee", "áo lửng", "áo dáng ngắn"],
"Áo ba lỗ": ["áo ba lỗ", "áo sát nách", "tanktop", "tank top", "áo dây", "áo 2 dây", "áo hai dây"],
"Váy liền": ["váy liền", "đầm"],
"Tất": ["tất", "vớ"],
"Túi xách": ["túi xách"],
"Quần váy": ["quần váy", "quần dài", "chân váy dài", "chân váy"],
"Quần soóc": ["quần soóc", "quần đùi", "quần short", "quần lửng", "quần ngố"],
"Quần nỉ": ["quần nỉ", "quần jogger", "quần ống bo chun"],
"Quần mặc nhà": ["quần mặc nhà"],
"Quần lót đùi": ["quần lót đùi"],
"Quần lót tam giác": ["quần lót tam giác"],
"Quần lót": ["quần lót", "quần chip", "quần sịp", "quần trong", "quần nhỏ", "quần xơ lít", "quần xì", "quần lót nữ", "quần lót nam", "quần lót trẻ em", "quần sơ lít"],
"Quần leggings mặc nhà": ["quần leggings mặc nhà"],
"Quần leggings": ["quần leggings"],
"Quần Khaki": ["quần khaki", "quần âu", "quần vải", "quần tây"],
"Quần jean": ["quần jean", "quần bò", "quần jeans", "denim", "jeans", "bò"],
"Quần giữ nhiệt": ["quần giữ nhiệt"],
"Quần dài": ["quần dài", "quần suông", "quần ống rộng"],
"Quần culottes": ["quần culottes"],
"Quần Body": ["quần body"],
"Pyjama": ["pyjama"],
"Mũ": ["mũ", "nón", "phụ kiện Canifa", "phụ kiện"],
"Khăn tắm": ["khăn tắm", "phụ kiện Canifa", "phụ kiện"],
"Khăn mặt": ["khăn mặt", "phụ kiện Canifa", "phụ kiện"],
"Khăn lau đầu": ["khăn lau đầu", "phụ kiện Canifa", "phụ kiện"],
"Khăn": ["khăn", "phụ kiện Canifa", "phụ kiện"],
"Găng tay chống nắng": ["găng tay chống nắng", "găng tay", "phụ kiện Canifa", "phụ kiện"],
"Chăn cá nhân": ["chăn cá nhân", "chăn", "phụ kiện Canifa", "phụ kiện"],
"Chân váy": ["chân váy", "váy maxi", "váy midi", "chân váy dài"],
"Cardigan": ["cardigan"],
"Bộ thể thao": ["bộ thể thao"],
"Bộ quần áo": ["bộ quần áo", "đồ bộ"],
"Bộ mặc nhà": ["bộ mặc nhà", "đồ ngủ", "đồ mặc nhà"],
"Blazer": ["blazer"],
}
# ==============================================================================
# AUTO-GENERATE reverse lookup: synonym → DB value
# "áo thun" → "Áo phông", "quần bò" → "Quần jean", ...
# ==============================================================================
SYNONYM_TO_DB: dict[str, str] = {}
for db_value, synonyms in PRODUCT_LINE_MAP.items():
for syn in synonyms:
SYNONYM_TO_DB[syn.lower()] = db_value
# Pre-sort synonyms by length DESC for longest-match-first
_SORTED_SYNONYMS = sorted(SYNONYM_TO_DB.keys(), key=len, reverse=True)
def resolve_product_name(raw_name: str) -> str:
"""
Resolve synonym trong product_name → tên DB thật.
Dùng longest-match-first để tránh match sai.
VD:
"áo cổ bẻ khaki" → "Áo Polo khaki"
"áo thun disney" → "Áo phông disney"
"quần bò ống rộng" → "Quần jean ống rộng"
"áo polo khaki" → "Áo Polo khaki" (giữ nguyên nếu đã đúng)
"""
name_lower = raw_name.lower().strip()
for synonym in _SORTED_SYNONYMS:
if name_lower.startswith(synonym):
db_value = SYNONYM_TO_DB[synonym]
remainder = name_lower[len(synonym):].strip()
return f"{db_value} {remainder}".strip() if remainder else db_value
return raw_name
def resolve_product_line(raw_value: str) -> list[str]:
"""
Lookup keyword → DB product_line_vn.
Hỗ trợ '/' separator (VD: "Quần/ Váy").
Không tìm thấy → giữ nguyên (prefix match ở SQL).
"""
parts = [p.strip() for p in raw_value.split("/") if p.strip()]
resolved = []
for part in parts:
mapped = SYNONYM_TO_DB.get(part.lower())
if mapped:
resolved.append(mapped)
else:
resolved.append(part)
return resolved
\ No newline at end of file
......@@ -4,6 +4,7 @@ import time
from common.embedding_service import create_embedding_async
logger = logging.getLogger(__name__)
......@@ -75,6 +76,28 @@ def _get_metadata_clauses(params, sql_params: list) -> list[str]:
sql_params.extend([f"%{color_lower}%", f"%{color_lower}%"])
logger.info(f"🎨 [SQL FILTER] Color: {color_val}")
# Product name filter: resolve synonym → split words → skip generic → LIKE AND
# "áo cổ bẻ khaki" → resolve → "Áo Polo khaki" → keywords: ["polo", "khaki"]
# "áo thun disney" → resolve → "Áo phông disney" → keywords: ["phông", "disney"]
# Auto-derive generic prefix words from PRODUCT_LINE_MAP keys
# "Áo Sơ mi" → "áo", "Quần jean" → "quần", "Váy liền" → "váy", ...
from agent.tools.product_mapping import PRODUCT_LINE_MAP
GENERIC_WORDS = {key.split()[0].lower() for key in PRODUCT_LINE_MAP.keys()}
name_val = getattr(params, "product_name", None)
if name_val:
from agent.tools.product_mapping import resolve_product_name
resolved_name = resolve_product_name(name_val)
words = resolved_name.lower().strip().split()
keywords = [w for w in words if w not in GENERIC_WORDS and len(w) >= 2]
# Fallback: nếu tất cả đều generic (VD: "quần"), vẫn dùng làm filter
if not keywords:
keywords = [w for w in words if len(w) >= 2]
for kw in keywords:
clauses.append("LOWER(product_name) LIKE %s")
sql_params.append(f"%{kw}%")
if keywords:
logger.info(f"🏷️ [SQL FILTER] Product name: '{name_val}' → resolved: '{resolved_name}' → keywords: {keywords}")
return clauses
......@@ -127,11 +150,7 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
# Metadata filters (gender + age)
where_clauses = _get_metadata_clauses(params, sql_params)
# Product line filter
product_line = getattr(params, "product_line_vn", None)
if product_line:
where_clauses.append("LOWER(product_line_vn) LIKE %s")
sql_params.append(f"{product_line.lower().strip()}%")
# Price filters
where_clauses.extend(_get_price_clauses(params, sql_params))
......@@ -244,12 +263,13 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
sql = f"""
WITH vector_matches AS (
SELECT /*+ SET_VAR(ann_params='{{"ef_search":128}}') */
SELECT /*+ SET_VAR(ann_params='{{"ef_search":256}}') */
internal_ref_code,
magento_ref_code,
product_color_code,
product_name,
master_color,
product_color_name,
product_image_url_thumbnail,
product_web_url,
sale_price,
......@@ -260,19 +280,19 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
gender_by_product,
product_line_vn,
product_line_en,
description_text_full,
description_text,
quantity_sold,
is_new_product,
approx_cosine_similarity(vector, {v_str}) as similarity_score
FROM shared_source.magento_product_dimension_with_text_embedding
ORDER BY similarity_score DESC
LIMIT 100
LIMIT 200
),
filtered_matches AS (
SELECT * FROM vector_matches
{post_filter_where}
ORDER BY similarity_score DESC
LIMIT 80
LIMIT 150
)
SELECT
internal_ref_code,
......@@ -286,7 +306,7 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
MAX_BY(original_price, similarity_score) as original_price,
MAX_BY(discount_amount, similarity_score) as discount_amount,
MAX_BY(discount_percent, similarity_score) as discount_percent,
MAX_BY(description_text_full, similarity_score) as description_text_full,
MAX_BY(description_text, similarity_score) as description_text,
MAX_BY(gender_by_product, similarity_score) as gender_by_product,
MAX_BY(age_by_product, similarity_score) as age_by_product,
MAX_BY(product_line_vn, similarity_score) as product_line_vn,
......@@ -295,26 +315,26 @@ async def build_starrocks_query(params, query_vector: list[float] | None = None)
FROM filtered_matches
GROUP BY product_color_code, internal_ref_code
{final_order}
LIMIT 70
LIMIT 80
"""
try:
query_log_path = os.path.join(os.path.dirname(__file__), "query.txt")
# Build executable query by substituting %s with actual values
executable_sql = sql
for param in sql_params:
if isinstance(param, str):
# Escape single quotes and wrap in quotes
escaped = param.replace("'", "''")
executable_sql = executable_sql.replace("%s", f"'{escaped}'", 1)
else:
executable_sql = executable_sql.replace("%s", str(param), 1)
with open(query_log_path, "w", encoding="utf-8") as f:
f.write(f"-- [HYDE SEARCH] Full Executable Query\n-- Original Params: {sql_params}\n{executable_sql}")
except Exception as e:
logger.error(f"Error writing to query.txt: {e}")
# try:
# query_log_path = os.path.join(os.path.dirname(__file__), "note/query.txt")
# # Build executable query by substituting %s with actual values
# executable_sql = sql
# for param in sql_params:
# if isinstance(param, str):
# # Escape single quotes and wrap in quotes
# escaped = param.replace("'", "''")
# executable_sql = executable_sql.replace("%s", f"'{escaped}'", 1)
# else:
# executable_sql = executable_sql.replace("%s", str(param), 1)
# with open(query_log_path, "w", encoding="utf-8") as f:
# f.write(f"-- [HYDE SEARCH] Full Executable Query\n-- Original Params: {sql_params}\n{executable_sql}")
# except Exception as e:
# logger.error(f"Error writing to query.txt: {e}")
return sql, sql_params
......@@ -206,8 +206,6 @@ class ConversationManager:
entry["message"] = message_content
if include_product_ids:
entry["product_ids"] = []
if include_product_ids:
entry["product_ids"] = []
history.append(entry)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment