前言

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

闲暇时间准备优化下 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. jmeter测试udp广播(jmeter发送udp)

    jmeter测试udp广播(jmeter发送udp) jmeter测试udp广播(jmeter接收udp) 先下载安装第三方插件 下载链接:https://jmeter-plugins.org/ins ...

  2. argparse学习笔记

    argparse是 Python 的一个内置模块,用于编写用户友好的命令行接口.使用 argparse,你可以很容易地为 Python 脚本添加参数解析功能,使得脚本可以接受命令行选项和参数.学起来也 ...

  3. 【Java】暂存逻辑

    需求说明: 需求是填写一个表单时暂时保存输入项,不提交表单 回来再次填写时可以恢复或者放弃,或者更改内容继续暂存 放两张UI图,一个移动端,一个手机端: 逻辑分析: 存储方式有这么几种,Cookie存 ...

  4. 【Spring】04 注解实现自动装配

    1.使用注解实现自动装配 注解的基础源于JDK1.5的新特性 在Spring2.5开始支持了注解功能 如何使用? 1.导入约束 xmlns:context="http://www.sprin ...

  5. 【Tutorial C】05 操作符 & 表达式

    基本运算符 C使用运算符(operator)来代表算术运算.例如,+运算符可以使它两侧的值加在一起. 如果您觉得术语"运算符"听起来比较奇怪,那么请您记住那些东西总得有个名称. 与 ...

  6. 公共的网络云盘的存储真的安全吗?—— 百度云盘上的PDF文件无故被改名

    在百度云盘上上传了一个PDF文件,内容: 本来是没有啥问题的,但是今天使用百度云盘发现这个PDF文件居然被改名,被取消掉了扩展名: 简直是离谱离了一个大谱,太可怕了,看来这种公共云盘真的不太靠谱,虽然 ...

  7. 根据baselines库修改的运行输入参数的解析代码

    如题: def arg_parser(): """ Create an empty argparse.ArgumentParser. """ ...

  8. centos 7 安装 flask

    最近 Python 特别火,尤其是在人工智能和大数据分析方面,更是如火如荼.正好放假有空就简单看了下Python 先从熟悉的地方入手,那就从web框架开始学学吧. 首先,官方给了安装方法:http:/ ...

  9. 常用的php方法

    /* * http 封装网络请求方法 */ /* * get method */ function get($url, $param=array()){ if(!is_array($param)){ ...

  10. SQL Server序列号的获取

    建表: 1 USE [JX_IMS_CPK] 2 GO 3 4 SET ANSI_NULLS ON 5 GO 6 7 SET QUOTED_IDENTIFIER ON 8 GO 9 10 CREATE ...