Skip to content

Commit 90e6efe

Browse files
Fix curated resources search (#737)
* Fix curated resources search: raise result limit and enable prefix matching The search was capped at 30 results (now 500), hiding most matches. Also, search terms required exact whole-word matches — e.g. "registra" wouldn't find "registration". Now the last word in a query uses prefix matching while earlier words still require full-word matches, keeping results relevant without noise. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Require all search terms to match (AND logic) Previously each query word matched independently (OR), so "pre-registration" returned more results than "registration" because "pre" matched extra items. Now every query word must appear in an item for it to be included. Multi-word phrases and prefix matching still contribute to ranking/weight. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Treat hyphens as optional so pre-registration = preregistration Content normalization produces both forms (split and joined) for hyphenated words. Query normalization strips hyphens so both "pre-registration" and "preregistration" search identically. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Replace search list with isotope card filtering Instead of loading index.json and rendering a separate results list, the search input now filters the existing resource cards in-place via isotope. Supports AND logic (all words must match), prefix matching, and works alongside the category filter buttons. Shows a count of matching resources. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Strip hyphens from card text so preregistration matches pre-registration Card text contains original hyphens, so searching "preregistration" wouldn't find cards with "pre-registration". Now hyphens are stripped from both the query (split to words) and card text (before matching). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 719a8d7 commit 90e6efe

3 files changed

Lines changed: 83 additions & 15 deletions

File tree

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
{{/* {{ partial "_shared/banner.html" . }} */}}
2-
<p id="loading">Loading search data...</p>
3-
<label for="searchBox">Enter full keywords below to find relevant resources for you or use the filters below:</label>
4-
<input disabled placeholder="Enter search text" type="text" name="searchBox" id="searchBox" class="w-100" />
5-
<div id="results"></div>
6-
<script src="{{"/js/search.js" | urlize | relURL }}"></script>
1+
<div class="project-search-box mb-3">
2+
<label for="projectSearch">Search resources by keyword:</label>
3+
<input type="search" id="projectSearch" class="project-search form-control" placeholder="Search resources..." autocapitalize="off" autocomplete="off" spellcheck="false">
4+
<small class="search-count text-muted"></small>
5+
</div>

static/js/search.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@ var searchFn = function () {
77
"of", "at", "by", "for", "with", "to", "then", "no", "not",
88
"so", "too", "can", "and", "but"];
99
var normalizer = document.createElement("textarea");
10+
// Content normalize: "pre-registration" → " pre registration preregistration "
1011
var normalize = function (input) {
1112
normalizer.innerHTML = input;
1213
var inputDecoded = normalizer.value;
13-
return " " + inputDecoded.trim().toLowerCase().replace(/[^0-9a-z ]/gi, " ").replace(/\s+/g, " ") + " ";
14+
var text = inputDecoded.trim().toLowerCase();
15+
var withSpaces = text.replace(/-/g, " ");
16+
var joined = (text.match(/\w+-[\w-]+/g) || []).map(function (w) { return w.replace(/-/g, ""); }).join(" ");
17+
return " " + (withSpaces + " " + joined).replace(/[^0-9a-z ]/gi, " ").replace(/\s+/g, " ") + " ";
1418
}
15-
var limit = 30;
19+
// Query normalize: strip hyphens so "pre-registration" → " preregistration "
20+
var normalizeQuery = function (input) {
21+
normalizer.innerHTML = input;
22+
var inputDecoded = normalizer.value;
23+
return " " + inputDecoded.trim().toLowerCase().replace(/-/g, "").replace(/[^0-9a-z ]/gi, " ").replace(/\s+/g, " ") + " ";
24+
}
25+
var limit = 500;
1626
var minChars = 2;
1727
var searching = false;
1828
var render = function (results) {
@@ -41,10 +51,22 @@ var searchFn = function () {
4151
});
4252
return weightResult;
4353
};
44-
var search = function (terms) {
54+
var search = function (terms, requiredWords) {
4555
var results = [];
4656
searchHost.index.forEach(function (item) {
4757
if (item.tags) {
58+
// AND logic: all query words must appear somewhere in the item
59+
var allText = item.title + item.subtitle + item.description + item.content;
60+
item.tags.forEach(function (tag) { allText += tag; });
61+
var allMatch = true;
62+
for (var w = 0; w < requiredWords.length; w++) {
63+
if (!~allText.indexOf(requiredWords[w])) {
64+
allMatch = false;
65+
break;
66+
}
67+
}
68+
if (!allMatch) return;
69+
4870
var weight_1 = 0;
4971
terms.forEach(function (term) {
5072
if (item.title.startsWith(term.term)) {
@@ -82,7 +104,7 @@ var searchFn = function () {
82104
if (searching) {
83105
return;
84106
}
85-
var term = normalize($("#searchBox").val()).trim();
107+
var term = normalizeQuery($("#searchBox").val()).trim();
86108
if (term === lastTerm) {
87109
return;
88110
}
@@ -107,14 +129,23 @@ var searchFn = function () {
107129
}
108130
var newTerm = str.trim();
109131
if (newTerm.length >= minChars && stopwords.indexOf(newTerm) < 0) {
132+
var isPrefix = (j === terms.length - 1);
110133
termsTree.push({
111134
weight: weight,
112-
term: " " + str.trim() + " "
135+
term: " " + str.trim() + (isPrefix ? "" : " ")
113136
});
114137
}
115138
}
116139
}
117-
search(termsTree);
140+
// Build required words for AND logic (each query word must appear)
141+
var requiredWords = [];
142+
for (var r = 0; r < terms.length; r++) {
143+
if (terms[r].length >= minChars && stopwords.indexOf(terms[r]) < 0) {
144+
var isLast = (r === terms.length - 1);
145+
requiredWords.push(" " + terms[r] + (isLast ? "" : " "));
146+
}
147+
}
148+
search(termsTree, requiredWords);
118149
searching = false;
119150
var endSearch = new Date();
120151
$("#results").append("<p><small>Search took " + (endSearch - startSearch) + "ms.</small></p>");

themes/academic/assets/js/academic.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -648,20 +648,58 @@
648648
}
649649

650650
$container.imagesLoaded(function () {
651+
let projectFilter = $section.find('.default-project-filter').text();
652+
let projectSearchTerms = null;
653+
651654
// Initialize Isotope after all images have loaded.
652655
$container.isotope({
653656
itemSelector: '.isotope-item',
654657
layoutMode: layout,
655658
masonry: {
656659
gutter: 20
657660
},
658-
filter: $section.find('.default-project-filter').text()
661+
filter: function () {
662+
let $this = $(this);
663+
let filterMatch = projectFilter ? $this.is(projectFilter) : true;
664+
if (!filterMatch) return false;
665+
if (!projectSearchTerms) return true;
666+
let text = $this.text().replace(/-/g, '');
667+
return projectSearchTerms.every(function (re) { return re.test(text); });
668+
}
669+
});
670+
671+
// Text search on cards.
672+
let searchTimeout;
673+
let $searchCount = $section.find('.search-count');
674+
$section.find('.project-search').keyup(function () {
675+
clearTimeout(searchTimeout);
676+
let input = this;
677+
searchTimeout = setTimeout(function () {
678+
let val = $(input).val().trim();
679+
if (val) {
680+
// Split on hyphens/spaces, require all words (AND logic, prefix matching)
681+
let words = val.replace(/-/g, ' ').split(/\s+/).filter(function (w) { return w.length >= 2; });
682+
projectSearchTerms = words.length ? words.map(function (w) {
683+
return new RegExp(w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
684+
}) : null;
685+
} else {
686+
projectSearchTerms = null;
687+
}
688+
$container.isotope();
689+
let count = $container.isotope('getFilteredItemElements').length;
690+
let total = $container.find('.isotope-item').length;
691+
if (projectSearchTerms) {
692+
$searchCount.text(count + ' of ' + total + ' resources shown');
693+
} else {
694+
$searchCount.text('');
695+
}
696+
}, 200);
659697
});
660698

661699
// Filter items when filter link is clicked.
662700
$section.find('.project-filters a').click(function () {
663-
let selector = $(this).attr('data-filter');
664-
$container.isotope({filter: selector});
701+
projectFilter = $(this).attr('data-filter');
702+
$container.isotope();
665703
$(this).removeClass('active').addClass('active').siblings().removeClass('active all');
666704
return false;
667705
});

0 commit comments

Comments
 (0)