/** * Từ khóa ngữ nghĩa chung — tách "câu hỏi về ảnh" khỏi "lookup sản phẩm". * ========================================================================= * * Trường ảnh này trộn hai thứ: ảnh interiors (xếp bằng CLIP cosine) và sản phẩm * catalogue (xếp lexical). merge.js ghép chúng, và có rule "crowd-out": khi * truy vấn rõ ràng là câu hỏi về SẢN PHẨM, mọi ảnh interiors bị gỡ khỏi trường. * * Rule đó từng nuốt luôn truy vấn ngữ nghĩa. "Bàn", "ghế", "nội thất", "nhà * phố", "biệt thự" khớp vào tên và specs của nhiều sản phẩm, nên rule crowd-out * kích hoạt và trường chỉ còn thẻ sản phẩm — mất khả năng tìm ảnh theo ngữ * nghĩa. Tập này là danh sách token (đã gấp dấu: NFD + bỏ dấu + đ→d + * lowercase, đúng normalizeText của catalogue.js) mà một truy vấn chứa vào là * đang hỏi về CẢNH / KHÔNG GIAN / ĐỒ ĐẸP, không phải sản phẩm cụ thể. merge.js * dùng nó để: * * đếm token "giống sản phẩm" (productLikeTerms) -- chỉ khi query có >= 2 * token product-like và tất cả cùng nằm trong tên một sản phẩm thì crowd-out * mới được phép gỡ interiors; * * giữ 100% hành vi lookup: trúng SKU/mã model/tên chính xác (score >= 700, * relevance.js EXACT_SCORE_FLOOR) vẫn luôn crowd-out. * * quyết định neon ring: products never ring on semantic queries. * * Nguyên tắc chọn từ: token chỉ được vào đây khi nó KHÔNG thể là danh từ chỉ một * dòng sản phẩm chủ lực. "ban"/"ghe" có mặt (tìm "bàn","ghế" là tìm ảnh). * "ke","tu","may","bep","kinh" KHÔNG có mặt (vừa là từ khóa ảnh vừa là dòng SP * chủ lực "kệ xoong","bếp từ","máy hút mùi","kính cường lực"); giữ nguyên * product-like, với rule >= 2 token, một token duy nhất ("bếp","tủ") không * kích hoạt crowd-out nên ảnh vẫn hiện. */ export const SEMANTIC_TERMS = new Set([ // ── nhà ở / bất động sản ── 'nha', 'pho', 'ong', 'lien', 'chung', 'cu', 'biet', 'thu', 'villa', 'vien', 'penthouse', 'dat', 'khu', // ── nội thất / trang trí ── 'noi', 'that', 'trang', 'tri', 'decor', // ── phòng ── 'phong', 'khach', 'ngu', 'an', 'tam', // ── kết cấu / hạng nhà (không bao giờ nằm trong tên dòng SP chủ lực) ── 'cap', 'tang', 'lau', 'mai', 'tre', 'ham', // ── đồ gỗ / vật liệu trang trí ── 'ban', 'ghe', 'tham', 'den', 'rem', 'goi', 'nem', 'go', 'tre', 'nua', 'gom', 'su', 'dong', 'vai', 'nen', 'nghe', // ── cảnh quan / kiến trúc ── 'hoa', 'cay', 'xanh', 'san', 'vuon', 'tuong', 'tran', 'gach', 'cua', 'so', 'mau', 'sac', // ── tính từ chung / meta ── 'xinh', 'dep', 'nhieu', 'lon', 'nho', 'sang', 'vang', 'bac', 'anh', 'hinh', ]); /** * Từ khóa của truy vấn LOẠI HÌNH NHÀ Ở / KIẾN TRÚC / NỘI THẤT. * * Những từ này định nghĩa "tôi đang tìm ẢNH một không gian", và chúng trái * nghĩa hoàn toàn với intent mua sắm: một câu hỏi "biệt thự", "nhà phố", * "phòng ngủ", "nhà cấp 4" không bao giờ là tên dòng sản phẩm. merge.js dùng * tập này để KHÓA các kết quả sản phẩm rác (khóa cửa "dùng cho cửa phòng ngủ", * tay nắm "phòng tắm") ra khỏi field khi intent là không gian -- những SP đó * chỉ khớp vài từ rải rác trong tên/spec dài, không phải là câu trả lời cho * "phòng khách". * * Giữ HẸP có chủ đích: "san" là sân (cảnh quan) nhưng cũng là "sàn" (vật liệu * sàn -- một mặt hàng), "tuong" là tường nhưng "tủ ống" không liên quan; nên * chỉ các từ mang NGHĨA LOẠI HÌNH NHÀ ở đây. Các đôi như "mai" (mái) vẫn nằm * ngoài vì "mái hiên" có thể là phụ kiện. */ export const SPACE_QUERY_TERMS = new Set([ 'nha', 'pho', 'ong', 'lien', 'chung', 'cu', 'biet', 'thu', 'villa', 'vien', 'penthouse', 'cap', 'tang', 'lau', 'mai', 'can', 'ho', 'khach', 'ngu', 'tam', 'an', 'bep', 'phong', ]); /** * Đồng nghĩa ngữ nghĩa, dùng để MỞ RỘNG truy vấn lexical (avatar interiors * không có embedding, chỉ match caption). * * "nhà phố" cũng nên bắt được caption "nhà ống"; "biệt thự" bắt được "villa"; * "nội thất" bắt được "trang trí". Danh sách giữ HẸP: "nha" → "vien" bắt cả * "gạch viên", "viên đá" -- false positive. Chỉ giữ cặp đồng nghĩa thực sự. */ export const SEMANTIC_SYNONYMS = { nha: ['ong', 'biet', 'thu', 'lien'], pho: ['nha', 'ong', 'lien'], ong: ['nha', 'pho', 'lien'], lien: ['nha', 'pho', 'ong'], biet: ['thu', 'villa', 'vuon'], thu: ['biet', 'villa', 'vuon'], villa: ['biet', 'thu'], vuon: ['biet', 'thu'], noi: ['that'], that: ['noi'], trang: ['tri'], tri: ['trang'], phong: ['khach', 'ngu', 'tam'], khach: ['phong'], ngu: ['phong'], tam: ['phong'], ban: ['ghe'], ghe: ['ban'], }; /** True khi một token (đã gấp dấu) là từ khóa ngữ nghĩa chung. */ export function isSemanticTerm(token) { return SEMANTIC_TERMS.has(String(token || '').trim().toLowerCase()); } /** * True khi truy vấn đang hỏi về MỘT KHÔNG GIAN / LOẠI HÌNH NHÀ Ở, không phải * về một sản phẩm. * * "phòng khách", "phòng ngủ", "nhà phố", "biệt thự", "nhà cấp 4" đều dẫn tới * đây. Ý nghĩa: một token keyword-space và MỘT token semantic khác ("phong" * + "khach") diễn đạt KHÔNG GIAN; nhiều token đều là keyword-space (nhà + cấp * 4, biệt thự + vườn) cũng vậy. merge.js đóng vai trò này để KHÔNG trả sản * phẩm rác (khóa cửa "dùng cho phòng ngủ") lấn ảnh nội thất. */ export function isSpaceQuery(query) { // Gấp dấu y hệt normalizeText (catalogue.js) — query thô có dấu "phòng // khách", nhưng SPACE_QUERY_TERMS lưu dạng đã gấp "phong"/"khach". const folded = String(query || '').trim() .normalize('NFD').replace(/[\u0300-\u036f]/g, '') .replace(/đ/g, 'd').replace(/Đ/g, 'D') .toLowerCase(); const tokens = folded.split(/[^a-z0-9]+/).filter(token => token.length > 1); if (!tokens.length) return false; const keywordSpace = tokens.filter(token => SPACE_QUERY_TERMS.has(token)); const semantic = tokens.filter(token => SEMANTIC_TERMS.has(token)); // Keyword-space duy nhất + ít nhất một semantic ("nhà vệ sinh", "khách // sạn", "phòng") vẫn là không gian; nhiều keyword-space ("nhà cấp 4") cũng // vậy. "bếp từ" (bep + tu, đều không semantic) không qua được. return keywordSpace.length > 0 && (semantic.length >= 1 || keywordSpace.length >= 2); } /** * Lọc tập token xuống còn phần THỰC SỰ nêu tên sản phẩm. * * "ban ghe" -> [] (cả hai đều là từ khóa ảnh); * "ke xoong" -> ["ke", "xoong"]; * "bep tu" -> ["bep", "tu"]. */ export function productLikeTerms(tokens) { return tokens.filter(token => !isSemanticTerm(token)); } /** * Mở rộng một tập token bằng đồng nghĩa ngữ nghĩa, giữ thứ tự và không trùng. * Dùng cho searchAvatarInteriors (match caption lexical). */ export function expandQueryTerms(terms) { const expanded = new Set(terms); for (const term of terms) { for (const alias of SEMANTIC_SYNONYMS[term] ?? []) expanded.add(alias); } return [...expanded]; }