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

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

如何检索关键字?

方案一

基于类 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。在线示例:倾城周刊快应用消息中心

您可能感兴趣的文章