From 219c61c5c14e3a8dfed3d7158d59d11c476e3586 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 3 Dec 2025 11:49:18 +0700 Subject: (andri) fix search for similar product side dan bottom pada detail product --- src/pages/api/shop/search.js | 230 +++++++++++++++++++------------------------ 1 file changed, 104 insertions(+), 126 deletions(-) (limited to 'src/pages/api/shop') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 42d16100..7d4adfcb 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -12,7 +12,7 @@ export default async function handler(req, res) { priceTo = 0, orderBy = '', operation = 'AND', - fq = '', + fq = '', // bisa berupa string atau array limit = 30, source = '', } = req.query; @@ -20,107 +20,119 @@ export default async function handler(req, res) { let { stock = '' } = req.query; // ============================================================ - // SITEMAP + // LOGIC KHUSUS UPSELL (Simple & Direct) // ============================================================ - if (source === 'sitemap') { + if (source === 'upsell') { try { - const offset = (page - 1) * limit; + // Ambil fq dari query (format: product_id_i:(...)) + // Pastikan fq adalah string tunggal + let fqUpsell = Array.isArray(fq) ? fq.join(' OR ') : fq; + fqUpsell = decodeURIComponent(fqUpsell); const parameter = [ 'q=*:*', `rows=${limit}`, - `start=${offset}`, - 'fl=product_id_i,name_s,default_code_s,image_s,category_name', 'wt=json', - 'omitHeader=true', + 'indent=true', + 'defType=edismax', + // Filter Query khusus Upsell + `fq=${encodeURIComponent(fqUpsell)}`, + // Tetap filter yang publish & ada harga agar produk valid + `fq=${encodeURIComponent('-publish_b:false')}`, + `fq=${encodeURIComponent('price_tier1_v2_f:[1 TO *]')}` ]; - // const parameter = [ - // 'q=*:*', - // `rows=${limit}`, - // `start=${offset}`, - - // // ❌ EXCLUDE PROMOTION - // 'fq=-(name_s:*promotion* OR display_name_s:*promotion* OR variants_name_t:*promotion*)', - - // // ❌ EXCLUDE DUMMY PRODUCT - // 'fq=-(name_s:*dummy* OR display_name_s:*dummy* OR variants_name_t:*dummy* OR default_code_s:A.*)', + // PENTING: SEARCH DI CORE 'VARIANTS' + const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&'); - // 'fl=product_id_i,name_s,default_code_s,image_s,category_name', - // 'wt=json', - // 'omitHeader=true', - // ]; + const result = await axios(solrUrl); - const solrUrl = - process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); - - // console.log('[SITEMAP SOLR QUERY]', solrUrl); - - const result = await axios(solrUrl, { timeout: 25000 }); - - // mapping seperti biasa - result.data.response.products = productMappingSolr( + // 1. Mapping dasar + const mappedProducts = productMappingSolr( result.data.response.docs, false ); - delete result.data.response.docs; + // 2. FIX URL LINK: Override ID Varian dengan Template ID + const rawDocs = result.data.response.docs; + + const fixedProducts = mappedProducts.map((p, index) => { + const raw = rawDocs[index]; + if (raw && raw.template_id_i) { + return { + ...p, + id: raw.template_id_i, // Ganti ID Varian jadi ID Template agar link valid + variantId: raw.product_id_i + }; + } + return p; + }); + + const finalResponse = { + ...result.data, + response: { + ...result.data.response, + products: fixedProducts + } + }; + + delete finalResponse.response.docs; + const camelCasedData = camelcaseObjectDeep(finalResponse); + + return res.status(200).json(camelCasedData); - result.data = camelcaseObjectDeep(result.data); + } catch (e) { + console.error('[UPSELL ERROR]', e.response?.data || e.message); + return res.status(200).json({ response: { products: [], numFound: 0 } }); + } + } + // ============================================================ + // SITEMAP (Biarkan tetap sama) + // ============================================================ + if (source === 'sitemap') { + try { + const offset = (page - 1) * limit; + const parameter = [ + 'q=*:*', + `rows=${limit}`, + `start=${offset}`, + 'fl=product_id_i,name_s,default_code_s,image_s,category_name', + 'wt=json', + 'omitHeader=true', + ]; + const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); + const result = await axios(solrUrl, { timeout: 25000 }); + result.data.response.products = productMappingSolr(result.data.response.docs, false); + delete result.data.response.docs; + result.data = camelcaseObjectDeep(result.data); return res.status(200).json(result.data); } catch (e) { - console.error('[SITEMAP ERROR]', e); return res.status(500).json({ error: 'Sitemap query failed' }); } } // ============================================================ - // SEARCH NORMAL + // SEARCH NORMAL (LOGIKA LAMA) // ============================================================ let paramOrderBy = ''; switch (orderBy) { - case 'flashsale-discount-desc': - paramOrderBy += 'flashsale_discount_f DESC'; - break; - case 'price-asc': - paramOrderBy += 'price_tier1_v2_f ASC'; - break; - case 'price-desc': - paramOrderBy += 'price_tier1_v2_f DESC'; - break; - case 'popular': - paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; - break; - case 'popular-weekly': - paramOrderBy += 'search_rank_weekly_i DESC'; - break; - case 'stock': - paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; - break; - case 'flashsale-price-asc': - paramOrderBy += 'flashsale_price_f ASC'; - break; - default: - paramOrderBy += ''; - break; + case 'flashsale-discount-desc': paramOrderBy += 'flashsale_discount_f DESC'; break; + case 'price-asc': paramOrderBy += 'price_tier1_v2_f ASC'; break; + case 'price-desc': paramOrderBy += 'price_tier1_v2_f DESC'; break; + case 'popular': paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; break; + case 'popular-weekly': paramOrderBy += 'search_rank_weekly_i DESC'; break; + case 'stock': paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; break; + case 'flashsale-price-asc': paramOrderBy += 'flashsale_price_f ASC'; break; + default: paramOrderBy += ''; break; } let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); let newQ = escapeSolrQuery(q); - const formattedQuery = `(${newQ - .split(' ') - .map((term) => (term.length < 2 ? term : `${term}*`)) - .join(' ')})`; - - const mm = - checkQ.length > 2 - ? checkQ.length > 5 - ? '55%' - : '85%' - : `${checkQ.length}`; + const formattedQuery = `(${newQ.split(' ').map((term) => (term.length < 2 ? term : `${term}*`)).join(' ')})`; + const mm = checkQ.length > 2 ? (checkQ.length > 5 ? '55%' : '85%') : `${checkQ.length}`; const filterQueries = [ '-publish_b:false', @@ -128,23 +140,19 @@ export default async function handler(req, res) { 'price_tier1_v2_f:[1 TO *]', ]; - if (orderBy === 'stock') { - filterQueries.push('stock_total_f:[1 TO *]'); - } + if (orderBy === 'stock') filterQueries.push('stock_total_f:[1 TO *]'); - if (fq && source != 'similar' && typeof fq != 'string') { - fq.push(...filterQueries); + // Handle 'fq' parameter from request + let finalFq = [...filterQueries]; + if (fq) { + if (Array.isArray(fq)) finalFq.push(...fq); + else finalFq.push(fq); } - const fq_ = filterQueries.join(' AND '); - let keywords = newQ; if (source === 'similar' || checkQ.length < 3) { - if (checkQ.length < 2 || checkQ[1].length < 2) { - keywords = newQ; - } else { - keywords = newQ + '*'; - } + if (checkQ.length < 2 || checkQ[1].length < 2) keywords = newQ; + else keywords = newQ + '*'; } else { keywords = formattedQuery; } @@ -164,16 +172,16 @@ export default async function handler(req, res) { `start=${parseInt(offset)}`, `rows=${limit}`, `sort=${paramOrderBy}`, - `fq=${encodeURIComponent(fq_)}`, `mm=${encodeURIComponent(mm)}`, ]; + // Masukkan semua Filter Query (fq) + finalFq.forEach(f => { + parameter.push(`fq=${encodeURIComponent(f)}`); + }); + if (priceFrom > 0 || priceTo > 0) { - parameter.push( - `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ - priceTo == '' ? '*' : priceTo - }]` - ); + parameter.push(`fq=price_tier1_v2_f:[${priceFrom || '*'} TO ${priceTo || '*'}]`); } let { auth } = req.cookies; @@ -183,49 +191,28 @@ export default async function handler(req, res) { } if (brand) { - const brandExpr = brand - .split(',') - .map( - (manufacturer) => - `manufacture_name:"${encodeURIComponent(manufacturer)}"` - ) - .join(' OR '); + const brandExpr = brand.split(',').map(m => `manufacture_name:"${encodeURIComponent(m)}"`).join(' OR '); parameter.push(`fq={!tag=brand}(${brandExpr})`); } if (category) { - const catExpr = category - .split(',') - .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) - .join(' OR '); + const catExpr = category.split(',').map(c => `category_name:"${encodeURIComponent(c)}"`).join(' OR '); parameter.push(`fq={!tag=cat}(${catExpr})`); } if (stock) parameter.push(`fq=stock_total_f:(1 TO *)`); - if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); - - if (Array.isArray(fq)) - parameter = parameter.concat( - fq.map((val) => `fq=${encodeURIComponent(val)}`) - ); - - const solrUrl = - process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); - - const result = await axios(solrUrl); + // SEARCH NORMAL: DEFAULT KE CORE 'PRODUCT' + const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); try { + const result = await axios(solrUrl); result.data.response.products = productMappingSolr( result.data.response.docs, auth?.pricelist || false ); - result.data.responseHeader.params.start = parseInt( - result.data.responseHeader.params.start - ); - result.data.responseHeader.params.rows = parseInt( - result.data.responseHeader.params.rows - ); + result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start); + result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows); delete result.data.response.docs; result.data = camelcaseObjectDeep(result.data); res.status(200).json(result.data); @@ -236,17 +223,8 @@ export default async function handler(req, res) { const escapeSolrQuery = (query) => { if (query == '*') return query; - query = query.replace(/-/g, ' '); - const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; const words = query.split(/\s+/); - const escapedWords = words.map((word) => { - if (specialChars.test(word)) { - return word.replace(specialChars, '\\$1'); - } - return word; - }); - - return escapedWords.join(' '); -}; + return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' '); +}; \ No newline at end of file -- cgit v1.2.3 From 31853dc731c6e4105c9cf9bd373c63e6e989caa4 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 16 Dec 2025 14:35:33 +0700 Subject: (andri) try compare with data --- src/pages/api/shop/search.js | 94 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 9 deletions(-) (limited to 'src/pages/api/shop') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 7d4adfcb..90841e09 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -2,6 +2,14 @@ import { productMappingSolr } from '@/utils/solrMapping'; import axios from 'axios'; import camelcaseObjectDeep from 'camelcase-object-deep'; +const escapeSolrQuery = (query) => { + if (query == '*') return query; + query = query.replace(/-/g, ' '); + const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; + const words = query.split(/\s+/); + return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' '); +}; + export default async function handler(req, res) { const { q = '*', @@ -19,6 +27,82 @@ export default async function handler(req, res) { let { stock = '' } = req.query; + // ============================================================ + // [BARU] 1. LOGIC KHUSUS COMPARE (Wajib ditaruh paling atas) + // ============================================================ + if (source === 'compare') { + try { + let qCompare = q === '*' ? '*:*' : q; + + // Sanitasi Query + if (qCompare !== '*:*') { + const escaped = escapeSolrQuery(qCompare); + qCompare = `*${escaped}*`; + } + + // Susun Parameter Solr + const parameter = [ + `q=${encodeURIComponent(qCompare)}`, + `rows=${limit}`, + 'wt=json', + 'indent=true', + 'defType=edismax', + + // Grouping agar varian tidak banjir (per template) + 'group=true', + 'group.field=template_id_i', + 'group.limit=1', + 'group.main=true', + + // Field Wajib (Perhatikan: kita butuh product_id_i/default_code_s) + 'fl=id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i', + + // Filter Dasar + 'fq=-publish_b:false', + 'fq=price_tier1_v2_f:[1 TO *]' + ]; + + // Logic Locking (Filter Attribute Set ID dari Frontend) + // Frontend akan mengirim fq="attribute_set_id_i:9" + if (fq) { + if (Array.isArray(fq)) { + fq.forEach(f => parameter.push(`fq=${encodeURIComponent(f)}`)); + } else { + parameter.push(`fq=${encodeURIComponent(fq)}`); + } + } + + // Target Core: VARIANTS (Karena compare butuh data spesifik) + const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&'); + + const result = await axios(solrUrl); + + // Mapping Result + const mappedProducts = productMappingSolr( + result.data.response.docs, + false + ); + + const finalResponse = { + ...result.data, + response: { + ...result.data.response, + products: mappedProducts + } + }; + + delete finalResponse.response.docs; + const camelCasedData = camelcaseObjectDeep(finalResponse); + + return res.status(200).json(camelCasedData); + + } catch (e) { + console.error('[COMPARE SEARCH ERROR]', e.message); + // Return JSON valid meski kosong, agar frontend tidak error syntax + return res.status(200).json({ response: { products: [], numFound: 0 } }); + } + } + // ============================================================ // LOGIC KHUSUS UPSELL (Simple & Direct) // ============================================================ @@ -219,12 +303,4 @@ export default async function handler(req, res) { } catch (error) { res.status(400).json({ error: error.message }); } -} - -const escapeSolrQuery = (query) => { - if (query == '*') return query; - query = query.replace(/-/g, ' '); - const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; - const words = query.split(/\s+/); - return words.map((word) => specialChars.test(word) ? word.replace(specialChars, '\\$1') : word).join(' '); -}; \ No newline at end of file +} \ No newline at end of file -- cgit v1.2.3 From 1876d142f492714c37efdc1eabb72709917a0c1d Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Fri, 19 Dec 2025 15:20:31 +0700 Subject: (andri) searching comparable --- src/pages/api/shop/search.js | 106 +++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 45 deletions(-) (limited to 'src/pages/api/shop') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 90841e09..5ea6a70a 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -28,54 +28,67 @@ export default async function handler(req, res) { let { stock = '' } = req.query; // ============================================================ - // [BARU] 1. LOGIC KHUSUS COMPARE (Wajib ditaruh paling atas) + // [PERBAIKAN] 1. LOGIC KHUSUS COMPARE (PAKAI URLSearchParams) // ============================================================ if (source === 'compare') { try { let qCompare = q === '*' ? '*:*' : q; - + // Sanitasi Query if (qCompare !== '*:*') { - const escaped = escapeSolrQuery(qCompare); - qCompare = `*${escaped}*`; + // Kita escape, tapi biarkan stringnya bersih (jangan ditambah wildcard * manual) + // karena kita serahkan ke 'edismax' parser + qCompare = escapeSolrQuery(qCompare); } - // Susun Parameter Solr - const parameter = [ - `q=${encodeURIComponent(qCompare)}`, - `rows=${limit}`, - 'wt=json', - 'indent=true', - 'defType=edismax', - - // Grouping agar varian tidak banjir (per template) - 'group=true', - 'group.field=template_id_i', - 'group.limit=1', - 'group.main=true', - - // Field Wajib (Perhatikan: kita butuh product_id_i/default_code_s) - 'fl=id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i', - - // Filter Dasar - 'fq=-publish_b:false', - 'fq=price_tier1_v2_f:[1 TO *]' - ]; + // [SOLUSI] Gunakan URLSearchParams untuk menyusun URL dengan aman + const params = new URLSearchParams(); + + params.append('q', qCompare); + params.append('rows', limit); + params.append('wt', 'json'); + params.append('indent', 'true'); + + // Gunakan eDisMax parser (Otak Cerdas) + params.append('defType', 'edismax'); + + // Set Prioritas Pencarian (Boost ^) + // 1. default_code_s^20 : SKU persis (Prioritas Tertinggi) + // 2. search_keywords_t^10 : Field baru (Case insensitive) + // 3. display_name_s^1 : Cadangan + params.append('qf', 'default_code_s^20 search_keywords_t^10 display_name_s^1'); + + // Minimum Match 100% (Semua kata harus ada), ubah jika ingin lebih longgar + params.append('mm', '100%'); + + // Grouping + params.append('group', 'true'); + params.append('group.field', 'template_id_i'); + params.append('group.limit', '1'); + params.append('group.main', 'true'); + + // Field List (fl) + params.append('fl', 'id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i'); + + // Filter Query (fq) Dasar + params.append('fq', '-publish_b:false'); + params.append('fq', 'price_tier1_v2_f:[1 TO *]'); // Logic Locking (Filter Attribute Set ID dari Frontend) - // Frontend akan mengirim fq="attribute_set_id_i:9" if (fq) { - if (Array.isArray(fq)) { - fq.forEach(f => parameter.push(`fq=${encodeURIComponent(f)}`)); - } else { - parameter.push(`fq=${encodeURIComponent(fq)}`); - } + if (Array.isArray(fq)) { + fq.forEach(f => params.append('fq', f)); + } else { + params.append('fq', fq); + } } - // Target Core: VARIANTS (Karena compare butuh data spesifik) - const solrUrl = process.env.SOLR_HOST + '/solr/variants/select?' + parameter.join('&'); - - const result = await axios(solrUrl); + // Target Core: VARIANTS + // HAPUS parameter manual dari string URL, gunakan params object + const solrUrl = process.env.SOLR_HOST + '/solr/variants/select'; + + // Axios akan otomatis handle encoding % dan & dengan benar + const result = await axios.get(solrUrl, { params: params }); // Mapping Result const mappedProducts = productMappingSolr( @@ -84,13 +97,13 @@ export default async function handler(req, res) { ); const finalResponse = { - ...result.data, - response: { - ...result.data.response, - products: mappedProducts - } + ...result.data, + response: { + ...result.data.response, + products: mappedProducts + } }; - + delete finalResponse.response.docs; const camelCasedData = camelcaseObjectDeep(finalResponse); @@ -98,13 +111,16 @@ export default async function handler(req, res) { } catch (e) { console.error('[COMPARE SEARCH ERROR]', e.message); - // Return JSON valid meski kosong, agar frontend tidak error syntax + if (e.response && e.response.data) { + // Log detail error dari Solr + console.error('[SOLR DETAILS]:', JSON.stringify(e.response.data, null, 2)); + } return res.status(200).json({ response: { products: [], numFound: 0 } }); } } // ============================================================ - // LOGIC KHUSUS UPSELL (Simple & Direct) + // LOGIC KHUSUS UPSELL (KODE LAMA ANDA) // ============================================================ if (source === 'upsell') { try { @@ -172,7 +188,7 @@ export default async function handler(req, res) { } // ============================================================ - // SITEMAP (Biarkan tetap sama) + // SITEMAP (KODE LAMA ANDA) // ============================================================ if (source === 'sitemap') { try { @@ -197,7 +213,7 @@ export default async function handler(req, res) { } // ============================================================ - // SEARCH NORMAL (LOGIKA LAMA) + // SEARCH NORMAL (KODE LAMA ANDA) // ============================================================ let paramOrderBy = ''; -- cgit v1.2.3 From 8ea5e52346fff91462e63e3e881f65a47ffd1354 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Tue, 27 Jan 2026 11:47:24 +0700 Subject: (andri) fix search non grouping compare --- src/pages/api/shop/search.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) (limited to 'src/pages/api/shop') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 5ea6a70a..37cfa1bd 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -23,6 +23,7 @@ export default async function handler(req, res) { fq = '', // bisa berupa string atau array limit = 30, source = '', + group = 'true', } = req.query; let { stock = '' } = req.query; @@ -61,11 +62,14 @@ export default async function handler(req, res) { // Minimum Match 100% (Semua kata harus ada), ubah jika ingin lebih longgar params.append('mm', '100%'); - // Grouping - params.append('group', 'true'); - params.append('group.field', 'template_id_i'); - params.append('group.limit', '1'); - params.append('group.main', 'true'); + if (group === 'false') { + params.append('group', 'false'); + } else { + params.append('group', 'true'); + params.append('group.field', 'template_id_i'); + params.append('group.limit', '1'); + params.append('group.main', 'true'); + } // Field List (fl) params.append('fl', 'id,display_name_s,default_code_s,image_s,price_tier1_v2_f,attribute_set_id_i,attribute_set_name_s,template_id_i,product_id_i'); -- cgit v1.2.3 From dac398aa33f8e57fd17fc9b115cf435c2d877b19 Mon Sep 17 00:00:00 2001 From: FIN-IT_AndriFP Date: Wed, 28 Jan 2026 11:05:37 +0700 Subject: fix --- src/pages/api/shop/search.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) (limited to 'src/pages/api/shop') diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 37cfa1bd..89d75cd0 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -35,11 +35,14 @@ export default async function handler(req, res) { try { let qCompare = q === '*' ? '*:*' : q; - // Sanitasi Query if (qCompare !== '*:*') { - // Kita escape, tapi biarkan stringnya bersih (jangan ditambah wildcard * manual) - // karena kita serahkan ke 'edismax' parser qCompare = escapeSolrQuery(qCompare); + qCompare = qCompare.split(/\s+/).map(term => { + if (term && !term.includes('*')) { + return term + '*'; + } + return term; + }).join(' '); } // [SOLUSI] Gunakan URLSearchParams untuk menyusun URL dengan aman @@ -59,8 +62,12 @@ export default async function handler(req, res) { // 3. display_name_s^1 : Cadangan params.append('qf', 'default_code_s^20 search_keywords_t^10 display_name_s^1'); - // Minimum Match 100% (Semua kata harus ada), ubah jika ingin lebih longgar - params.append('mm', '100%'); + const compareWords = qCompare.split(/\s+/).filter(w => w.length > 0); + let compareMm = '100%'; + if (compareWords.length >= 3) { + compareMm = '75%'; + } + params.append('mm', compareMm); if (group === 'false') { params.append('group', 'false'); -- cgit v1.2.3