Skip to content

Commit 9a7bd68

Browse files
committed
Improved issue #8 - Improve palette search functionality
Reworked the search logic in the chip API to provide more robust matching and ranking Changes include: - Normalized key/text helpers - Tokenization - Weighted scoring across fields (key, chipName, paletteName, description) - Special handling for very short queries - A light trigram-based fuzzy boost (Jaccard) - Results are now collected with scores and sorted by relevance (with deterministic tie-breakers) for combined and grouped data structures - Early return for empty queries is added - Also updated binary RR Circuits API.bsdesign These changes aim to improve search accuracy and ordering of chips/objects
1 parent 44367f1 commit 9a7bd68

2 files changed

Lines changed: 115 additions & 23 deletions

File tree

RR Circuits API.bsdesign

1001 Bytes
Binary file not shown.

Source/assets/js/Modules/chip.mjs

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -112,39 +112,131 @@ async function search(query, opt) {
112112
jsonDataIsCombined = false;
113113
}
114114

115-
let results = {};
116-
117-
query = query.toLowerCase().replace(/\s/gm, '');
115+
// Properties to search through for each chip/object:
116+
// - key (This is the chip/object name without spaces)
117+
// - chipName (The actual chip/object name with spaces, as it appears in the data)
118+
// - paletteName (The chip/object name as it appears in the palette, which may differ from chipName for some chips/objects)
119+
// - description (The chip/object description, which may contain the chip/object name or related keywords)
120+
121+
const results = {};
122+
123+
const qRaw = String(query ?? '').trim();
124+
const normalizeKey = (s) => String(s ?? '').toLowerCase().replace(/\s+/g, '');
125+
const normalizeText = (s) => String(s ?? '')
126+
.toLowerCase()
127+
.replace(/[_\-]+/g, ' ')
128+
.replace(/[^a-z0-9]+/g, ' ')
129+
.replace(/\s+/g, ' ')
130+
.trim();
131+
const tokenize = (s) => {
132+
const t = normalizeText(s);
133+
return t ? t.split(' ') : [];
134+
};
135+
136+
const qKey = normalizeKey(qRaw);
137+
const qText = normalizeText(qRaw);
138+
const qTokens = tokenize(qRaw);
139+
const isShort = qKey.length <= 2;
140+
if (!qKey) return {};
141+
142+
const scoreCandidate = (q, candidate, weight) => {
143+
if (!candidate) return 0;
144+
const cText = normalizeText(candidate);
145+
const cKey = normalizeKey(candidate);
146+
if (!cText && !cKey) return 0;
147+
148+
// Exact matches should always win.
149+
if (cKey === qKey) return weight + 100000;
150+
151+
const cTokens = tokenize(candidate);
152+
const hasTokenExact = cTokens.includes(qText) || cTokens.includes(qKey);
153+
const hasTokenPrefix = qTokens.length > 0
154+
? qTokens.every(qt => cTokens.some(ct => ct.startsWith(qt)))
155+
: cTokens.some(ct => ct.startsWith(qText));
156+
157+
let score = 0;
158+
159+
// For very short queries (like "if"), avoid substring matches inside other words.
160+
if (isShort) {
161+
if (hasTokenExact) score = Math.max(score, weight + 60000);
162+
if (cTokens.some(t => t.startsWith(qKey))) score = Math.max(score, weight + 50000);
163+
if (cText.startsWith(qText)) score = Math.max(score, weight + 45000);
164+
return score;
165+
}
166+
167+
if (hasTokenExact) score = Math.max(score, weight + 65000);
168+
if (cText.startsWith(qText) || cKey.startsWith(qKey)) score = Math.max(score, weight + 55000);
169+
if (hasTokenPrefix) score = Math.max(score, weight + 45000);
170+
if (cText.includes(qText) || cKey.includes(qKey)) score = Math.max(score, weight + 35000);
171+
172+
// Light fuzzy boost: trigram overlap on the space-stripped forms.
173+
const tri = (s) => {
174+
const ss = String(s || '');
175+
if (ss.length < 3) return new Set([ss]);
176+
const set = new Set();
177+
for (let i = 0; i <= ss.length - 3; i++) set.add(ss.slice(i, i + 3));
178+
return set;
179+
};
180+
const a = tri(qKey);
181+
const b = tri(cKey);
182+
let inter = 0;
183+
for (const x of a) if (b.has(x)) inter++;
184+
const union = a.size + b.size - inter;
185+
const jacc = union > 0 ? inter / union : 0;
186+
if (jacc > 0) score += Math.floor(jacc * 5000);
187+
188+
return score;
189+
};
190+
191+
const scoreChip = (chipName, chipData) => {
192+
const name = chipData?.chipName || chipName;
193+
const paletteName = chipData?.paletteName || '';
194+
const description = chipData?.description || '';
195+
196+
// Search fields listed in your comment header (with different weights).
197+
const byKey = scoreCandidate(qKey, chipName, 2000);
198+
const byChipName = scoreCandidate(qKey, name, 3000);
199+
const byPalette = scoreCandidate(qKey, paletteName, 1500);
200+
const byDesc = scoreCandidate(qKey, description, 500);
201+
return Math.max(byKey, byChipName, byPalette, byDesc);
202+
};
118203

119204
if (jsonDataIsCombined) {
205+
const matches = [];
120206
$.each(jsonData, function(chipName, chipData) {
121-
if (chipName.toLowerCase().replace(/\s/gm, '').match(new RegExp(`${query}`, 'gm'))) {
122-
results[chipName] = chipData;
123-
}
207+
const score = scoreChip(chipName, chipData);
208+
if (score > 0) matches.push({ chipName, chipData, score });
124209
});
125-
} else {
126-
$.each(jsonData, function(key, value) {
210+
matches.sort((a, b) => (b.score - a.score) || a.chipName.localeCompare(b.chipName));
211+
for (const m of matches) results[m.chipName] = m.chipData;
212+
return results;
213+
}
214+
215+
if (options.combineResults) {
216+
const matches = [];
217+
$.each(jsonData, function(groupKey, value) {
127218
$.each(value, function(chipName, chipData) {
128-
if (chipName.toLowerCase().replace(/\s/gm, '').match(new RegExp(`${query}`, 'gm'))) {
129-
if (options.combineResults) {
130-
results[chipName] = chipData;
131-
return;
132-
}
133-
results[key] = results[key] || {};
134-
results[key][chipName] = chipData;
135-
}
219+
const score = scoreChip(chipName, chipData);
220+
if (score > 0) matches.push({ chipName, chipData, score });
136221
});
137222
});
223+
matches.sort((a, b) => (b.score - a.score) || a.chipName.localeCompare(b.chipName));
224+
for (const m of matches) results[m.chipName] = m.chipData;
225+
return results;
138226
}
139227

140-
// Sort results alphabetically by chipName within each category (chips/objects) or overall if combined.
141-
if (options.combineResults) {
142-
results = Object.fromEntries(Object.entries(results).sort((a, b) => a[0].localeCompare(b[0])));
143-
} else {
144-
$.each(results, function(key, value) {
145-
results[key] = Object.fromEntries(Object.entries(value).sort((a, b) => a[0].localeCompare(b[0])));
228+
// Grouped results: keep group key but order by relevance inside each group.
229+
$.each(jsonData, function(groupKey, value) {
230+
const matches = [];
231+
$.each(value, function(chipName, chipData) {
232+
const score = scoreChip(chipName, chipData);
233+
if (score > 0) matches.push({ chipName, chipData, score });
146234
});
147-
}
235+
if (matches.length === 0) return;
236+
matches.sort((a, b) => (b.score - a.score) || a.chipName.localeCompare(b.chipName));
237+
results[groupKey] = {};
238+
for (const m of matches) results[groupKey][m.chipName] = m.chipData;
239+
});
148240

149241
return results;
150242
}

0 commit comments

Comments
 (0)