如何打造体验良好的本地全文搜索?
对于博客或者文档,全文搜索是非常重要的功能。现如今,网站生成器工具,无论是静态的、携带完整后台的、还是基于低代码、无代码的,皆不可胜数,诸如:Hexo、Vuepress、Docsify、Hugo、Ghost 等,但之于「搜索」功能,或未提供、或不尽如人意;基于这样的背景,本文旨在探讨如何打造本地全文搜索?
如何检索关键字?
方案一
基于类 Gitbook 默认方案来创建索引。如快应用文档,其数据来源于:search_plus_index.json;在本地编译 md 文件时,将所有其内容纯文本化,作为数据源,并将其他信息如路径、标题、关键字等,一并存储于该 JSON 文件;发起关键字检索时,只需读取该 json 内容,并逐个遍历;将存有关键字的项 push 至结果数组,从而展示给用户就好。
创建索引
gitbook build
发起搜索
function query(keyword) {
if (keyword == null || keyword.trim() === '') return;
var results = [],
index = -1;
for (var page in INDEX_DATA) {
if ((index = INDEX_DATA[page].body.toLowerCase().indexOf(keyword.toLowerCase())) !== -1) {
results.push({
url: page,
title: INDEX_DATA[page].title,
body: INDEX_DATA[page].body.substr(Math.max(0, index - 50), MAX_DESCRIPTION_SIZE).replace(new RegExp('(' + escapeReg(keyword) + ')', 'gi'), '<span class="search-highlight-keyword">$1</span>')
});
}
}
displayResults({
count: results.length,
query: keyword,
results: results
});
}
方案二
基于 Lunr.js (A bit like Solr, but much smaller and not as bright.)结合 nodejieba("结巴"中文分词的 Node.js 版本),来实现中文全文搜索。
Lunr.js is a small, full-text search library for use in the browser. It indexes JSON documents and provides a simple search interface for retrieving documents that best match text queries.
Lunr.js 是一个用于浏览器的小型全文搜索库。它索引 JSON 文档并提供一个简单的搜索界面,用于检索与文本查询最匹配的文档。它具有以下功能特征:
- 简单的:Lunr 设计小巧但功能齐全,让您无需外部服务器端搜索服务即可提供出色的搜索体验。
- 可扩展:添加强大的语言处理器为用户查询提供更准确的结果,或调整内置处理器以更好地适应您的内容
- 到处:Lunr 没有外部依赖项,可以在您的浏览器或带有 node.js 的服务器上工作;
基本原理:Lunr 将字符串拆分为单词
标记,然后经过一系列处理(score
计算分数、metadata
元数据),最终组装成为结果对象数组;关于这块儿详细信息,可参见 Lunr.js 核心概念;值得一提的是,Lunr 不能很好支持中文,因此对于中文分析,有借助 nodejieba
做了处理。具体创建索引的过程,参见如下代码(基于 Gatsbyjs 的实现):
创建索引
const marked = require('marked')
const striptags = require(`striptags`)
const lunr = require('lunr')
require('./src/helper/lib/lunr.stemmer.support.js')(lunr)
require('./src/helper/lib/lunr.zh.js')(lunr)
const createIndex = async (docsNodes, type, cache) => {
const cacheKey = `IndexLunr`
const cached = await cache.get(cacheKey)
if (cached) {
return cached
}
const documents = []
const store = {}
// iterate over all posts
for (const node of docsNodes) {
const { slug } = node.fields
const title = node.frontmatter.title
const tag = node.frontmatter.tag
const content = striptags(marked.parse(node.rawMarkdownBody))
documents.push({
slug,
title,
tag,
content,
})
store[slug] = {
title,
tag,
content,
}
}
const index = lunr(function () {
this.use(lunr.zh)
this.ref('slug')
this.field('title')
this.field('tag')
this.field('content')
for (const doc of documents) {
this.add(doc)
}
})
const json = { index: index.toJSON(), store }
await cache.set(cacheKey, json)
return json
}
发起搜索
getQueryResult(query) {
const lunrData = this.props.lunrData
const { store } = lunrData.LunrIndex
// Lunr in action here
const index = Index.load(lunrData.LunrIndex.index)
let results = []
try {
// Search is a lunr method
results = index.search(query).map(({ ref }) => {
// Map search results to an array of {slug, title, excerpt} objects
return {
slug: ref,
...store[ref],
}
})
return results || []
} catch (error) {
console.error(`Something Error: ${error}`)
return results
}
}
handleSearch(keywords) {
let queryResultArr;
if (!keywords) {
queryResultArr = []
} else {
queryResultArr = this.getQueryResult(keywords)
}
this.keywords = keywords
this.setState({
queryResultArr: queryResultArr,
isShowResults: queryResultArr.length > 0
})
}
如何高亮文本?
highlightKeyword(keyword) {
const contentDom = document.querySelector(`#layout .wrapper .content`)
const instance = new Mark(contentDom);
instance.mark(keyword, {
exclude: ["h1"],
className: "mark-highlight"
});
}
如何定位到具体内容?
setTimeout(() => {
const markNode = document.querySelector("#layout .mark-highlight");
markNode && markNode.scrollIntoView({ behavior: "smooth", block: "start" });
}, 1000)
先前基于 Gatsbyjs 搭建一个静态网站,上文中提及的代码,完整版可参见:blog.nicelinks.site | search。在线示例:倾城周刊、快应用消息中心。