Commit 193f8778 authored by S Anand's avatar S Anand

ENH: add g1.fuzzyseach. Helps #132

parent 41f23f60
Pipeline #78441 passed with stage
in 2 minutes and 26 seconds
......@@ -39,6 +39,7 @@ To use all features, add this to your HTML:
- [g1.url.parse](docs/url.md) parses a URL into a structured object
- [url.join](docs/url.md#urljoin) joins two URLs
- [url.update](docs/url.md#urlupdate) updates a URL's query parameters
- [g1.fuzzysearch](docs/fuzzysearch.md) searches for text with fuzzy matching
- [$.ajaxchain](docs/ajaxchain.md) chains AJAX requests, loading multiple items in sequence
- [L.TopoJSON](docs/topojson.md) loads TopoJSON files just like GeoJSON. Requires [topojson](https://github.com/topojson/topojson)
- [$.dispatch](docs/dispatch.md) is like [trigger](https://api.jquery.com/trigger/) but sends a native event (triggers non-jQuery events too)
......@@ -72,14 +73,6 @@ For debugging, use [dist/g1.js](dist/g1.js) -- an un-minified version.
[CHANGELOG](CHANGELOG.md) mentions all release changes.
Brief notes with examples are described in Gramex releases. For example:
- [v0.12](https://learn.gramener.com/guide/release/1.49/#g1-animated-templates)
- [v0.11](https://learn.gramener.com/guide/release/1.47/#g1)
- [v0.10.1](https://learn.gramener.com/guide/release/1.45/#g1)
- [v0.10.0](https://learn.gramener.com/guide/release/1.44/#g1)
- [v0.9.0](https://learn.gramener.com/guide/release/1.41/#g1)
## Browser support
Every release is tested on the current versions of Chrome, Edge and Firefox.
......
# g1.fuzzysearch
`g1.fuzzysearch(data, options)` returns a fuzzy search function that filteres
the data based on the text.
For example:
```js
var data = [
{"product": "Cider Apple Vinegar"},
{"product": "JBL In-Ear Headphones"},
{"product": "Vaseline Body Lotion"},
{"product": "Redux Men's Watch"},
{"product": "Omega3 Fish Oil"},
]
var search = g1.fuzzysearch(data, {
keys: ['product'], // Search within these keys
limit: 2, // Return only the top 2 results
})
search('omega')
// Returns {product: "Omega3 Fish Oil", ...} since it's the only one
search('red')
// Returns {product: "Redux Men's Watch"} and {product: "JBL In-Ear Headphones"}
// The second matches r (in "Ear"), followed by e then d in "Headphones"
```
It matches with the following priority. For example, if the string is "alpha
beta", then:
1. Match the exact phrase ("alpha beta")
2. Match all words in the same order ("alp bet")
3. Match words in any order ("bet alp")
4. Match partial words in any order ("ba aph")
5. Match letters in order ("abt")
It accepts an `options` dict with these keys:
- `keys`: a list of keys to search in. The keys are calculated and joined with a
space. (Default: assumes that data is a string list.) Each key can be either:
- a string (e.g. `"name"`, `"title"`) picks keys from the objects in the
`data` list.
- a function (e.g. `function (v) { return v['key'] })`) runs the function on
each element in the `data` list
- `limit`: the maximum number of results to return. (Default: `100`)
- `case`: `true` for case-sensitive comparisons. (Default: `false`)
export { version } from './src/package.js'
export { fuzzysearch } from './src/fuzzysearch.js'
// export item into the g1.* namespace
export { version } from './src/package.js'
export { types } from './index-types.js'
export { url } from './index-urlfilter.js'
export { scale } from './index-scale.js'
export { datafilter } from './index-datafilter.js'
export { fuzzysearch } from './index-fuzzysearch.js'
export { sanddance } from './index-sanddance.js'
export { scale } from './index-scale.js'
export { types } from './index-types.js'
export { url } from './index-urlfilter.js'
// Mapviewer is not part of g1
// import './index-mapviewer.js'
......
......@@ -148,6 +148,11 @@ export default [
extend: true
}
},
{
input: "index-fuzzysearch",
output: { file: "dist/fuzzysearch.min.js", format: "umd", name: "g1", extend: true },
plugins: [uglify()]
},
{
input: "index-types",
output: { file: "dist/types.min.js", format: "umd", name: "g1", extend: true },
......
/*
var search = g1.fuzzysearch(data, options)
// Specify keys in data as a list of column names or functions
options.keys = [ 'col1', function(v) { return v.info.name } ]
// Specify max results to return. Default: 100
options.limit = 10
// Case sensitive search. Default: false
options.case = True
*/
export function fuzzysearch (data, options) {
options = options || {}
// "values" is the string array with the text to search
var values
// If no options.keys are provided, use the raw data as-is
if (!options.keys)
values = data
// Else, join the provided keys
else
values = data.map(function(row) {
return options.keys.map(function(v) {
return typeof v == 'function' ? v(row) : row[v]
}).join(' ')
})
var limit = options.limit || 100
var flags = options.case ? '' : 'i'
// TODO: document these options once stabilized
var depth = options.depth || 10
var escape = options.escape || true
return function(text) {
var results = [], // Final results
vals = values.slice(), // Values to search. Crosses off matches to avoid duplication
re
// Trim the search text
text = text.replace(/^\s/, '').replace(/\s$/, '')
if (escape)
text = text.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
// 1. Match full phrase
re = new RegExp(text.replace(/\s+/, '\\s+'), flags)
vals.forEach(function (v, i) {
if (v && re.test(v)) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 1 || results.length >= limit) return results.slice(0, limit)
// 2. Match words in order
re = new RegExp(text.replace(/\s+/, '.*'), flags)
vals.forEach(function (v, i) {
if (v && re.test(v)) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 2 || results.length >= limit) return results.slice(0, limit)
// 3. Match words in any order
re = text.split(/\s+/).map(function (word) { return new RegExp(word, flags) })
vals.forEach(function (v, i) {
if (v && re.every(function (word) { return word.test(v) })) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 3 || results.length >= limit) return results.slice(0, limit)
// 4. Match partial words in any order
re = text.split(/\s+/).map(function (word) { return new RegExp(word.replace(/(.)/g, '$&[\\S]*'), flags) })
vals.forEach(function (v, i) {
if (v && re.every(function (word) { return word.test(v) })) { results.push(data[i]); vals[i] = '' }
})
// 5. Match characters in order
re = new RegExp(text.replace(/(.)/g, '$&.*'), flags)
vals.forEach(function (v, i) {
if (v && re.test(v)) { results.push(data[i]); vals[i] = '' }
})
if (depth <= 4 || results.length >= limit) return results.slice(0, limit)
return results.slice(0, limit)
}
}
const test = require('tape')
const g1 = require('../dist/fuzzysearch.min')
test('fuzzy search', function (t) {
t.test('Example in docs', function (t) {
var data = [
{ "product": "Cider Apple Vinegar" },
{ "product": "JBL In-Ear Headphones" },
{ "product": "Vaseline Body Lotion" },
{ "product": "Redux Men's Watch" },
{ "product": "Omega3 Fish Oil" },
]
var search = g1.fuzzysearch(data, {
keys: ['product'], // Search within these keys
limit: 2, // Return only the top 2 results
})
t.deepEquals(search('omega'), [
{ "product": "Omega3 Fish Oil" }
])
t.deepEquals(search('red'), [
{ "product": "Redux Men's Watch" }, // Returns highest priority match first
{ "product": "JBL In-Ear Headphones" }, // Then the second priority (no duplicates)
])
t.end()
})
t.test('Test specs', function (t) {
var search = g1.fuzzysearch(['alpha beta'])
// 1. Match the exact phrase("alpha beta")
t.deepEquals(search('alpha beta'), ['alpha beta'])
// 2. Match all words in the same order("alp bet")
t.deepEquals(search('alp bet'), ['alpha beta'])
// 3. Match words in any order("bet alp")
t.deepEquals(search('bet alp'), ['alpha beta'])
// 4. Match partial words in any order("ba aph")
t.deepEquals(search('ba aph'), ['alpha beta'])
// 5. Match letters in order("abt")
t.deepEquals(search('abt'), ['alpha beta'])
t.end()
})
t.test('Options', function (t) {
var search
search = g1.fuzzysearch(['X1', 'x2'], { case: true })
t.deepEquals(search('x'), ['x2'])
search = g1.fuzzysearch(['X1', 'x2'], { limit: 1 })
t.deepEquals(search('x'), ['X1'])
search = g1.fuzzysearch([{ x: 'X1' }, { x: 'x2' }], {
keys: [
'x',
function (v) { return 'a' + v.x },
function (v) { return 'b' + v.x },
]
})
t.deepEquals(search('x ax bx'), [{ x: 'X1' }, { x: 'x2' }])
t.end()
})
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment