前言

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

闲暇时间准备优化下 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.conf

    学成在线里教的部署门户,记录一下NGINX的部分配置是如何使用的 location块用于定义如何处理不同URL请求 1.代理: server { listen 80; server_name file ...

  2. 【BatchProgram】 读取文本批量创建目录

    NameList.txt文件内容 FILE-NAME-A FILE-NAME-B FILE-NAME-C ... 根据上面文件批量创建对应的目录,且附加序号 CMD代码: @ECHO OFF SETL ...

  3. 【微信小程序】02 常用标签组件

    轮播标签: 轮播图 <swiper> <!-- 轮播项 --> <swiper-item>1</swiper-item> <swiper-item ...

  4. 【Spring Data JPA】10 对象导航查询

    定义: 查询一个记录时,也就是查询这个对象,通过这个对象查询他的关联对象 [说白了不就是从我们设置好的集合中获取不就完了吗] 环境搭建: INSERT INTO `jpa`.`cst_customer ...

  5. 跨平台字幕制作软件ARCTIME

      官网:http://arctime.cn/

  6. 【摘录】人形机器人和自动驾驶技术 —— 3D机器视觉技术

    以下内容引自: https://www.eda365.com/forum.php?mod=viewthread&tid=744288 3D机器视觉技术分为两个部分,即3D重构技术和3D数据分析 ...

  7. pytorch 第三方模块 GraphNAS 安装成功记录

    实验室的小师妹要安装pytorch的第三方模块,经过多方努力没有安装上,后来我接手后也是感觉头疼. 该模块地址:   https://github.com/GraphNAS/GraphNAS 该模块主 ...

  8. 作业帮基于 DolphinScheduler 的数据开发平台实践

    摘要 随着任务数量.任务类型需求不断增长,对我们的数据开发平台提出了更高的要求.本文主要分享我们将调度引擎升级到 Apache DolphinScheduler 的实践经验,以及对数据开发平台的一些思 ...

  9. 性能测试面试题大曝光,让你如何迅速拿下 offer!

    性能测试面试题精选 1. 以前做过性能测试么?请结合例子具体说明性能测试的流程 面试考察点:性能测试的流程 首选做性能测试的需求分析,明确性能测试的目标.范围.场景和性能指标(如响应时间.吞吐量.并发 ...

  10. [学习笔记] hash & kmp & Trie树 - 字符串

    Hash Oulipo 没啥好说的,进制hash板子. #include<bits/stdc++.h> using namespace std; #define ull unsigned ...