Fuse.js : 检索与多词搜索完全匹配的记录
Fuse.js : retrieve records which exactly match a multiple word search
使用 Fuse.js
我正在尝试在 JS 对象中进行 "multiple word" 搜索,以获取恰好包含要查找的每个单词的记录。
我的数据结构如下(来自fuse.js):
[{
title: "The Lost Symbol",
author: {
firstName: "Dan",
lastName: "Brown"
}
}, ...]
我的问题是我的设置适用于单词搜索(例如 Brown
)但不适用于更多(Dan Brown
或 Dan Brown Vinci
)。
保险丝选项:
{
shouldSort: true,
matchAllTokens: true,
findAllMatches: true,
includeScore: true,
threshold: 0,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"title",
"author.firstName",
"author.lastName"
]
}
new Vue({
el: "#app",
data: {
Fuse: null,
searchText: '',
result : [],
fuseOptions: {
shouldSort: true,
matchAllTokens: true,
findAllMatches: true,
includeScore: true,
threshold: 0,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"title",
"author.firstName",
"author.lastName"
]
},
list: [{
title: "Old Man's War",
author: {
firstName: "John",
lastName: "Scalzi"
}
},
{
title: "The Lock Artist",
author: {
firstName: "Steve",
lastName: "Hamilton"
}
},
{
title: "HTML5",
author: {
firstName: "Remy",
lastName: "Sharp"
}
},
{
title: "Right Ho Jeeves",
author: {
firstName: "P.D",
lastName: "Woodhouse"
}
},
{
title: "The Code of the Wooster",
author: {
firstName: "P.D",
lastName: "Woodhouse"
}
},
{
title: "Thank You Jeeves",
author: {
firstName: "P.D",
lastName: "Woodhouse"
}
},
{
title: "The DaVinci Code",
author: {
firstName: "Dan",
lastName: "Brown"
}
},
{
title: "Angels & Demons",
author: {
firstName: "Dan",
lastName: "Brown"
}
},
{
title: "The Silmarillion",
author: {
firstName: "J.R.R",
lastName: "Tolkien"
}
},
{
title: "Syrup",
author: {
firstName: "Max",
lastName: "Barry"
}
},
{
title: "The Lost Symbol",
author: {
firstName: "Dan",
lastName: "Brown"
}
},
{
title: "The Book of Lies",
author: {
firstName: "Brad",
lastName: "Meltzer"
}
},
{
title: "Lamb",
author: {
firstName: "Christopher",
lastName: "Moore"
}
},
{
title: "Fool",
author: {
firstName: "Christopher",
lastName: "Moore"
}
},
{
title: "Incompetence",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "Fat",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "Colony",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "Backwards, Red Dwarf",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "The Grand Design",
author: {
firstName: "Stephen",
lastName: "Hawking"
}
},
{
title: "The Book of Samson",
author: {
firstName: "David",
lastName: "Maine"
}
},
{
title: "The Preservationist",
author: {
firstName: "David",
lastName: "Maine"
}
},
{
title: "Fallen",
author: {
firstName: "David",
lastName: "Maine"
}
},
{
title: "Monster 1959",
author: {
firstName: "David",
lastName: "Maine"
}
}
]
},
methods: {
fuseSearch: function() {
let self = this;
this.result = this.Fuse.search(self.searchText)
}
},
mounted() {
let self = this
this.Fuse = new window.Fuse(self.list, self.fuseOptions);
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
table {
width: 100%;
margin-top:20px
}
table th{
font-weight:bold
}
table td{
padding-top:5px
}
input{
height:30px;
width:200px;
font-size:14px
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.2.1/fuse.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<input type="text" v-model="searchText" @keyup="fuseSearch()" placeholder="search for text">
<div v-if="result.length == 0" style="margin-top:10px">No matching results, here is the full list</div>
<div v-else style="margin-top:10px">{{result.length}} records found</div>
<table>
<tr>
<th>Title</th>
<th>FistName</th>
<th>LastName</th>
<th>Score</th>
</tr>
<tr v-if="result.length >0" v-for="data in result" >
<td>{{data.item.title}}</td>
<td>{{data.item.author.firstName}}</td>
<td>{{data.item.author.lastName}}</td>
<td>{{Math.round(data.score*100,2)/100}}</td>
</tr>
<tr v-if="result.length == 0" v-for="data in list">
<td>{{data.title}}</td>
<td>{{data.author.firstName}}</td>
<td>{{data.author.lastName}}</td>
<td></td>
</tr>
</table>
</div>
不幸的是,fuse.js 不会查看所有字段,而是查看一个匹配字段。我通过将所有字段放入带有字符串数组的一个字段中解决了这个问题。
示例:
[{
title: "The Lost Symbol",
author: {
firstName: "Dan",
lastName: "Brown"
},
keywords: ["The Lost Symbol", "Dan", "Brown"] //values of title, firstname & lastname
}, ...]
并且只需指定 keywords Fuse 选项的 keys 字段
{
shouldSort: true,
matchAllTokens: true,
findAllMatches: true,
includeScore: true,
threshold: 0,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ["keywords"] //just put keywords alone
}
这对我有用。希望对你也有用。
我们也有类似的需求,最终解决如下:
(注意:我最初是在 https://github.com/krisk/Fuse/issues/235#issuecomment-850269634 上分享的)
For anyone who ends up here from a google search or otherwise, we ended up taking a different approach in https://github.com/sparkletown/sparkle/pull/1460 (thanks to @yarikoptic's > awesome work debugging, exploring, and refining this)
We basically split our search query using regex (tokeniseStringWithQuotesBySpaces
), to tokenise each individual word, but keep words that are between "
and "
as a single token):
/**
* Split the provided string by spaces (ignoring spaces within "quoted text") into an array of tokens.
*
* @param string
*
* @see
*
* @debt Depending on the outcome of https://github.com/github/codeql/issues/5964 we may end up needing to change
* this regex for performance reasons.
*/
export const tokeniseStringWithQuotesBySpaces = (string: string): string[] =>
string.match(/("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)/g) ?? [];
(Note: Please check https://github.com/github/codeql/issues/5964 as the regex may have a ReDoS vulnerability, but it also might just be a false positive in the CodeQL scanner)
With our standard Fuse config:
new Fuse(filteredPosterVenues, {
keys: [
"name",
"poster.title",
"poster.authorName",
"poster.categories",
],
threshold: 0.2, // 0.1 seems to be exact, default 0.6: brings too distant if anyhow related hits
ignoreLocation: true, // default False: True - to search ignoring location of the words.
findAllMatches: true,
}),
But then use our tokeniseStringWithQuotesBySpaces
tokeniser + customised Fuse query (using $and
to join each of our tokens, then $or
for the different fields) for the search:
const tokenisedSearchQuery = tokeniseStringWithQuotesBySpaces(
normalizedSearchQuery
);
if (tokenisedSearchQuery.length === 0) return filteredPosterVenues;
return fuseVenues
.search({
$and: tokenisedSearchQuery.map((searchToken: string) => {
const orFields: Fuse.Expression[] = [
{ name: searchToken },
{ "poster.title": searchToken },
{ "poster.authorName": searchToken },
{ "poster.categories": searchToken },
];
return {
$or: orFields,
};
}),
})
.map((fuseResult) => fuseResult.item);
This seems to work pretty effectively for our needs from my testing of it all today.>
使用 Fuse.js
我正在尝试在 JS 对象中进行 "multiple word" 搜索,以获取恰好包含要查找的每个单词的记录。
我的数据结构如下(来自fuse.js):
[{
title: "The Lost Symbol",
author: {
firstName: "Dan",
lastName: "Brown"
}
}, ...]
我的问题是我的设置适用于单词搜索(例如 Brown
)但不适用于更多(Dan Brown
或 Dan Brown Vinci
)。
保险丝选项:
{
shouldSort: true,
matchAllTokens: true,
findAllMatches: true,
includeScore: true,
threshold: 0,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"title",
"author.firstName",
"author.lastName"
]
}
new Vue({
el: "#app",
data: {
Fuse: null,
searchText: '',
result : [],
fuseOptions: {
shouldSort: true,
matchAllTokens: true,
findAllMatches: true,
includeScore: true,
threshold: 0,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"title",
"author.firstName",
"author.lastName"
]
},
list: [{
title: "Old Man's War",
author: {
firstName: "John",
lastName: "Scalzi"
}
},
{
title: "The Lock Artist",
author: {
firstName: "Steve",
lastName: "Hamilton"
}
},
{
title: "HTML5",
author: {
firstName: "Remy",
lastName: "Sharp"
}
},
{
title: "Right Ho Jeeves",
author: {
firstName: "P.D",
lastName: "Woodhouse"
}
},
{
title: "The Code of the Wooster",
author: {
firstName: "P.D",
lastName: "Woodhouse"
}
},
{
title: "Thank You Jeeves",
author: {
firstName: "P.D",
lastName: "Woodhouse"
}
},
{
title: "The DaVinci Code",
author: {
firstName: "Dan",
lastName: "Brown"
}
},
{
title: "Angels & Demons",
author: {
firstName: "Dan",
lastName: "Brown"
}
},
{
title: "The Silmarillion",
author: {
firstName: "J.R.R",
lastName: "Tolkien"
}
},
{
title: "Syrup",
author: {
firstName: "Max",
lastName: "Barry"
}
},
{
title: "The Lost Symbol",
author: {
firstName: "Dan",
lastName: "Brown"
}
},
{
title: "The Book of Lies",
author: {
firstName: "Brad",
lastName: "Meltzer"
}
},
{
title: "Lamb",
author: {
firstName: "Christopher",
lastName: "Moore"
}
},
{
title: "Fool",
author: {
firstName: "Christopher",
lastName: "Moore"
}
},
{
title: "Incompetence",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "Fat",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "Colony",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "Backwards, Red Dwarf",
author: {
firstName: "Rob",
lastName: "Grant"
}
},
{
title: "The Grand Design",
author: {
firstName: "Stephen",
lastName: "Hawking"
}
},
{
title: "The Book of Samson",
author: {
firstName: "David",
lastName: "Maine"
}
},
{
title: "The Preservationist",
author: {
firstName: "David",
lastName: "Maine"
}
},
{
title: "Fallen",
author: {
firstName: "David",
lastName: "Maine"
}
},
{
title: "Monster 1959",
author: {
firstName: "David",
lastName: "Maine"
}
}
]
},
methods: {
fuseSearch: function() {
let self = this;
this.result = this.Fuse.search(self.searchText)
}
},
mounted() {
let self = this
this.Fuse = new window.Fuse(self.list, self.fuseOptions);
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
table {
width: 100%;
margin-top:20px
}
table th{
font-weight:bold
}
table td{
padding-top:5px
}
input{
height:30px;
width:200px;
font-size:14px
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fuse.js/3.2.1/fuse.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<input type="text" v-model="searchText" @keyup="fuseSearch()" placeholder="search for text">
<div v-if="result.length == 0" style="margin-top:10px">No matching results, here is the full list</div>
<div v-else style="margin-top:10px">{{result.length}} records found</div>
<table>
<tr>
<th>Title</th>
<th>FistName</th>
<th>LastName</th>
<th>Score</th>
</tr>
<tr v-if="result.length >0" v-for="data in result" >
<td>{{data.item.title}}</td>
<td>{{data.item.author.firstName}}</td>
<td>{{data.item.author.lastName}}</td>
<td>{{Math.round(data.score*100,2)/100}}</td>
</tr>
<tr v-if="result.length == 0" v-for="data in list">
<td>{{data.title}}</td>
<td>{{data.author.firstName}}</td>
<td>{{data.author.lastName}}</td>
<td></td>
</tr>
</table>
</div>
不幸的是,fuse.js 不会查看所有字段,而是查看一个匹配字段。我通过将所有字段放入带有字符串数组的一个字段中解决了这个问题。
示例:
[{
title: "The Lost Symbol",
author: {
firstName: "Dan",
lastName: "Brown"
},
keywords: ["The Lost Symbol", "Dan", "Brown"] //values of title, firstname & lastname
}, ...]
并且只需指定 keywords Fuse 选项的 keys 字段
{
shouldSort: true,
matchAllTokens: true,
findAllMatches: true,
includeScore: true,
threshold: 0,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: ["keywords"] //just put keywords alone
}
这对我有用。希望对你也有用。
我们也有类似的需求,最终解决如下:
(注意:我最初是在 https://github.com/krisk/Fuse/issues/235#issuecomment-850269634 上分享的)
For anyone who ends up here from a google search or otherwise, we ended up taking a different approach in https://github.com/sparkletown/sparkle/pull/1460 (thanks to @yarikoptic's > awesome work debugging, exploring, and refining this)
We basically split our search query using regex (
tokeniseStringWithQuotesBySpaces
), to tokenise each individual word, but keep words that are between"
and"
as a single token):/** * Split the provided string by spaces (ignoring spaces within "quoted text") into an array of tokens. * * @param string * * @see * * @debt Depending on the outcome of https://github.com/github/codeql/issues/5964 we may end up needing to change * this regex for performance reasons. */ export const tokeniseStringWithQuotesBySpaces = (string: string): string[] => string.match(/("[^"]*?"|[^"\s]+)+(?=\s*|\s*$)/g) ?? [];
(Note: Please check https://github.com/github/codeql/issues/5964 as the regex may have a ReDoS vulnerability, but it also might just be a false positive in the CodeQL scanner)
With our standard Fuse config:
new Fuse(filteredPosterVenues, { keys: [ "name", "poster.title", "poster.authorName", "poster.categories", ], threshold: 0.2, // 0.1 seems to be exact, default 0.6: brings too distant if anyhow related hits ignoreLocation: true, // default False: True - to search ignoring location of the words. findAllMatches: true, }),
But then use our
tokeniseStringWithQuotesBySpaces
tokeniser + customised Fuse query (using$and
to join each of our tokens, then$or
for the different fields) for the search:const tokenisedSearchQuery = tokeniseStringWithQuotesBySpaces( normalizedSearchQuery ); if (tokenisedSearchQuery.length === 0) return filteredPosterVenues; return fuseVenues .search({ $and: tokenisedSearchQuery.map((searchToken: string) => { const orFields: Fuse.Expression[] = [ { name: searchToken }, { "poster.title": searchToken }, { "poster.authorName": searchToken }, { "poster.categories": searchToken }, ]; return { $or: orFields, }; }), }) .map((fuseResult) => fuseResult.item);
This seems to work pretty effectively for our needs from my testing of it all today.>