DevNow: Search with Lunrjs
前言
假期真快,转眼国庆假期已经到了最后一天。这次国庆没有出去玩,在北京看了看房子,原先的房子快要到期了,找了个更加通透一点的房子,采光也很好。
闲暇时间准备优化下 DevNow 的搜索组件,经过上一版 搜索组件优化 - Command ⌘K 的优化,现在的搜索内容只能支持标题,由于有时候标题不能百分百概括文章主题,所以希望支持 摘要 和 文章内容 搜索。
搜索库的横向对比
这里需要对比了 fuse.js 、 lunr 、 flexsearch 、 minisearch 、 search-index 、 js-search 、 elasticlunr ,对比详情。下边是各个库的下载趋势和star排名。
选择 Lunr 的原因
其实每个库都有一些相关的侧重点。
lunr.js是一个轻量级的JavaScript库,用于在客户端实现全文搜索功能。它基于倒排索引的原理,能够在不依赖服务器的情况下快速检索出匹配的文档。lunr.js的核心优势在于其简单易用的API接口,开发者只需几行代码即可为静态网页添加强大的搜索功能。
lunr.js的工作机制主要分为两个阶段:索引构建和查询处理。首先,在页面加载时,lunr.js会根据预定义的规则构建一个倒排索引,该索引包含了所有文档的关键字及其出现的位置信息。接着,在用户输入查询字符串后,lunr.js会根据索引快速找到包含这些关键字的文档,并按照相关度排序返回结果。
为了提高搜索效率和准确性,lunr.js还支持多种高级特性,比如同义词扩展、短语匹配以及布尔运算等。这些功能使得开发者能够根据具体应用场景定制搜索算法,从而提供更加个性化的用户体验。此外,lunr.js还允许用户自定义权重分配策略,以便更好地反映文档的重要程度。
DevNow 中接入 Lunr
这里使用 Astro 的 API端点 来构建。
在静态生成的站点中,你的自定义端点在构建时被调用以生成静态文件。如果你选择启用 SSR 模式,自定义端点会变成根据请求调用的实时服务器端点。静态和 SSR 端点的定义类似,但 SSR 端点支持附加额外的功能。
构造索引文件
// search-index.json.js
import { latestPosts } from '@/utils/content';
import lunr from 'lunr';
import MarkdownIt from 'markdown-it';
const stemmerSupport = await import('lunr-languages/lunr.stemmer.support.js');
const zhPlugin = await import('lunr-languages/lunr.zh.js');
// 初始化 stemmer 支持
stemmerSupport.default(lunr);
// 初始化中文插件
zhPlugin.default(lunr);
const md = new MarkdownIt();
let documents = latestPosts.map((post) => {
return {
slug: post.slug,
title: post.data.title,
description: post.data.desc,
content: md.render(post.body)
};
});
export const LunrIdx = lunr(function () {
this.use(lunr.zh);
this.ref('slug');
this.field('title');
this.field('description');
this.field('content');
// This is required to provide the position of terms in
// in the index. Currently position data is opt-in due
// to the increase in index size required to store all
// the positions. This is currently not well documented
// and a better interface may be required to expose this
// to consumers.
// this.metadataWhitelist = ['position'];
documents.forEach((doc) => {
this.add(doc);
}, this);
});
export async function GET() {
return new Response(JSON.stringify(LunrIdx), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
}
构建搜索内容
// search-docs.json.js
import { latestPosts } from '@/utils/content';
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
let documents = latestPosts.map((post) => {
return {
slug: post.slug,
title: post.data.title,
description: post.data.desc,
content: md.render(post.body),
category: post.data.category
};
});
export async function GET() {
return new Response(JSON.stringify(documents), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
}
重构搜索组件
// 核心代码
import { debounce } from 'lodash-es';
import lunr from 'lunr';
interface SEARCH_TYPE {
slug: string;
title: string;
description: string;
content: string;
category: string;
}
const [LunrIdx, setLunrIdx] = useState<null | lunr.Index>(null);
const [LunrDocs, setLunrDocs] = useState<SEARCH_TYPE[]>([]);
const [content, setContent] = useState<
| {
label: string;
id: string;
children: {
label: string;
id: string;
}[];
}[]
| null
>(null);
useEffect(() => {
const _init = async () => {
if (!LunrIdx) {
const response = await fetch('/search-index.json');
const serializedIndex = await response.json();
setLunrIdx(lunr.Index.load(serializedIndex));
}
if (!LunrDocs.length) {
const response = await fetch('/search-docs.json');
setLunrDocs(await response.json());
}
};
_init();
}, [LunrIdx, LunrDocs.length]);
const onInputChange = useCallback(
debounce(async (search: string) => {
if (!LunrIdx || !LunrDocs.length) return;
// 根据搜索内容从索引中结果
const searchResult = LunrIdx.search(search);
const map = new Map<
string,
{ label: string; id: string; children: { label: string; id: string }[] }
>();
if (searchResult.length > 0) {
for (var i = 0; i < searchResult.length; i++) {
const slug = searchResult[i]['ref'];
// 根据索引结果 获取对应文章内容
const doc = LunrDocs.filter((doc) => doc.slug == slug)[0];
// 下边主要是数据结构优化
const category = categories.find((item) => item.slug === doc.category);
if (!category) {
return;
} else if (!map.has(category.slug)) {
map.set(category.slug, {
label: category.title || 'DevNow',
id: category.slug || 'DevNow',
children: []
});
}
const target = map.get(category.slug);
if (!target) return;
target.children.push({
label: doc.title,
id: doc.slug
});
map.set(category.slug, target);
}
}
setContent([...map.values()].sort((a, b) => a.label.localeCompare(b.label)));
}, 200),
[LunrIdx, LunrDocs.length]
);
过程中遇到的问题
基于 shadcn/ui Command 搜索展示
如果像我这样自定义搜索方式和内容的话,需要把 Command
组件中自动过滤功能关掉。否则搜索结果无法正常展示。
上调函数最大持续时间
当文档比较多的时候,构建的 索引文件
和 内容文件
可能会比较大,导致请求 504
。 需要上调 Vercel 的超时策略。可以在项目社会中适当上调,默认是10s。
前端搜索的优劣
特性 | Lunr.js | Algolia |
---|---|---|
搜索方式 | 纯前端(在浏览器中处理) | 后端 API 服务 |
成本 | 完全免费 | 有免费计划,但有使用限制 |
性能 | 大量数据时性能较差 | 高效处理大规模数据 |
功能 | 基础搜索功能 | 高级搜索功能(拼写纠错、同义词等) |
索引更新 | 手动更新索引(需要重新生成) | 实时更新索引 |
数据量 | 适合小规模数据 | 适合大规模数据 |
隐私 | 索引暴露在客户端,难以保护私有数据 | 后端处理,数据可以安全存储 |
部署复杂度 | 简单(无需后端或 API) | 需要配置后端或使用 API |
适合使用 Lunr.js 的场景
- 小型静态网站:如果你的网站内容较少(如几十篇文章或文档),Lunr.js 可以提供不错的搜索体验,不需要复杂的后端服务。
- 不依赖外部服务:如果你不希望依赖第三方服务(如 Algolia),并且希望完全控制搜索的实现,Lunr.js 是一个不错的选择。
- 预算有限:对于不想支付搜索服务费用的项目,Lunr.js 是完全免费的,且足够应对基础需求。
- 无私密内容:如果你的站点没有敏感或私密的内容,Lunr.js 的客户端索引是可接受的。
适合使用 Algolia 的场景
- 大规模数据网站:如果你的网站有大量内容(成千上万条数据),Algolia 的后端搜索服务可以提供更好的性能和更快的响应时间。
- 需要高级搜索功能:如果你需要拼写纠错、自动补全、过滤器等功能,Algolia 提供的搜索能力远超 Lunr.js。
- 动态内容更新:如果你的网站内容经常变动,Algolia 可以更方便地实时更新索引。
- 数据隐私需求:如果你需要保护某些私密数据,使用 Algolia 的后端服务更为安全。
总结
基于 Lunr.js 的前端搜索方案适合小型、静态、预算有限且无私密数据的网站,它提供了简单易用的纯前端搜索解决方案。但如果你的网站规模较大、搜索需求复杂或有隐私保护要求,Algolia 这样专业的搜索服务会提供更好的性能和功能。
DevNow: Search with Lunrjs的更多相关文章
- [数据结构]——二叉树(Binary Tree)、二叉搜索树(Binary Search Tree)及其衍生算法
二叉树(Binary Tree)是最简单的树形数据结构,然而却十分精妙.其衍生出各种算法,以致于占据了数据结构的半壁江山.STL中大名顶顶的关联容器--集合(set).映射(map)便是使用二叉树实现 ...
- Leetcode 笔记 99 - Recover Binary Search Tree
题目链接:Recover Binary Search Tree | LeetCode OJ Two elements of a binary search tree (BST) are swapped ...
- Leetcode 笔记 98 - Validate Binary Search Tree
题目链接:Validate Binary Search Tree | LeetCode OJ Given a binary tree, determine if it is a valid binar ...
- 基于WebGL 的3D呈现A* Search Algorithm
http://www.hightopo.com/demo/astar/astar.html 最近搞个游戏遇到最短路径的常规游戏问题,一时起兴基于HT for Web写了个A*算法的WebGL 3D呈现 ...
- Leetcode: Convert sorted list to binary search tree (No. 109)
Sept. 22, 2015 学一道算法题, 经常回顾一下. 第二次重温, 决定增加一些图片, 帮助自己记忆. 在网上找他人的资料, 不如自己动手. 把从底向上树的算法搞通俗一些. 先做一个例子: 9 ...
- [LeetCode] Closest Binary Search Tree Value II 最近的二分搜索树的值之二
Given a non-empty binary search tree and a target value, find k values in the BST that are closest t ...
- [LeetCode] Closest Binary Search Tree Value 最近的二分搜索树的值
Given a non-empty binary search tree and a target value, find the value in the BST that is closest t ...
- [LeetCode] Verify Preorder Sequence in Binary Search Tree 验证二叉搜索树的先序序列
Given an array of numbers, verify whether it is the correct preorder traversal sequence of a binary ...
- [LeetCode] Search a 2D Matrix II 搜索一个二维矩阵之二
Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the follo ...
- [LeetCode] Lowest Common Ancestor of a Binary Search Tree 二叉搜索树的最小共同父节点
Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BS ...
随机推荐
- 初读nginx.conf
学成在线里教的部署门户,记录一下NGINX的部分配置是如何使用的 location块用于定义如何处理不同URL请求 1.代理: server { listen 80; server_name file ...
- 【BatchProgram】 读取文本批量创建目录
NameList.txt文件内容 FILE-NAME-A FILE-NAME-B FILE-NAME-C ... 根据上面文件批量创建对应的目录,且附加序号 CMD代码: @ECHO OFF SETL ...
- 【微信小程序】02 常用标签组件
轮播标签: 轮播图 <swiper> <!-- 轮播项 --> <swiper-item>1</swiper-item> <swiper-item ...
- 【Spring Data JPA】10 对象导航查询
定义: 查询一个记录时,也就是查询这个对象,通过这个对象查询他的关联对象 [说白了不就是从我们设置好的集合中获取不就完了吗] 环境搭建: INSERT INTO `jpa`.`cst_customer ...
- 跨平台字幕制作软件ARCTIME
官网:http://arctime.cn/
- 【摘录】人形机器人和自动驾驶技术 —— 3D机器视觉技术
以下内容引自: https://www.eda365.com/forum.php?mod=viewthread&tid=744288 3D机器视觉技术分为两个部分,即3D重构技术和3D数据分析 ...
- pytorch 第三方模块 GraphNAS 安装成功记录
实验室的小师妹要安装pytorch的第三方模块,经过多方努力没有安装上,后来我接手后也是感觉头疼. 该模块地址: https://github.com/GraphNAS/GraphNAS 该模块主 ...
- 作业帮基于 DolphinScheduler 的数据开发平台实践
摘要 随着任务数量.任务类型需求不断增长,对我们的数据开发平台提出了更高的要求.本文主要分享我们将调度引擎升级到 Apache DolphinScheduler 的实践经验,以及对数据开发平台的一些思 ...
- 性能测试面试题大曝光,让你如何迅速拿下 offer!
性能测试面试题精选 1. 以前做过性能测试么?请结合例子具体说明性能测试的流程 面试考察点:性能测试的流程 首选做性能测试的需求分析,明确性能测试的目标.范围.场景和性能指标(如响应时间.吞吐量.并发 ...
- [学习笔记] hash & kmp & Trie树 - 字符串
Hash Oulipo 没啥好说的,进制hash板子. #include<bits/stdc++.h> using namespace std; #define ull unsigned ...