对于博客或者文档,全文搜索是非常重要的功能。现如今,网站生成器工具,无论是静态的、携带完整后台的、还是基于低代码、无代码的,皆不可胜数,诸如:HexoVuepressDocsifyHugoGhost 等,但之于「搜索」功能,或未提供、或不尽如人意;基于这样的背景,本文旨在探讨如何打造本地全文搜索

如何打造体验良好的本地全文搜索?

根据先前的些许经验,要使得本地全文检索,拥有良好体验,至少要包含以下几个方面:

  • 基于全文本内容,建好索引(数据),支持用户输入关键字,能快速、准确检索到结果;
  • (实时)展示检索结果列表,高亮检索关键字,最好可以按照匹配程度高低来排序;
  • 支持点击结果列表项跳转至页面,高亮所有关键字,并将页面滚动至第一个关键字位置;
  • 更多优化,支持用户分词搜搜,增加 -\* 等匹配方式(一般来讲,满足以上三条已足够);

下面,与大家分享下,如何通过简洁操作,做到以上三条:

如何检索关键字?

方案一

基于类 Gitbook 默认方案来创建索引。如快应用文档,其数据来源于:search_plus_index.json;在本地编译 md 文件时,将所有其内容纯文本化,作为数据源,并将其他信息如路径、标题、关键字等,一并存储于该 JSON 文件;发起关键字检索时,只需读取该 json 内容,并逐个遍历;将存有关键字的项 push 至结果数组,从而展示给用户就好。

创建索引

1
gitbook build

发起搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 的实现):

创建索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
}

发起搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
})
}

如何高亮文本?

1
2
3
4
5
6
7
8
highlightKeyword(keyword) {
const contentDom = document.querySelector(`#layout .wrapper .content`)
const instance = new Mark(contentDom);
instance.mark(keyword, {
exclude: ["h1"],
className: "mark-highlight"
});
}

如何定位到具体内容?

1
2
3
4
setTimeout(() => {
const markNode = document.querySelector("#layout .mark-highlight");
markNode && markNode.scrollIntoView({ behavior: "smooth", block: "start" });
}, 1000)

先前基于 Gatsbyjs 搭建一个静态网站,上文中提及的代码,完整版可参见:blog.nicelinks.site | search。在线示例:倾城周刊快应用消息中心

您可能感兴趣的文章


静晴轩 ~ 晚晴幽草轩
个人微信公众号晚晴幽草轩;名字取自:“天意怜幽草,人间重晚晴”。
专注互联网开发(Web 大前端、快应用、小程序),以及分享优质网站、AI 应用、效率工具、心得感悟等内容。

文章目录
  1. 1. 如何检索关键字?
    1. 1.1. 方案一
      1. 1.1.1. 创建索引
      2. 1.1.2. 发起搜索
    2. 1.2. 方案二
      1. 1.2.1. 创建索引
      2. 1.2.2. 发起搜索
  2. 2. 如何高亮文本?
  3. 3. 如何定位到具体内容?
  4. 4. 您可能感兴趣的文章