fix(aio): make search results better
* update to latest version of lunr search * add trailing wildcard to search terms to increase matches * fix unwanted error when escape was pressed Closes #17417
This commit is contained in:
parent
bffccf4622
commit
0a846a2fce
@ -11,7 +11,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
|
<a class="nav-link home" href="/"><img src="{{ homeImageUrl }}" title="Home" alt="Home"></a>
|
||||||
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
|
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
|
||||||
<aio-search-box class="search-container" #searchBox (search)="doSearch($event)"></aio-search-box>
|
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)"></aio-search-box>
|
||||||
</md-toolbar>
|
</md-toolbar>
|
||||||
<aio-search-results #searchResults *ngIf="showSearchResults" (resultSelected)="hideSearchResults()"></aio-search-results>
|
<aio-search-results #searchResults *ngIf="showSearchResults" (resultSelected)="hideSearchResults()"></aio-search-results>
|
||||||
|
|
||||||
|
@ -7,10 +7,10 @@ import { LocationService } from 'app/shared/location.service';
|
|||||||
import { MockLocationService } from 'testing/location.service';
|
import { MockLocationService } from 'testing/location.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
template: '<aio-search-box (search)="doSearch($event)"></aio-search-box>'
|
template: '<aio-search-box (onSearch)="searchHandler($event)"></aio-search-box>'
|
||||||
})
|
})
|
||||||
class HostComponent {
|
class HostComponent {
|
||||||
doSearch = jasmine.createSpy('doSearch');
|
searchHandler = jasmine.createSpy('searchHandler');
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('SearchBoxComponent', () => {
|
describe('SearchBoxComponent', () => {
|
||||||
@ -39,40 +39,61 @@ describe('SearchBoxComponent', () => {
|
|||||||
location.search.and.returnValue({ search: 'initial search' });
|
location.search.and.returnValue({ search: 'initial search' });
|
||||||
component.ngOnInit();
|
component.ngOnInit();
|
||||||
expect(location.search).toHaveBeenCalled();
|
expect(location.search).toHaveBeenCalled();
|
||||||
expect(host.doSearch).toHaveBeenCalledWith('initial search');
|
expect(host.searchHandler).toHaveBeenCalledWith('initial search');
|
||||||
expect(component.searchBox.nativeElement.value).toEqual('initial search');
|
expect(component.searchBox.nativeElement.value).toEqual('initial search');
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on input', () => {
|
describe('on input', () => {
|
||||||
it('should trigger the search event', () => {
|
it('should trigger the onSearch event', () => {
|
||||||
const input = fixture.debugElement.query(By.css('input'));
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
input.triggerEventHandler('input', { target: { value: 'some query' } });
|
input.nativeElement.value = 'some query (input)';
|
||||||
expect(host.doSearch).toHaveBeenCalledWith('some query');
|
input.triggerEventHandler('input', { });
|
||||||
|
expect(host.searchHandler).toHaveBeenCalledWith('some query (input)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on keyup', () => {
|
describe('on keyup', () => {
|
||||||
it('should trigger the search event', () => {
|
it('should trigger the onSearch event', () => {
|
||||||
const input = fixture.debugElement.query(By.css('input'));
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
input.triggerEventHandler('keyup', { target: { value: 'some query' } });
|
input.nativeElement.value = 'some query (keyup)';
|
||||||
expect(host.doSearch).toHaveBeenCalledWith('some query');
|
input.triggerEventHandler('keyup', { });
|
||||||
|
expect(host.searchHandler).toHaveBeenCalledWith('some query (keyup)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on focus', () => {
|
describe('on focus', () => {
|
||||||
it('should trigger the search event', () => {
|
it('should trigger the onSearch event', () => {
|
||||||
const input = fixture.debugElement.query(By.css('input'));
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
input.triggerEventHandler('focus', { target: { value: 'some query' } });
|
input.nativeElement.value = 'some query (focus)';
|
||||||
expect(host.doSearch).toHaveBeenCalledWith('some query');
|
input.triggerEventHandler('focus', { });
|
||||||
|
expect(host.searchHandler).toHaveBeenCalledWith('some query (focus)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('on click', () => {
|
describe('on click', () => {
|
||||||
it('should trigger the search event', () => {
|
it('should trigger the search event', () => {
|
||||||
const input = fixture.debugElement.query(By.css('input'));
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
input.triggerEventHandler('click', { target: { value: 'some query'}});
|
input.nativeElement.value = 'some query (click)';
|
||||||
expect(host.doSearch).toHaveBeenCalledWith('some query');
|
input.triggerEventHandler('click', { });
|
||||||
|
expect(host.searchHandler).toHaveBeenCalledWith('some query (click)');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('event filtering', () => {
|
||||||
|
it('should only send events if the search value has changed', () => {
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
|
||||||
|
input.nativeElement.value = 'some query';
|
||||||
|
input.triggerEventHandler('input', { });
|
||||||
|
expect(host.searchHandler).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
input.triggerEventHandler('input', { });
|
||||||
|
expect(host.searchHandler).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
input.nativeElement.value = 'some other query';
|
||||||
|
input.triggerEventHandler('input', { });
|
||||||
|
expect(host.searchHandler).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output } from '@angular/core';
|
import { Component, OnInit, ViewChild, ElementRef, EventEmitter, Output } from '@angular/core';
|
||||||
import { LocationService } from 'app/shared/location.service';
|
import { LocationService } from 'app/shared/location.service';
|
||||||
|
import { Subject } from 'rxjs/Subject';
|
||||||
|
import 'rxjs/add/operator/filter';
|
||||||
|
import 'rxjs/add/operator/distinctUntilChanged';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component provides a text box to type a search query that will be sent to the SearchService.
|
* This component provides a text box to type a search query that will be sent to the SearchService.
|
||||||
@ -17,15 +20,19 @@ import { LocationService } from 'app/shared/location.service';
|
|||||||
type="search"
|
type="search"
|
||||||
aria-label="search"
|
aria-label="search"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
(input)="onSearch($event.target.value)"
|
(input)="doSearch()"
|
||||||
(keyup)="onSearch($event.target.value)"
|
(keyup)="doSearch()"
|
||||||
(focus)="onSearch($event.target.value)"
|
(focus)="doSearch()"
|
||||||
(click)="onSearch($event.target.value)">`
|
(click)="doSearch()">`
|
||||||
})
|
})
|
||||||
export class SearchBoxComponent implements OnInit {
|
export class SearchBoxComponent implements OnInit {
|
||||||
|
|
||||||
|
private searchSubject = new Subject<string>();
|
||||||
|
|
||||||
@ViewChild('searchBox') searchBox: ElementRef;
|
@ViewChild('searchBox') searchBox: ElementRef;
|
||||||
@Output() search = new EventEmitter<string>();
|
@Output() onSearch = this.searchSubject
|
||||||
|
.filter(value => !!(value && value.trim()))
|
||||||
|
.distinctUntilChanged();
|
||||||
|
|
||||||
constructor(private locationService: LocationService) { }
|
constructor(private locationService: LocationService) { }
|
||||||
|
|
||||||
@ -36,12 +43,12 @@ export class SearchBoxComponent implements OnInit {
|
|||||||
const query = this.locationService.search()['search'];
|
const query = this.locationService.search()['search'];
|
||||||
if (query) {
|
if (query) {
|
||||||
this.searchBox.nativeElement.value = query;
|
this.searchBox.nativeElement.value = query;
|
||||||
this.onSearch(query);
|
this.doSearch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSearch(query: string) {
|
doSearch() {
|
||||||
this.search.emit(query);
|
this.searchSubject.next(this.searchBox.nativeElement.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
|
@ -6,21 +6,22 @@
|
|||||||
var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
|
var SEARCH_TERMS_URL = '/generated/docs/app/search-data.json';
|
||||||
|
|
||||||
// NOTE: This needs to be kept in sync with `ngsw-manifest.json`.
|
// NOTE: This needs to be kept in sync with `ngsw-manifest.json`.
|
||||||
importScripts('https://unpkg.com/lunr@0.7.2/lunr.min.js');
|
importScripts('https://unpkg.com/lunr@2.1.0/lunr.js');
|
||||||
|
|
||||||
var index = createIndex();
|
var index;
|
||||||
var pages = {};
|
var pages = {};
|
||||||
|
|
||||||
self.onmessage = handleMessage;
|
self.onmessage = handleMessage;
|
||||||
|
|
||||||
// Create the lunr index - the docs should be an array of objects, each object containing
|
// Create the lunr index - the docs should be an array of objects, each object containing
|
||||||
// the path and search terms for a page
|
// the path and search terms for a page
|
||||||
function createIndex() {
|
function createIndex(addFn) {
|
||||||
return lunr(/** @this */function() {
|
return lunr(/** @this */function() {
|
||||||
this.ref('path');
|
this.ref('path');
|
||||||
this.field('titleWords', {boost: 50});
|
this.field('titleWords', {boost: 50});
|
||||||
this.field('members', {boost: 40});
|
this.field('members', {boost: 40});
|
||||||
this.field('keywords', {boost: 20});
|
this.field('keywords', {boost: 20});
|
||||||
|
addFn(this);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,7 +33,7 @@ function handleMessage(message) {
|
|||||||
switch(type) {
|
switch(type) {
|
||||||
case 'load-index':
|
case 'load-index':
|
||||||
makeRequest(SEARCH_TERMS_URL, function(searchInfo) {
|
makeRequest(SEARCH_TERMS_URL, function(searchInfo) {
|
||||||
loadIndex(searchInfo);
|
index = createIndex(loadIndex(searchInfo));
|
||||||
self.postMessage({type: type, id: id, payload: true});
|
self.postMessage({type: type, id: id, payload: true});
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -67,16 +68,29 @@ function makeRequest(url, callback) {
|
|||||||
|
|
||||||
// Create the search index from the searchInfo which contains the information about each page to be indexed
|
// Create the search index from the searchInfo which contains the information about each page to be indexed
|
||||||
function loadIndex(searchInfo) {
|
function loadIndex(searchInfo) {
|
||||||
|
return function(index) {
|
||||||
// Store the pages data to be used in mapping query results back to pages
|
// Store the pages data to be used in mapping query results back to pages
|
||||||
// Add search terms from each page to the search index
|
// Add search terms from each page to the search index
|
||||||
searchInfo.forEach(function(page) {
|
searchInfo.forEach(function(page) {
|
||||||
index.add(page);
|
index.add(page);
|
||||||
pages[page.path] = page;
|
pages[page.path] = page;
|
||||||
});
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the index and return the processed results
|
// Query the index and return the processed results
|
||||||
function queryIndex(query) {
|
function queryIndex(query) {
|
||||||
|
// The index requires the query to be lowercase
|
||||||
|
var terms = query.toLowerCase().split(/\s+/);
|
||||||
|
var results = index.query(function(qb) {
|
||||||
|
terms.forEach(function(term) {
|
||||||
|
// Only include terms that are longer than 2 characters, if there is more than one term
|
||||||
|
// Add trailing wildcard to each term so that it will match more results
|
||||||
|
if (terms.length === 1 || term.trim().length > 2) {
|
||||||
|
qb.term(term, { wildcard: lunr.Query.wildcard.TRAILING });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
// Only return the array of paths to pages
|
// Only return the array of paths to pages
|
||||||
return index.search(query).map(function(hit) { return pages[hit.ref]; });
|
return results.map(function(hit) { return pages[hit.ref]; });
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user