前言

假期真快,转眼国庆假期已经到了最后一天。这次国庆没有出去玩,在北京看了看房子,原先的房子快要到期了,找了个更加通透一点的房子,采光也很好。

闲暇时间准备优化下 DevNow 的搜索组件,经过上一版 搜索组件优化 - Command ⌘K 的优化,现在的搜索内容只能支持标题,由于有时候标题不能百分百概括文章主题,所以希望支持 摘要文章内容 搜索。

搜索库的横向对比

这里需要对比了 fuse.jslunrflexsearchminisearchsearch-indexjs-searchelasticlunr对比详情。下边是各个库的下载趋势和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的更多相关文章

  1. [数据结构]——二叉树(Binary Tree)、二叉搜索树(Binary Search Tree)及其衍生算法

    二叉树(Binary Tree)是最简单的树形数据结构,然而却十分精妙.其衍生出各种算法,以致于占据了数据结构的半壁江山.STL中大名顶顶的关联容器--集合(set).映射(map)便是使用二叉树实现 ...

  2. Leetcode 笔记 99 - Recover Binary Search Tree

    题目链接:Recover Binary Search Tree | LeetCode OJ Two elements of a binary search tree (BST) are swapped ...

  3. Leetcode 笔记 98 - Validate Binary Search Tree

    题目链接:Validate Binary Search Tree | LeetCode OJ Given a binary tree, determine if it is a valid binar ...

  4. 基于WebGL 的3D呈现A* Search Algorithm

    http://www.hightopo.com/demo/astar/astar.html 最近搞个游戏遇到最短路径的常规游戏问题,一时起兴基于HT for Web写了个A*算法的WebGL 3D呈现 ...

  5. Leetcode: Convert sorted list to binary search tree (No. 109)

    Sept. 22, 2015 学一道算法题, 经常回顾一下. 第二次重温, 决定增加一些图片, 帮助自己记忆. 在网上找他人的资料, 不如自己动手. 把从底向上树的算法搞通俗一些. 先做一个例子: 9 ...

  6. [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 ...

  7. [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 ...

  8. [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 ...

  9. [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 ...

  10. [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 ...

随机推荐

  1. nginx灰度发布、网站限速和防盗链

    一.灰度发布(金丝雀发布) 灰度发布时使用比较平稳的过渡方式升级或者替换产品项目的方法统称 主要作用 及时发现项目问题 尽早获取用户反馈的信息,以改进产品 如果项目产生问题,可以将问题影响控制到最小范 ...

  2. java开发,json转list集合,原生实现

    java 是一门面象对象的语言,对象需要先定义,但是在外理网络请求时候会用到json 转成java 对象,虽然现代开发框架中也提供了很多工具和方法直接转换, 但是作为学习者了解 一下底层实现,更能灵活 ...

  3. Spring 常用的三种拦截器详解

    前言 在开发过程中,我们常常使用到拦截器来处理一些逻辑.最常用的三种拦截器分别是 AOP. Interceptor . Filter,但其实很多人并不知道什么时候用AOP,什么时候用Intercept ...

  4. exceptionx:灵活便捷的Python异常处理库,让异常处理更高效!

    exceptionx English | 中文 exceptionx 是一个灵活且便捷的Python异常处理库,允许你动态创建异常类,并提供多种异常处理机制. exceptionx 的前身是 gqyl ...

  5. 【Linux】Re04

    一.文件所有者 [root@localhost ~]# ls -ahl 总用量 40K dr-xr-x---. 3 root root 187 11月 29 19:20 . dr-xr-xr-x. 1 ...

  6. 【Oracle】Windiws-11G 安装

    教程参考: https://jingyan.baidu.com/article/363872eccfb9266e4aa16f5d.html 安装包文件目录: 注意,使用[管理员运行此文件] 然后稍等许 ...

  7. 再测python3.13a —— python3.13是否移除了GIL的限制(续)

    前文: python3.13是否移除了GIL的限制 x86_64 ubuntu22.04环境下编译版本python3.13.0 alpha 0源码--python3.13.0 alpha 0的源码编译 ...

  8. Google的Jax框架的JAX-Triton目前只能成功运行在TPU设备上(使用Pallas为jax编写kernel扩展)—— GPU上目前无法正常运行,目前正处于 experimental 阶段

    使用Pallas为jax编写kernel扩展,需要使用JAX-Triton扩展包.由于Google的深度学习框架Jax主要是面向自己的TPU进行开发的,虽然也同时支持NVIDIA的GPU,但是支持力度 ...

  9. 【转载】 日内瓦大学 & NeurIPS 2020 | 在强化学习中动态分配有限的内存资源

    原文地址: https://hub.baai.ac.cn/view/4029 ======================================================== [论文标 ...

  10. 如何配置域名的 CNAME —— 添加记录集时,为什么会提示“与已有解析记录冲突”?

    参考: https://support.huaweicloud.com/dns_faq/dns_faq_016.html https://developer.qiniu.com/fusion/kb/1 ...