首页 > 其他分享 >DevNow: Search with Lunrjs

DevNow: Search with Lunrjs

时间:2024-10-08 12:10:59浏览次数:8  
标签:category lunr Search const DevNow js 搜索 Lunrjs slug

前言

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

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

搜索库的横向对比

这里需要对比了 fuse.jslunrflexsearchminisearchsearch-indexjs-searchelasticlunr对比详情。下边是各个库的下载趋势和star排名。

下载趋势

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。

Function Max Duration

前端搜索的优劣

特性 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 这样专业的搜索服务会提供更好的性能和功能。

标签:category,lunr,Search,const,DevNow,js,搜索,Lunrjs,slug
From: https://www.cnblogs.com/LaughingZhu/p/18451403

相关文章

  • ElasticSearch之集群中的节点
    对官方文档Node的阅读笔记。ES集群由一个或者多个ES节点组成。ES集群中的节点,支持处理两类通信平面,见文档集群内节点之间的通信,官方文档称之为transportlayer。集群外的通信,处理客户端下发的请求,比如数据的CRUD,检索等,官方文档称之为HTTPlayer。通过集群内节点的通信,节点......
  • 37_初识搜索引擎_快速掌握query string search语法以及_all metadata原理揭秘
    1、querystring基础语法GET/test_index/test_type/_search?q=test_field:testGET/test_index/test_type/_search?q=+test_field:testGET/test_index/test_type/_search?q=-test_field:test一个是掌握q=field:searchcontent的语法,还有一个是掌握+和-的含义2、_allmetada......
  • 34_初识搜索引擎_search结果深入解析(search timeout机制揭秘)
    课程大纲1、我们如果发出一个搜索请求的话,会拿到一堆搜索结果,本节课,我们来讲解一下,这个搜索结果里的各种数据,都代表了什么含义2、我们来讲解一下,搜索的timeout机制,底层的原理,画图讲解GET/_search{"took":6,"timed_out":false,"_shards":{"total":6,"successful":6,......
  • 20_图解Elasticsearch内部如何基于_version进行乐观锁并发控制
    1、图解Elasticsearch内部如何基于_version进行乐观锁并发控制(1)_version元数据PUT/test_index/test_type/6{"test_field":"testtest"}{"_index":"test_index","_type":"test_type","_id":"6",&......
  • 02_用大白话告诉你什么是Elasticsearch
    大白话、什么是ElasticsearchElasticsearch,分布式,高性能,高可用,可伸缩的搜索和分析系统1、什么是搜索?2、如果用数据库做搜索会怎么样?3、什么是全文检索、倒排索引和Lucene?4、什么是Elasticsearch?1、什么是搜索?百度:我们比如说想找寻任何的信息的时候,就会上百度去搜索一下,比......
  • 03_Elasticsearch的功能、适用场景以及特点介绍
    1、Elasticsearch的功能,干什么的2、Elasticsearch的适用场景,能在什么地方发挥作用3、Elasticsearch的特点,跟其他类似的东西不同的地方在哪里1、Elasticsearch的功能(1)分布式的搜索引擎和数据分析引擎搜索:百度,网站的站内搜索,IT系统的检索数据分析:电商网站,最近7天牙膏这种商品......
  • 04_手工画图剖析Elasticsearch核心概念:NRT、索引、分片、副本等
    课程大纲1、lucene和elasticsearch的前世今生2、elasticsearch的核心概念3、elasticsearch核心概念vs.数据库核心概念1、lucene和elasticsearch的前世今生lucene,最先进、功能最强大的搜索库,直接基于lucene开发,非常复杂,api复杂(实现一些简单的功能,写大量的java代码),需要深入......
  • 01_Elasticsearch顶尖高手系列课程的介绍
    3、课程内容介绍(1)核心知识篇课程特点(1)使用最新Elasticsearch5.2版本讲解,市面上的书籍和视频几乎都停留在2.x版本(2)深入浅出ES核心工作原理,全部手工画图讲解,完全不同于市面上已有视频的PPT讲解(3)涵盖Elasticsearch所有核心知识点,系统化,体系完整详细,有一定深度,包括完整Java开发......
  • Elasticsearch学习笔记(3)
    RestAPIElasticsearch(ES)官方提供了多种语言的客户端库,用于与Elasticsearch进行交互。这些客户端库的主要功能是帮助开发者更方便地构建和发送DSL(DomainSpecificLanguage)查询语句,并通过HTTP请求与Elasticsearch集群进行通信。官方文档地址:https://www.elastic.co/guide/en/......
  • 对面试官说精通elastic search之底层原理解读(面试可用)
    一串文本,先经过分词分成词项被称为term。我们要搜索一个词项的时候,如果挨个遍历时间复杂度是0n为了解决查询速度,可以将词项按从小到大排序,排序过后通过二分查找的方法,将时间复杂度优化为ologn,这就组成了一个termdictionary,词项对应的docid就叫postinglist,这两个共同组......