Gatsby 站点搜索接入指南:从 js-search 客户端搜索到 Algolia API 搜索
【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby
导读
本文围绕 Gatsby 官方文档 adding-search.md 的核心主题展开,系统讲解如何为 Gatsby 站点添加搜索功能:先厘清搜索的三大核心组件(搜索索引、搜索引擎、搜索 UI),再深入两种主流实现路线——基于js-search的纯客户端搜索,以及基于 Algolia 的 API 托管式搜索。读完本文,你将掌握两种方案的适用场景、完整配置步骤、关键代码实现与常见故障排查方法,并能直接在真实项目中落地。
站点搜索的三大组件
Gatsby 官方文档指出,为站点添加搜索功能需要三个必要组件:
| 搜索组件 | 说明 |
|---|---|
| 搜索索引(Search index) | 以搜索友好格式存储的数据副本。索引用于优化搜索查询的速度与性能;没有索引,每次搜索都需要扫描站点中的每个页面,内容一多效率就会急剧下降。 |
| 搜索引擎(Search engine) | 对内容建立索引、接收查询词、在索引中执行查询并返回匹配文档的引擎。搜索引擎既可以是托管服务(如 Algolia),也可以是可自建托管的开源方案(如 Elasticsearch)。 |
| 搜索 UI | 站点中供用户输入查询词并查看查询结果的界面组件。部分搜索服务商会提供开箱即用的 React 组件,可直接嵌入 Gatsby 站点。 |
对应到 Gatsby 项目中,一个完整搜索流程是:先通过 Gatsby 的 GraphQL 数据层把内容(如 Markdown 博客文章)抽取出来构建索引,再由搜索引擎(客户端库或外部服务)执行查询,最后由 React 组件渲染结果。
添加搜索的两种技术路线
官方文档给出了两种截然不同的思路,选择哪种取决于内容规模、预算与隐私需求:
客户端搜索(Client-side search)
所有工作都在 Gatsby 站点内完成,无需第三方服务。优点是不依赖外部服务、无需付费;代价是需要自己写一部分代码,而且当需要索引的内容很多时,索引数据会显著增大打包体积(因为整个索引都要随站点 JS 一起下发到浏览器)。
官方推荐的实现方式与插件包括:
- 使用
js-search库:Adding Search with JS Search - 两个官方收录的 Gatsby 插件:
gatsby-plugin-elasticlunr-search(基于 ElasticLunr 的本地搜索)gatsby-plugin-local-search(基于 FlexSearch 的本地搜索)
基于 API 的搜索引擎(API-based search engine)
另一种思路是把搜索索引托管到外部搜索引擎。这种方式扩展性更好:访问者无需下载整个搜索索引(站点越大索引越大),搜索请求直接发给服务商。代价是需要为托管搜索引擎付费,或购买商业搜索服务。
官方文档列出的可选方案包括:
- Algolia—— SaaS 托管服务,有官方 Gatsby 插件
- ElasticSearch—— 开源,有商业托管
- Solr—— 开源,有商业托管
- Meilisearch—— 开源,有 Gatsby 插件
- Typesense—— 开源,有托管版本,也有 Gatsby 插件
其中最常见的是Algolia。下面分别对两条路线给出完整实操。
路线一:使用 js-search 实现客户端搜索
js-search由 Brian Vaughn 开发,是一个在客户端用 JavaScript 对 JSON 数据进行高效搜索的库,支持丰富的自定义选项。官方在 examples/using-js-search 中提供了完整示例站点,仓库内package.json显示的依赖为js-search@^1.4.3与axios@^0.20.0。
环境准备
基于官方hello worldstarter 创建新站点并安装依赖:
gatsby new js-search-example https://github.com/gatsbyjs/gatsby-starter-default cd js-search-example npm install js-search axios或使用 Yarn:
yarn add js-search axios其中axios用于处理基于 Promise 的 HTTP 请求(官方示例用它拉取远程书籍数据)。
策略选择:两种实现方式
官方文档建议按数据量选择策略:
- 中小数据集:在组件内直接获取数据并构建索引,实现简单;
- 大数据集:利用 Gatsby 的
createPagesAPI 在构建期预先取数,通过pageContext把数据注入页面模板,减少浏览器端负担。
中小数据集:组件内完成一切
在src/components/下创建SearchContainer.js,核心逻辑分五步:
- 组件挂载时触发
componentDidMount(),用 axios 拉取数据; - 成功后把数据存入 state 并调用
rebuildIndex(); - 创建并配置 js-search 搜索引擎(索引策略、清洗器、搜索索引类型);
- 用
addIndex()指定可搜索字段、用addDocuments()把数据加入索引; - 输入框内容变化时调用
search.search(),把结果渲染进表格。
关键配置代码:
const dataToSearch = new JsSearch.Search("isbn") dataToSearch.indexStrategy = new JsSearch.PrefixIndexStrategy() dataToSearch.sanitizer = new JsSearch.LowerCaseSanitizer() dataToSearch.searchIndex = new JsSearch.TfIdfSearchIndex("isbn") dataToSearch.addIndex("title") dataToSearch.addIndex("author") dataToSearch.addDocuments(bookList)Search("isbn")以isbn作为文档唯一标识(uid);PrefixIndexStrategy前缀匹配策略;LowerCaseSanitizer将查询与文本统一转为小写,避免大小写导致漏匹配;TfIdfSearchIndex基于词频-逆文档频率打分排序;addIndex("title")/addIndex("author")声明参与索引的字段。
搜索触发逻辑:
searchData = e => { const { search } = this.state const queryResult = search.search(e.target.value) this.setState({ searchQuery: e.target.value, searchResults: queryResult }) }在页面中引入该组件即可使用(示例见 examples/using-js-search/src/pages/index.js):
import Search from "../components/SearchContainer" // ... <Search />运行gatsby develop后访问http://localhost:8000即可看到可用的搜索框。
大数据集:借助 Gatsby 构建期 API
这种方式把"取数+建索引"的重活放在构建期完成。先在gatsby-node.js中通过createPages创建动态页面,并把数据塞进pageContext(相关概念见 gatsby-internals-terminology.md):
const path = require("path") const axios = require("axios") exports.createPages = ({ actions }) => { const { createPage } = actions return new Promise((resolve, reject) => { axios .get("https://bvaughn.github.io/js-search/books.json") .then(result => { const { data } = result createPage({ path: "/search", component: path.resolve(`./src/templates/ClientSearchTemplate.js`), context: { bookData: { allBooks: data.books, options: { indexStrategy: "Prefix match", searchSanitizer: "Lower Case", TitleIndex: true, AuthorIndex: true, SearchByTerm: true, }, }, }, }) resolve() }) .catch(err => { console.log(`error creating Page:${err}`) reject(new Error(`error on page creation:\n${err}`)) }) }) }仓库中的 examples/using-js-search/gatsby-node.js 与该代码一致,可直接对照。
随后在src/templates/创建ClientSearchTemplate.js读取pageContext并渲染搜索组件,在src/components/创建ClientSearch.js依据options动态配置索引策略(支持Prefix match/Exact match/All三种策略、大小写敏感/不敏感两种清洗器、TfIdf 与无序索引两种索引类型、可选的停用词分词器)。最后访问http://localhost:8000/search验证效果。
路线二:使用 Algolia 实现 API 搜索
Algolia 是站点搜索托管平台:它托管搜索索引,你告诉它有哪些页面、页面路径及导航方式,用户搜索时由 Algolia API 返回结果。它提供免费额度(每月有限次数),更高流量需付费。官方完整指南见 adding-search-with-algolia.md。
搜索的两个阶段
- 索引(Indexing):由
gatsby-plugin-algolia插件处理——每次执行gatsby build时把页面推送到 Algolia,用 GraphQL 自定义要索引的页面与字段; - 搜索界面(Search UI):本指南使用 Algolia 提供的
react-instantsearch组件库搭建(也可自行实现自定义 UI)。
提示:如果为技术文档类站点做搜索,Algolia 提供DocSearch产品,可自动从页面内容创建索引,省去手动索引,是文档站的首选方案。
项目初始化
以 Gatsby 官方博客 starter 为基础(其文章存放在content/blog,Markdown 文件的 frontmatter 含title字段,该字段会在索引查询中引用,若改名需同步修改查询):
gatsby new gatsby-algolia-guide https://github.com/gatsbyjs/gatsby-starter-blog配置索引插件
安装插件并准备凭据:
npm install gatsby-plugin-algolia注册 Algolia 账号后在 "API Keys" 页面复制Application ID、Search-Only API Key、Admin API Key,在项目根目录创建.env文件:
GATSBY_ALGOLIA_APP_ID=<App ID> GATSBY_ALGOLIA_SEARCH_KEY=<Search-Only API Key> ALGOLIA_ADMIN_KEY=<Admin API Key>注意:Admin Key 拥有写权限,必须保密,不能出现在随站点分发的任何代码中;同时建议不要把.env提交进 git,可另建不含真实值的.env.example供协作者参考(环境变量机制详见 environment-variables.md)。
修改gatsby-config.js,先加载 dotenv,再注册插件:
require("dotenv").config() module.exports = { plugins: [ // ... your existing plugins here { resolve: `gatsby-plugin-algolia`, options: { appId: process.env.GATSBY_ALGOLIA_APP_ID, apiKey: process.env.ALGOLIA_ADMIN_KEY, queries: require("./src/utils/algolia-queries") }, } ], }编写索引查询
在src/utils/algolia-queries.js中定义查询。每个 query 对应一个索引,包含:GraphQL 查询(取回待索引页面与数据)、transformer(把 GraphQL 数据转换为 Algolia record)、indexName(索引名,不存在时会在索引过程中自动创建)以及可选的settings:
const escapeStringRegexp = require("escape-string-regexp") const pagePath = `content` const indexName = `Pages` const pageQuery = `{ pages: allMarkdownRemark( filter: { fileAbsolutePath: { regex: "/${escapeStringRegexp(pagePath)}/" }, } ) { edges { node { id frontmatter { title } fields { slug } excerpt(pruneLength: 5000) } } } }` function pageToAlgoliaRecord({ node: { id, frontmatter, fields, ...rest } }) { return { objectID: id, ...frontmatter, ...fields, ...rest, } } const queries = [ { query: pageQuery, transformer: ({ data }) => data.pages.edges.map(pageToAlgoliaRecord), indexName, settings: { attributesToSnippet: [`excerpt:20`] }, }, ] module.exports = queries要点:
- 每条 record 必须有
objectID,作为唯一标识; - 本示例索引了
slug、excerpt与 frontmatter 的title三个字段,展示在搜索结果中;要索引更多字段,在pageQuery里继续加即可; attributesToSnippet让 Algolia 对excerpt属性生成命中上下文片段(snippet);- 若未使用 starter blog,需把
pagePath改为实际内容目录; - 支持多索引,本指南仅使用单个索引。
验证索引结果
运行gatsby build,输出应包含类似内容:
success Building static HTML for pages - 7.610s - 5/5 0.66/s Algolia: 1 queries to index Algolia: query 0: executing query Algolia: query 0: graphql resulted in 3 records Algolia: query 0: splitting in 1 jobs核对graphql resulted in后的数字是否等于站点页面数;若不符,说明查询有问题。随后登录 Algolia 控制台,在 "Indices" 中查看Pages索引即可看到已索引的页面数据。
常见故障排查
GraphQLError: Field "fileAbsolutePath" is not defined by type MarkdownRemarkFilterInput:说明项目中没有找到页面,请检查gatsby-source-filesystem配置的路径以及查询中的pagePath;AlgoliaSearchError: Record at the position XX objectID=xx-xx-xx-xx-xx is too big size=xxxx bytes:Algolia 单条索引记录上限为10KB,超限即报错。注意查询中已将 excerpt 裁剪到 5000 字符,实际使用时务必裁剪长字段、不要索引无关数据。
构建搜索 UI
安装所需框架:
npm install react-instantsearch algoliasearch styled-components gatsby-plugin-styled-components @styled-icons/fa-solid并在gatsby-config.js中加入gatsby-plugin-styled-components。随后按以下结构创建组件(完整代码见 adding-search-with-algolia.md):
- 搜索框
src/components/search/search-box.js:一个表单,内含输入框与放大镜图标。核心是react-instantsearch的useSearchBoxHook,它暴露当前查询词query与修改函数refine,输入变化时调用refine(e.target.value)即触发搜索; - 结果组件
src/components/search/search-result.js:用Hits渲染命中列表、Highlight高亮命中字段、Snippet显示命中上下文片段、useStats展示命中数量、PoweredBy展示归属标识(Algolia 免费套餐要求展示 "Powered by Algolia")。由于 Algolia 支持多索引,SearchResult遍历indices逐索引渲染; - 串联组件
src/components/search/index.js:用algoliasearch/lite创建搜索客户端(通过useMemo缓存客户端实例,避免重复创建、充分利用其查询缓存),InstantSearch包裹搜索框与结果;useClickOutsideHook 实现点击组件外部自动收起弹出层; - 支撑文件:
use-click-outside.js(监听mousedown/touchstart判断点击是否在组件外)与一组styled-*组件(实现"收起为图标、聚焦展开输入框、结果以 popover 弹出"的交互与样式)。若使用其他 CSS 方案,可跳过styled-*组件,直接替换为裸组件。
接入布局并在本地运行
把搜索组件放进src/components/layout.js的 header 中,并在此处定义要搜索的索引:
import Search from "./search" const searchIndices = [{ name: `Pages`, title: `Pages` }] // ... <header className="global-header"> <Search indices={searchIndices} /> {header} </header>运行gatsby develop即可得到可用的搜索功能。
部署到 Netlify 的环境变量问题
若把项目部署到 Netlify,构建会报错AlgoliaSearchError: Please provide an application ID——因为 Netlify 拿不到未提交的.env中的配置。解决办法是在 Netlify 站点后台Settings > Build & deploy > Environment > Environment variables中声明GATSBY_ALGOLIA_APP_ID、GATSBY_ALGOLIA_SEARCH_KEY、ALGOLIA_ADMIN_KEY三个环境变量(值与本地.env一致),重新部署后搜索即可正常工作。
两条路线的取舍与总结
| 维度 | 客户端搜索(js-search) | API 搜索(Algolia) |
|---|---|---|
| 服务依赖 | 无外部服务,纯本地 | 依赖第三方托管服务 |
| 成本 | 免费,仅付出开发成本 | 免费额度有限,高流量需付费 |
| 可扩展性 | 索引随内容增大,打包体积显著膨胀 | 索引在服务端,站点体积不受影响 |
| 实现复杂度 | 需自行编写取数与索引逻辑 | 插件负责索引,UI 用现成组件 |
| 适用场景 | 中小型内容站点 | 内容量大、持续增长的站点 |
无论选择哪条路线,都可以回到 Gatsby 官方文档的顶层指南 adding-search.md 回顾三大组件模型:搜索索引是性能基石、搜索引擎是查询核心、搜索 UI是用户体验的最终呈现——三者在两种方案中的实现位置不同,但职责边界始终清晰。读者也可以参考仓库内benchmarks/与examples/目录下的搜索相关示例,结合实际内容规模选择最合适的方案。
【免费下载链接】gatsbyReact-based framework with performance, scalability, and security built in.项目地址: https://gitcode.com/gh_mirrors/ga/gatsby
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考