【问题标题】:Fuse.js : retrieve records which exactly match a multiple word searchFuse.js:检索与多词搜索完全匹配的记录
【发布时间】:2026-01-03 05:15:01
【问题描述】:

使用Fuse.js 我正在尝试在 JS 对象中进行“多个单词”搜索,以获取包含所查找的每个单词的记录。

我的数据结构如下(来自fuse.js):

[{
    title: "The Lost Symbol",
    author: {
      firstName: "Dan",
      lastName: "Brown"
    }
 }, ...]

我的问题是我的设置适用于单字搜索(例如 Brown),但不适用于更多(Dan BrownDan 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>

【问题讨论】:

    标签: javascript fuse.js


    【解决方案1】:

    不幸的是,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
    }
    

    这对我有用。希望它也对你有用。

    【讨论】:

      【解决方案2】:

      我们也有类似的需求,最终解决如下:

      (注意:我最初在https://github.com/krisk/Fuse/issues/235#issuecomment-850269634上分享了这个)


      对于任何通过 google 搜索或其他方式到达这里的人,我们最终在 https://github.com/sparkletown/sparkle/pull/1460 中采用了不同的方法(感谢 @yarikoptic 的 > 出色的工作调试、探索和改进)

      我们基本上使用正则表达式 (tokeniseStringWithQuotesBySpaces) 拆分搜索查询,以标记每个单独的单词,但将 "" 之间的单词保留为单个标记):

      /**
       * Split the provided string by spaces (ignoring spaces within "quoted text") into an array of tokens.
       *
       * @param string
       *
       * @see https://*.com/a/16261693/1265472
       *
       * @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) ?? [];
      

      注意:请检查https://github.com/github/codeql/issues/5964,因为正则表达式可能存在 ReDoS 漏洞,但也可能只是 CodeQL 扫描仪中的误报)

      使用我们的标准 Fuse 配置:

            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,
            }),
      

      然后使用我们的tokeniseStringWithQuotesBySpaces tokeniser + 自定义 Fuse 查询(使用 $and 连接我们的每个令牌,然后使用 $or 连接不同的字段)进行搜索:

      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);
      

      从我今天的测试来看,这似乎非常有效地满足了我们的需求。>

      【讨论】:

        最近更新 更多