VSEARCH / src /semantic-terms.js
bep40's picture
Fix semantic search: space-query product filtering, neon on top CLIP results, tighter layout, always show matched avatar interiors
7ce3b43
Raw History Blame Contribute Delete
7.7 kB
/**
* 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];
}