Skip to content

Commit 2d21cf9

Browse files
authored
Merge pull request #296 from canjs/290-search-result-ordering
Improve the search result ordering
2 parents 9490db6 + 9b18d76 commit 2d21cf9

File tree

4 files changed

+201
-76
lines changed

4 files changed

+201
-76
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"main": "static/canjs",
66
"scripts": {
77
"start": "node make-example.js -f",
8-
"styles": "rm -rf node_modules/bit-docs-generate-html/site/static && node make-example.js -f",
9-
"test": "npm run testee",
8+
"styles": "rm -rf node_modules/bit-docs-generate-html/site/static && npm start",
9+
"test": "npm start && npm run testee",
1010
"testee": "testee test/browser.html --browsers firefox",
1111
"preversion": "npm test",
1212
"postversion": "git push --tags && git push",

static/search.js

Lines changed: 110 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ var searchResultsRenderer = require("./templates/search-results.stache!steal-sta
44
var joinURIs = require("can-util/js/join-uris/");
55

66
//https://lunrjs.com/guides/getting_started.html
7-
var searchEngine = require("lunr");
7+
var lunr = require("lunr");
88

99
var Search = Control.extend({
1010

@@ -32,7 +32,7 @@ var Search = Control.extend({
3232
//search options
3333
searchTimeout: 400,
3434

35-
localStorageKeyPrefix: "bit-docs-search",
35+
localStorageKeyPrefix: "search",
3636

3737
//whether or not to animate in upon initialization
3838
animateInOnStart: true
@@ -66,6 +66,7 @@ var Search = Control.extend({
6666

6767
init: function(){
6868

69+
var options = this.options;
6970
var self = this;
7071

7172
//init elements
@@ -76,23 +77,28 @@ var Search = Control.extend({
7677

7778
this.useLocalStorage = this.localStorageIsAvailable();
7879

79-
this.checkSearchMapHash(this.options.pathPrefix + this.options.searchMapHashUrl).then(function(searchMapHashChangedObject){
80-
self.getSearchMap(self.options.pathPrefix + self.options.searchMapUrl, searchMapHashChangedObject).then(function(searchMap){
81-
self.initSearchEngine(searchMap);
80+
this.searchEnginePromise = new Promise(function(resolve, reject) {
81+
self.checkSearchMapHash(options.pathPrefix + options.searchMapHashUrl).then(function(searchMapHashChangedObject){
82+
self.getSearchMap(options.pathPrefix + options.searchMapUrl, searchMapHashChangedObject).then(function(searchMap){
83+
var searchEngine = self.initSearchEngine(searchMap);
84+
resolve(searchEngine);
8285

83-
//show the search input when the search engine is ready
84-
if(self.options.animateInOnStart){
85-
self.$inputWrap.fadeIn(400);
86-
}else{
87-
self.$inputWrap.show();
88-
}
86+
//show the search input when the search engine is ready
87+
if(self.options.animateInOnStart){
88+
self.$inputWrap.fadeIn(400);
89+
}else{
90+
self.$inputWrap.show();
91+
}
8992

90-
self.bindResultsEvents();
93+
self.bindResultsEvents();
94+
}, function(error){
95+
console.error("getSearchMap error", error);
96+
reject(error);
97+
});
9198
}, function(error){
92-
console.error("getSearchMap error", error);
99+
console.error("checkSearchMapHash error", error);
100+
reject(error);
93101
});
94-
}, function(error){
95-
console.error("checkSearchMapHash error", error);
96102
});
97103
},
98104
destroy: function(){
@@ -164,7 +170,7 @@ var Search = Control.extend({
164170
// ---- END LOCAL STORAGE ---- //
165171

166172
// ---- END DATA RETRIEVAL ---- //
167-
searchMapLocalStorageKey: "searchMap",
173+
searchMapLocalStorageKey: 'map',
168174
searchMap: null,
169175

170176
// function getSearchMap
@@ -225,7 +231,7 @@ var Search = Control.extend({
225231
return returnDeferred;
226232
},
227233

228-
searchMapHashLocalStorageKey: "searchMapHash",
234+
searchMapHashLocalStorageKey: 'map-hash',
229235
// function checkSearchMapHash
230236
// retrieves the searchMapHash localStorage (if present)
231237
// and from the specified url
@@ -235,22 +241,18 @@ var Search = Control.extend({
235241
//
236242
// @returns thenable that resolves to true if localStorage was cleared and false otherwise
237243
checkSearchMapHash: function(dataUrl) {
238-
var self = this,
239-
returnDeferred = $.Deferred(),
240-
localStorageKey = self.formatLocalStorageKey(self.searchMapHashLocalStorageKey),
241-
searchMapHashLocalStorage = self.getLocalStorageItem(localStorageKey),
242-
lsHash = searchMapHashLocalStorage && searchMapHashLocalStorage.hash;
244+
var returnDeferred = $.Deferred();
245+
var self = this;
243246

244247
//no need to do anything if localStorage isn't present
245-
if(!window.localStorage){
248+
if (!this.useLocalStorage) {
246249
returnDeferred.resolve(false);
247250
return;
248251
}
249252

250-
251-
localStorageKey = self.formatLocalStorageKey(self.searchMapHashLocalStorageKey);
252-
searchMapHashLocalStorage = self.getLocalStorageItem(localStorageKey);
253-
lsHash = searchMapHashLocalStorage && searchMapHashLocalStorage.hash;
253+
var localStorageKey = self.formatLocalStorageKey(self.searchMapHashLocalStorageKey);
254+
var searchMapHashLocalStorage = self.getLocalStorageItem(localStorageKey);
255+
var lsHash = searchMapHashLocalStorage && searchMapHashLocalStorage.hash;
254256

255257
$.ajax({
256258
url: dataUrl,
@@ -296,7 +298,8 @@ var Search = Control.extend({
296298

297299
// ---- SEARCHING / PARSING ---- //
298300

299-
searchIndexLocalStorageKey: "searchIndex",
301+
searchIndexLocalStorageKey: 'index',
302+
searchIndexVersionLocalStorageKey: 'index-version',
300303
searchEngine: null,
301304
// function initSearchEngine
302305
// checks localStorage for an index
@@ -305,15 +308,27 @@ var Search = Control.extend({
305308
// else
306309
// generates search engine from searchMap & saves index to local storage
307310
initSearchEngine: function(searchMap){
308-
var localStorageKey = this.formatLocalStorageKey(this.searchIndexLocalStorageKey),
309-
index = this.getLocalStorageItem(localStorageKey);
310-
if(index){
311-
this.searchEngine = searchEngine.Index.load(index);
311+
var searchEngine;
312+
var searchIndexKey = this.formatLocalStorageKey(this.searchIndexLocalStorageKey);
313+
var searchIndexVersionKey = this.formatLocalStorageKey(this.searchIndexVersionLocalStorageKey);
314+
var index = this.getLocalStorageItem(searchIndexKey);
315+
var indexVersion = this.getLocalStorageItem(searchIndexVersionKey);
316+
var currentIndexVersion = 1;// Bump this whenever the index code is changed
317+
318+
if (index && currentIndexVersion === indexVersion) {
319+
searchEngine = lunr.Index.load(index);
312320
}else{
313-
this.searchEngine = searchEngine(function(){
321+
searchEngine = lunr(function(){
322+
lunr.tokenizer.separator = /[\s]+/;
323+
324+
this.pipeline.remove(lunr.stemmer);
325+
this.pipeline.remove(lunr.stopWordFilter);
326+
this.searchPipeline.remove(lunr.stemmer);
327+
314328
this.ref('name');
315329
this.field('title');
316330
this.field('description');
331+
this.field('name');
317332
this.field('url');
318333

319334
for (var itemKey in searchMap) {
@@ -322,19 +337,46 @@ var Search = Control.extend({
322337
}
323338
}
324339
});
325-
this.setLocalStorageItem(localStorageKey, this.searchEngine);
340+
this.setLocalStorageItem(searchIndexKey, searchEngine);
341+
this.setLocalStorageItem(searchIndexVersionKey, currentIndexVersion);
326342
}
343+
return searchEngine;
327344
},
328345

329346
// function searchEngineSearch
330347
// takes a value and returns a map of all relevant search items
331348
searchEngineSearch: function(value){
349+
var searchTerm = value.toLowerCase();
332350
var self = this;
333-
return this.searchEngine
334-
//run the search
335-
.search(this.formatSearchTerm(value))
336-
//convert the results into a searchMap subset
337-
.map(function(result){ return self.searchMap[result.ref] });
351+
return this.searchEnginePromise.then(function(searchEngine) {
352+
return searchEngine
353+
//run the search
354+
.query(function(q) {
355+
356+
357+
if (searchTerm.indexOf('can-') > -1) {// If the search term includes “can-”
358+
359+
// look for an exact match and apply a large positive boost
360+
q.term(searchTerm, { usePipeline: true, boost: 120 });
361+
362+
} else {
363+
// add “can-”, look for an exact match in the title field, and apply a positive boost
364+
q.term('can-' + searchTerm, { usePipeline: false, fields: ['title'], boost: 12 });
365+
}
366+
367+
// look for terms that match the beginning or end of this query
368+
q.term('*' + searchTerm + '*', { usePipeline: false });
369+
370+
// look for matches in any of the fields and apply a medium positive boost
371+
var split = searchTerm.split(lunr.tokenizer.separator);
372+
split.forEach(function(term) {
373+
q.term(term, { usePipeline: false, fields: q.allFields, boost: 10 });
374+
q.term(term + '*', { usePipeline: false, fields: q.allFields });
375+
});
376+
})
377+
//convert the results into a searchMap subset
378+
.map(function(result){ return self.searchMap[result.ref] });
379+
});
338380
},
339381

340382
//function formatSearchTerm
@@ -395,7 +437,7 @@ var Search = Control.extend({
395437
this.selectActiveResult();
396438
break;
397439
default:
398-
440+
399441
if(value !== this.searchTerm){
400442
this.searchTerm = value;
401443
this.search(value);
@@ -485,36 +527,37 @@ var Search = Control.extend({
485527
clearTimeout(this.searchDebounceHandle);
486528
var self = this;
487529
this.searchDebounceHandle = setTimeout(function(){
488-
var resultsMap = self.searchEngineSearch(value),
489-
numResults = Object.keys(resultsMap).length,
490-
resultsFrag = self.options.resultsRenderer({
491-
results:resultsMap,
492-
numResults:numResults,
493-
searchValue:value,
494-
pathPrefix: (self.options.pathPrefix === '.') ? '' : '/' + self.options.pathPrefix + '/'
495-
},{
496-
docUrl: function(){
497-
if(!self.options.pathPrefix){
498-
return this.url;
499-
}
500-
501-
var root = joinURIs(window.location.href, self.options.pathPrefix);
502-
if(root.substr(-1) === "/"){
503-
root = root.substr(0, root.length-1);
504-
}
505-
506-
return root + "/" + this.url;
530+
self.searchEngineSearch(value).then(function(resultsMap) {
531+
var numResults = Object.keys(resultsMap).length;
532+
var resultsFrag = self.options.resultsRenderer({
533+
results:resultsMap,
534+
numResults:numResults,
535+
searchValue:value,
536+
pathPrefix: (self.options.pathPrefix === '.') ? '' : '/' + self.options.pathPrefix + '/'
537+
},{
538+
docUrl: function(){
539+
if(!self.options.pathPrefix){
540+
return this.url;
507541
}
508-
});
509542

510-
self.$resultsWrap.empty();
511-
self.$resultsWrap[0].appendChild(resultsFrag);
543+
var root = joinURIs(window.location.href, self.options.pathPrefix);
544+
if(root.substr(-1) === "/"){
545+
root = root.substr(0, root.length-1);
546+
}
512547

513-
//refresh necessary dom
514-
self.$resultsList = null;
515-
if(numResults){
516-
self.$resultsList = self.$resultsWrap.find(".search-results > ul");
517-
}
548+
return root + "/" + this.url;
549+
}
550+
});
551+
552+
self.$resultsWrap.empty();
553+
self.$resultsWrap[0].appendChild(resultsFrag);
554+
555+
//refresh necessary dom
556+
self.$resultsList = null;
557+
if(numResults){
558+
self.$resultsList = self.$resultsWrap.find(".search-results > ul");
559+
}
560+
});
518561
}, this.options.searchTimeout);
519562
},
520563

test/browser.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,2 @@
1-
import QUnit from 'steal-qunit';
2-
3-
QUnit.module('bit-docs-html-canjs');
4-
5-
QUnit.test('Placeholder test', function(){
6-
QUnit.equal(1, 1, '1 is 1');
7-
});
1+
// All test files that should be run in a browser go here
2+
require('./search');

test/search.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
var QUnit = require('steal-qunit');
2+
var SearchControl = require('../static/search');
3+
4+
/* Helper function for finding a specific result */
5+
var indexOfPageInResults = function(pageName, results) {
6+
return results.findIndex(function(result) {
7+
return result.name === pageName;
8+
});
9+
};
10+
11+
/* Clear local storage */
12+
window.localStorage.clear();
13+
14+
/* Create the search bar element */
15+
var searchBar = document.createElement('div');
16+
searchBar.id = 'search-bar';
17+
document.body.appendChild(searchBar);
18+
19+
/* Create a new instance of the search control */
20+
var search = new SearchControl('#search-bar', {
21+
pathPrefix: '../temp'
22+
});
23+
24+
/* Tests */
25+
QUnit.module('search control');
26+
27+
QUnit.test('Search for “about”', function(assert) {
28+
var done = assert.async();
29+
search.searchEngineSearch('about').then(function(results) {
30+
assert.equal(results.length > 0, true, 'got results');
31+
assert.equal(indexOfPageInResults('about', results), 0, 'first result is the About page');
32+
done();
33+
});
34+
});
35+
36+
QUnit.test('Search for “can-component”', function(assert) {
37+
var done = assert.async();
38+
search.searchEngineSearch('can-component').then(function(results) {
39+
assert.equal(results.length > 0, true, 'got results');
40+
assert.equal(indexOfPageInResults('can-component', results), 0, 'first result is the can-component page');
41+
done();
42+
});
43+
});
44+
45+
QUnit.test('Search for “can-connect”', function(assert) {
46+
var done = assert.async();
47+
search.searchEngineSearch('can-connect').then(function(results) {
48+
assert.equal(results.length > 0, true, 'got results');
49+
assert.equal(indexOfPageInResults('can-connect', results), 0, 'first result is the can-connect page');
50+
done();
51+
});
52+
});
53+
54+
QUnit.test('Search for “helpers/', function(assert) {
55+
var done = assert.async();
56+
search.searchEngineSearch('helpers/').then(function(results) {
57+
assert.equal(results.length > 0, true, 'got results');
58+
done();
59+
});
60+
});
61+
62+
QUnit.test('Search for “Live Binding”', function(assert) {
63+
var done = assert.async();
64+
search.searchEngineSearch('Live Binding').then(function(results) {
65+
assert.equal(results.length > 0, true, 'got results');
66+
assert.equal(indexOfPageInResults('can-stache.Binding', results) < 2, true, 'first result is the can-stache Live Binding page');
67+
done();
68+
});
69+
});
70+
71+
QUnit.test('Search for “Play”', function(assert) {
72+
var done = assert.async();
73+
search.searchEngineSearch('Play').then(function(results) {
74+
assert.equal(results.length > 0, true, 'got results');
75+
assert.equal(indexOfPageInResults('guides/recipes/playlist-editor', results), 0, 'first result is the “Playlist Editor (Advanced)” guide');
76+
done();
77+
});
78+
});
79+
80+
QUnit.test('Search for “stache”', function(assert) {
81+
var done = assert.async();
82+
search.searchEngineSearch('stache').then(function(results) {
83+
assert.equal(results.length > 0, true, 'got results');
84+
assert.equal(indexOfPageInResults('can-stache', results), 0, 'first result is the can-stache page');
85+
done();
86+
});
87+
});

0 commit comments

Comments
 (0)