autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
110 lines (109 loc) • 3.31 kB
JavaScript
/**
* similarity — 统一相似度计算工具
*
* 项目内所有文本/向量相似度计算统一使用此模块:
* - jaccardSimilarity: 基于 token 集合的 Jaccard 系数
* - cosineSimilarity: 向量余弦相似度
* - textSimilarity: 高层文本相似度(Jaccard + 可选子串加分)
* - tokenizeForSimilarity: 通用 bigram 分词(面向相似度场景)
*
* @module shared/similarity
*/
/**
* 通用 bigram 分词 — 面向相似度计算
*
* 将文本小写化、去标点后,生成 word + character n-gram 集合。
* 同时支持 CJK(单字 + bigram)和英文(整词 + bigram)。
*
* @param text 原始文本
* @param [n=2] n-gram 长度
* @returns token 集合
*/
export function tokenizeForSimilarity(text, n = 2) {
if (!text) {
return new Set();
}
const lower = text
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fff\u3400-\u4dbf]+/g, ' ')
.trim();
const tokens = new Set();
const words = lower.split(/\s+/);
for (const w of words) {
if (w.length >= n) {
tokens.add(w);
}
for (let i = 0; i <= w.length - n; i++) {
tokens.add(w.slice(i, i + n));
}
}
return tokens;
}
/**
* Jaccard 相似度 — |A ∩ B| / |A ∪ B|
*
* @param a token 集合 A
* @param b token 集合 B
* @returns 0.0 - 1.0
*/
export function jaccardSimilarity(a, b) {
if ((!a || a.size === 0) && (!b || b.size === 0)) {
return 0;
}
if (!a || a.size === 0 || !b || b.size === 0) {
return 0;
}
let intersection = 0;
const smaller = a.size <= b.size ? a : b;
const larger = a.size <= b.size ? b : a;
for (const t of smaller) {
if (larger.has(t)) {
intersection++;
}
}
const union = a.size + b.size - intersection;
return union > 0 ? intersection / union : 0;
}
/**
* 余弦相似度 — 向量点积 / (||a|| * ||b||)
*
* @param a 向量 A
* @param b 向量 B
* @returns 0.0 - 1.0(输入均为正值时)
*/
export function cosineSimilarity(a, b) {
if (!a || !b || a.length !== b.length || a.length === 0) {
return 0;
}
let dotProduct = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom > 0 ? dotProduct / denom : 0;
}
/**
* 高层文本相似度 — Jaccard + 可选子串包含加分
*
* @param textA 文本 A
* @param textB 文本 B
* @param [opts.n=2] n-gram 长度
* @param [opts.substringBonus=false] 是否启用子串包含加分 (+0.3)
* @returns 0.0 - 1.0
*/
export function textSimilarity(textA, textB, opts = {}) {
const { n = 2, substringBonus = false } = opts;
const tokensA = tokenizeForSimilarity(textA, n);
const tokensB = tokenizeForSimilarity(textB, n);
let sim = jaccardSimilarity(tokensA, tokensB);
if (substringBonus) {
const lowerA = (textA || '').toLowerCase();
const lowerB = (textB || '').toLowerCase();
if (lowerA && lowerB && (lowerA.includes(lowerB) || lowerB.includes(lowerA))) {
sim = Math.min(1.0, sim + 0.3);
}
}
return sim;
}