简单、无索引的 JavaScript 文本搜索,在我的个人项目(如 YC Vibe Check、linus.zone/entr)和我的个人生产力软件中使用。阅读带注释的源代码以了解其幕后工作原理。
让我们从一些简单的例子开始:
import { search } from 'libsearch' ; // on Node.js
const { search } = window . libsearch ; // in the browser
const articles = [
{ title : 'Weather in Berkeley, California' } ,
{ title : 'University report: UC Berkeley' } ,
{ title : 'Berkeley students rise in solidarity...' } ,
{ title : 'Californian wildlife returning home' } ,
] ;
// basic usage
search ( articles , 'berkeley cali' , a => a . title ) ;
// => [{ title: 'Weather in Berkeley, California' }]
search ( articles , 'california' , a => a . title ) ;
// => [
// { title: 'Weather in Berkeley, California' },
// { title: 'Californian wildlife returning home' },
// ]
// mode: 'word' only returns whole-word matches
search ( articles , 'california' , a => a . title , { mode : 'word' } ) ;
// => [{ title: 'Weather in Berkeley, California' }]
// case sensitivity
search ( articles , 'W' , a => a . title , { caseSensitive : true } ) ;
// => [{ title: 'Weather in Berkeley, California' }]
// empty query returns the full list, unmodified
search ( articles , '' , a => a . title ) ;
// => [{...}, {...}, {...}, {...}]
更正式地说,libsearch 公开了一个 API,即search
功能。该函数采用两个必需参数和两个可选参数:
function search < T > (
items : T [ ] ,
query : string ,
by ?: ( it : T ) => string ,
options ?: {
caseSensitive : boolean ,
mode : 'word' | 'prefix' | 'autocomplete' ,
} ,
) : T [ ]
items
是要搜索的项目列表。通常, items
是一个字符串数组或具有某些字符串属性的对象数组。query
是一个字符串查询,用于搜索项目列表。by
(可选)是一个谓词函数,它从items
中获取一个项目并返回一个字符串值,用于搜索该项目。例如,如果items
是像{ name: 'Linus' }
这样的对象列表, by
将需要是一个函数x => x.name
。默认情况下,其值为x => String(x)
,适用于string[]
类型的items
。options
(可选) 是一个选项字典:caseSensitive
使搜索区分大小写。默认情况下它是false
。mode
控制不完整查询词的匹配方式:mode: 'word'
要求每个查询单词仅匹配完整、精确的单词而不是部分单词。例如,查询“California”将匹配“University of California ”,但不匹配“ California n University”。mode: 'prefix'
表示每个查询词可能是匹配词的不完整“前缀”。 “Uni Cali”将匹配“ Uni versity of Cali fornia”和“ Cali fornian Uni versity”即使在这种模式下,每个查询词也必须在某处匹配 - “ Cali fornia”不是匹配项,因为它与查询不匹配“大学”这个词。mode: 'autocomplete'
是其他两种模式的混合,在自动完成式搜索中使用时非常有用,在自动完成式搜索中,用户在返回搜索结果时连续键入查询。此模式与mode: 'word'
相同,只是最后一个查询单词可能不完整,如mode: 'prefix'
。这意味着“University of Cali”将匹配“ University of Cali fornia”,这很有用,因为用户可以在输入完整查询之前找到他们的匹配项。您可以在单元测试中找到有关这些选项如何组合在一起的更多示例。
<script>
将其放入您的 HTML 中:
< script src =" https://unpkg.com/libsearch/dist/browser.js " > </ script >
这会将search
函数公开为window.libsearch.search
。
npm install libsearch
# or
yarn add libsearch
并在您的代码中使用:
import { search } from 'libsearch' ;
// search(...);
libsearch 附带从源文件生成的 TypeScript 类型定义。使用 NPM 中的 libsearch 应该可以让 TypeScript 编译器识别它们。
libsearch 允许您在 JavaScript 对象列表中快速执行基本的全文搜索,无需预先构建的搜索索引,同时提供相当好的 TF-IDF 结果排名。它没有提供 FlexSearch 和 lunr.js 等库附带的广泛功能,但比text.indexOf(query) > -1
迈出了一大步,并且速度足够快,可用于搜索数千个文档根据我的经验,每一次击键。
libsearch 如何实现这一点有两个关键思想:
现代 JavaScript 引擎配备了高度优化的正则表达式引擎,libsearch 通过在搜索时将查询字符串转换为正则表达式过滤器,利用这一点进行快速、无索引的文本搜索。
大多数全文搜索库的工作方式是首先要求开发人员建立一个“索引”数据结构,将搜索词映射到它们出现的文档。这通常是一个很好的权衡,因为它使“搜索”的一些计算工作提前完成,因此搜索本身可以保持快速和准确。它还允许进行奇特的转换和数据清理,例如对索引数据进行词形还原,而不会破坏搜索速度。但在构建原型和简单的网络应用程序时,我通常不想承担单独的“索引”步骤来获得“足够好”的搜索解决方案的复杂性。索引需要存储在某个地方,并随着底层数据集的变化和增长而不断维护。
搜索索引的主要任务是将数据集中出现的“标记”或关键字映射到它们出现的文档,以便回答“哪些文档包含单词 X?”的问题。在搜索时快速回答( O(1)
)。如果没有索引,这就会变成一个O(n)
问题,因为每个文档都需要扫描关键字。但通常,在现代硬件上,对于客户端 Web 应用程序中典型的足够小的数据集(几 MB), n
非常小,小到每次击键的O(n)
都不明显。
libsearch 将像“Uni of California”这样的查询转换为正则表达式过滤器列表, (^|W)Uni($|W)
、 (^|W)of($|W)
、 (^|W)California
。然后,它通过每个正则表达式过滤语料库来“搜索”,而无需索引。
每个单词的传统 TF-IDF 度量计算如下:
( # matches ) / ( # words in the doc ) * log ( # total docs / # docs that matched )
获取文档中的单词数需要对文档进行标记,或者至少用空格分割文档,这在计算上是昂贵的。因此 libsearch 通过使用文档的长度(字符数)来近似这一点。
使用上述正则表达式查询,libsearch 的 TF-IDF 公式为:
( # RegExp matches ) / ( doc . length ) * log ( # docs / # docs that matched RegExp )
在执行搜索时针对每个单词计算该值,然后在最后聚合以进行排序。
libsearch 的源代码是用 TypeScript 编写的。为了允许跨 TypeScript、vanilla Node.js 和 Web 使用该库,我们编译了两个版本:
search.ts
进行类型检查并删除类型。这是Node.js中导入libsearch
时导入的代码search
功能导出到window.libsearch
全局ES 模块构建是使用 TypeScript 编译器tsc
生成的,而缩小的浏览器构建是使用 Webpack 进一步生成的。
NPM/纱线命令:
lint
和fmt
,它对存储库中的源代码进行 lint 和自动格式化test
在库的最新版本上运行单元测试;你应该在运行test
之前运行build:tsc
build:*
命令协调生成不同类型的库构建:build:tsc
构建 ES 模块构建build:w
在每个文件写入时运行build:tsc
build:cjs
从 ES 模块构建构建浏览器构建build:all
按顺序构建两个构建clean
删除dist/
中所有生成/构建的文件docs
构建了基于 Litterate 的文档,该文档位于sephist.github.io/libsearch。在推送到 main 或发布之前,我通常运行
yarn fmt && yarn build:all && yarn test && yarn docs
以确保我没有忘记任何事情。