56 lines
2.0 KiB
JavaScript
56 lines
2.0 KiB
JavaScript
'use strict';
|
|
|
|
const { Pool } = require('pg');
|
|
|
|
const STOPWORDS = new Set(['the', 'a', 'an', 'and', 'or', 'for', 'to', 'of', 'in', 'on', 'with', 'do', 'does', 'is', 'are', 'how', 'what', 'can', 'you', 'my', 'your', 'i', 'we', 'it', 'please', 'show', 'me']);
|
|
|
|
// 全量商品页站点地图(bridge_site_map_urls):服务端匹配,不进 LLM/向量库。
|
|
function createSiteMap(cfg) {
|
|
if (!cfg.pgUrl) return { enabled: false, search: async () => [] };
|
|
const pool = new Pool({ connectionString: cfg.pgUrl, max: 2, connectionTimeoutMillis: 5000 });
|
|
|
|
function tokensFor(query) {
|
|
return String(query || '')
|
|
.toLowerCase()
|
|
.replace(/[^\p{L}\p{N}\s-]+/gu, ' ')
|
|
.split(/\s+/)
|
|
.filter((w) => w && w.length > 1 && !STOPWORDS.has(w))
|
|
.slice(0, 8);
|
|
}
|
|
|
|
function nameFromUrl(url) {
|
|
const seg = String(url || '').split('/').filter(Boolean).pop() || '';
|
|
return seg
|
|
.replace(/\.html$/, '')
|
|
.replace(/-?\d+$/, '')
|
|
.split('-')
|
|
.filter((w) => w && w.length > 1)
|
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
.join(' ');
|
|
}
|
|
|
|
async function search(query, site = 'www', limit = 3) {
|
|
const tokens = tokensFor(query);
|
|
if (tokens.length < 2) return [];
|
|
try {
|
|
const patterns = tokens.map((t) => `%${t}%`);
|
|
const r = await pool.query(
|
|
`SELECT path, url, is_product, level, slug_tokens
|
|
FROM bridge_site_map_urls
|
|
WHERE site = $1 AND slug_tokens ILIKE ALL($2::text[])
|
|
ORDER BY is_product DESC, char_length(slug_tokens) ASC, level ASC
|
|
LIMIT $3`,
|
|
[site, patterns, Math.min(Number(limit) || 3, 5)]
|
|
);
|
|
return r.rows.map((row) => ({ name: nameFromUrl(row.url), url: row.url, is_product: row.is_product, path: row.path }));
|
|
} catch (err) {
|
|
console.error(JSON.stringify({ ts: new Date().toISOString(), warn: `sitemap search failed: ${err.message}` }));
|
|
return [];
|
|
}
|
|
}
|
|
|
|
return { enabled: true, search, pool };
|
|
}
|
|
|
|
module.exports = { createSiteMap };
|