Skip to content

Commit f50d7dc

Browse files
committed
add fuzzy search
1 parent 3866205 commit f50d7dc

File tree

3 files changed

+68
-24
lines changed

3 files changed

+68
-24
lines changed

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"base64-arraybuffer": "^1.0.2",
1010
"codemirror": "^5.65.1",
1111
"mime-types": "^2.1.34",
12+
"minisearch": "^6.2.0",
1213
"react": "^17.0.2",
1314
"react-codemirror2": "^7.2.1",
1415
"react-dom": "^17.0.2",

src/components/LoadGist.tsx

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import MiniSearch from 'minisearch'
23
import EditLine from './EditLine.js';
34
import {classNames} from '../libs/css-utils.js';
45
import * as gists from '../libs/gists.js';
@@ -16,20 +17,29 @@ type Gist = {
1617

1718
interface GistIdMap {
1819
[key: string]: Gist;
19-
}
20+
}
21+
22+
interface ScoredGist extends Gist {
23+
score: number,
24+
id: string,
25+
};
2026

2127
type LoadGistState = {
2228
loading: boolean,
2329
gists: GistIdMap,
2430
checks: Set<string>,
2531
filter: string, // TODO: move up
32+
newFilter: boolean,
2633
sortKey: string, // TODO: move up
2734
sortDir: string, // TODO: move up
2835
shift: boolean,
36+
index: any,
2937
};
3038

31-
function getSortFn(sortKey: string, checks: Set<string>): (a: Gist, b: Gist) => number {
39+
function getSortFn(sortKey: string, checks: Set<string>): (a: ScoredGist, b: ScoredGist) => number {
3240
switch (sortKey) {
41+
case 'score':
42+
return (a: ScoredGist, b: ScoredGist) => Math.sign(a.score - b.score);
3343
case 'name':
3444
return (a: Gist, b: Gist) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : (a.name.toLowerCase() > b.name.toLowerCase() ? 1 : 0);
3545
case 'date':
@@ -51,24 +61,32 @@ function getSortFn(sortKey: string, checks: Set<string>): (a: Gist, b: Gist) =>
5161
}
5262
}
5363

54-
function gistsToSortedArray(gists: GistIdMap, checks: Set<string>, sortKey: string, sortDir: string) {
64+
function scoredGistsToSortedArray(gists: ScoredGist[], checks: Set<string>, sortKey: string, sortDir: string) {
5565
const compareDirMult = sortDir === 'down' ? 1 : -1;
5666
const compFn = getSortFn(sortKey, checks);
57-
return Object.entries(gists).map(([id, {name, date, public: _public}]) => {
58-
return {id, name, date, public: _public};
59-
}).sort((b, a) => compFn(a, b) * compareDirMult);
67+
return gists.slice().sort((b, a) => compFn(a, b) * compareDirMult);
6068
}
6169

62-
function matchFilter(filter: string) {
63-
filter = filter.trim().toLowerCase();
64-
return function(gist: Gist) {
65-
const {name, date} = gist;
66-
return filter === '' ||
67-
name.toLowerCase().includes(filter) ||
68-
date.substring(0, 10).includes(filter);
69-
}
70+
function createIndex(gists: GistIdMap) {
71+
const miniSearch = new MiniSearch({
72+
fields: ['name'], // fields to index for full-text search
73+
});
74+
miniSearch.addAll(Object.entries(gists).map(([id, {name}]) => ({id, name})));
75+
return miniSearch;
7076
}
7177

78+
function matchingGists(index: MiniSearch, filter: string, gists: GistIdMap): ScoredGist[] {
79+
filter = filter.trim();
80+
if (filter === '') {
81+
return Object.entries(gists)
82+
.map(([id, gist]) => ({...gist, id, score: 0}))
83+
}
84+
const results = new Map(index.search(filter, { prefix: true, fuzzy: 0.2 }).map(r => [r.id, r.score]));
85+
return Object.entries(gists)
86+
.filter(([id]) => results.has(id))
87+
.map(([id, gist]) => ({...gist, id, score: results.get(id)! }))
88+
89+
}
7290

7391
type SortKeyInfo = {
7492
sortDir: string,
@@ -106,14 +124,17 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
106124
gists: _gists,
107125
checks: new Set(),
108126
filter: '',
127+
newFilter: false,
109128
sortKey: 'date',
110129
sortDir: 'down',
111130
shift: false,
131+
index: createIndex(_gists),
112132
};
113133
}
114134
handleNewGists = (gists: GistIdMap) => {
115135
this.setState({
116136
gists,
137+
index: createIndex(gists),
117138
});
118139
}
119140
toggleCheck = (id: string) => {
@@ -133,8 +154,7 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
133154
}
134155
}
135156
updateSort = (sortKey: string, sortDir: string) => {
136-
console.log('update:', sortKey, sortDir);
137-
this.setState({sortDir, sortKey});
157+
this.setState({sortDir, sortKey, newFilter: false});
138158
}
139159
componentDidMount() {
140160
const {userManager} = this.context;
@@ -219,10 +239,17 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
219239
}
220240
renderLoad() {
221241
const {userManager} = this.context;
222-
const {gists, checks, loading, filter, sortKey, sortDir, shift} = this.state;
242+
const {gists, checks, loading, index, filter, sortKey, sortDir, shift, newFilter} = this.state;
223243
const userData = userManager.getUserData();
224244
const canLoad = !!userData && !loading;
225-
const gistArray = gistsToSortedArray(gists, checks, sortKey, sortDir);
245+
const effectiveSortKey = newFilter ? 'score' : sortKey;
246+
const effectiveSortDir = newFilter ? 'down' : sortDir;
247+
const gistArray = scoredGistsToSortedArray(
248+
matchingGists(index, filter, gists),
249+
checks,
250+
effectiveSortKey,
251+
effectiveSortDir);
252+
226253
return (
227254
<div>
228255
<p>
@@ -235,21 +262,26 @@ export default class LoadGist extends React.Component<{}, LoadGistState> {
235262
gistArray.length >= 0 &&
236263
<React.Fragment>
237264
<p>
238-
<EditLine className="foobar" placeholder="search:" value={filter} onChange={(filter:string) => {this.setState({filter})}} />
265+
<EditLine className="foobar" placeholder="search:" value={filter} onChange={
266+
(filter:string) => {
267+
this.setState({filter, newFilter: filter.trim() !== ''});
268+
}
269+
}
270+
/>
239271
</p>
240272
<div className="gists">
241273
<table>
242274
<thead>
243275
<tr>
244-
<th><SortBy selected={sortKey === 'check'} sortDir={sortDir} update={(dir: string) => this.updateSort('check', dir)}/></th>
245-
<th><SortBy selected={sortKey === 'name'} sortDir={sortDir} update={(dir: string) => this.updateSort('name', dir)}/></th>
246-
<th><SortBy selected={sortKey === 'date'} sortDir={sortDir} update={(dir: string) => this.updateSort('date', dir)}/></th>
247-
<th><SortBy selected={sortKey === 'public'} sortDir={sortDir} update={(dir: string) => this.updateSort('public', dir)}/></th>
276+
<th><SortBy selected={effectiveSortKey === 'check'} sortDir={sortDir} update={(dir: string) => this.updateSort('check', dir)}/></th>
277+
<th><SortBy selected={effectiveSortKey === 'name'} sortDir={sortDir} update={(dir: string) => this.updateSort('name', dir)}/></th>
278+
<th><SortBy selected={effectiveSortKey === 'date'} sortDir={sortDir} update={(dir: string) => this.updateSort('date', dir)}/></th>
279+
<th><SortBy selected={effectiveSortKey === 'public'} sortDir={sortDir} update={(dir: string) => this.updateSort('public', dir)}/></th>
248280
</tr>
249281
</thead>
250282
<tbody>
251283
{
252-
gistArray.filter(matchFilter(filter)).map((gist, ndx) => {
284+
gistArray.map((gist, ndx) => {
253285
return (
254286
<tr key={`g${ndx}`}>
255287
<td><input type="checkbox" id={`gc${ndx}`} checked={checks.has(gist.id)} onChange={() => this.toggleCheck(gist.id)}/><label htmlFor={`gc${ndx}`}/></td>

0 commit comments

Comments
 (0)