Skip to content

搜索方案选型对比

为 VitePress 站点选择合适的搜索方案是提升用户体验的关键决策。本文全面对比本地搜索、Algolia DocSearch 和其他第三方方案,帮助你做出最佳选择。

方案概览

特性本地搜索Algolia DocSearchPagefind自定义搜索
成本免费免费(开源项目)/ 付费免费取决于实现
索引构建构建时云端爬虫构建后自定义
索引大小全量加载按需加载分片加载取决于实现
搜索速度非常快取决于实现
中文支持⚠️ 需配置✅ 原生支持✅ 支持取决于实现
离线可用✅ 是❌ 否✅ 是取决于实现
配置复杂度
维护成本
适用规模< 500 页任意< 1000 页任意

本地搜索

工作原理

text
┌─────────────┐     构建时      ┌──────────────┐
│  Markdown    │ ──────────────→ │  搜索索引     │
│  源文件      │   miniSearch    │  (JSON)      │
└─────────────┘                 └──────────────┘


┌─────────────┐     运行时      ┌──────────────┐
│  用户输入    │ ──────────────→ │  客户端搜索   │
│  搜索词      │   miniSearch    │  结果匹配     │
└─────────────┘                 └──────────────┘

VitePress 内置本地搜索基于 miniSearch,在构建时生成搜索索引,运行时在浏览器端完成搜索。

优势

  • 零成本:无需第三方服务
  • 零依赖:无需申请 API Key
  • 离线可用:PWA 场景下可用
  • 配置简单:开箱即用
  • 数据私密:索引不离开站点

劣势

  • 索引体积:所有页面索引打包进 JS,页面多时影响加载
  • 中文分词:默认不支持中文分词,需额外配置
  • 搜索质量:模糊匹配能力有限,同义词/拼写纠错支持弱
  • 更新延迟:需重新构建才能更新索引

最佳配置

ts
// .vitepress/config.mts
export default defineConfig({
  themeConfig: {
    search: {
      provider: 'local',
      options: {
        // 中文分词优化
        _render(src, env, md) {
          // 添加 HTML 标签以支持中文搜索
          const html = md.render(src, env)
          // 移除 HTML 标签,保留纯文本
          return html
            .replace(/<[^>]+>/g, ' ')
            .replace(/\s+/g, ' ')
            .trim()
        },
        // 详细视图配置
        detailedView: true,
        // 翻译
        locales: {
          root: {
            translations: {
              button: { buttonText: '搜索文档', buttonAriaLabel: '搜索文档' },
              modal: {
                noResultsText: '无法找到相关结果',
                resetButtonTitle: '清除查询条件',
                footer: { selectText: '选择', navigateText: '切换', closeText: '关闭' }
              }
            }
          }
        }
      }
    }
  }
})

适用场景

  • 个人博客或小型文档站(< 200 页)
  • 内网/离线环境
  • 不想依赖第三方服务
  • 快速原型验证

Algolia DocSearch

工作原理

text
┌─────────────┐    爬虫抓取     ┌──────────────┐    API 查询    ┌──────────────┐
│  VitePress   │ ─────────────→ │  Algolia     │ ←──────────── │  用户浏览器   │
│  站点页面    │   定期爬取      │  云端索引     │   搜索请求     │  搜索结果     │
└─────────────┘                 └──────────────┘               └──────────────┘

Algolia DocSearch 通过爬虫定期抓取站点内容,在云端建立索引,用户搜索时通过 API 实时查询。

优势

  • 搜索质量高:支持同义词、拼写纠错、高亮匹配
  • 中文支持好:原生支持中文分词
  • 零运维:索引托管在 Algolia 云端
  • 分析功能:搜索统计和用户行为分析
  • 扩展性强:支持大规模文档(10 万+ 页面)
  • 按需加载:搜索索引不占用站点体积

劣势

  • 需要申请:开源项目免费,商业项目需付费
  • 更新延迟:爬虫定期抓取,非实时更新
  • 网络依赖:需联网才能搜索
  • 配置复杂:需配置爬虫、API Key 等
  • 国内访问:Algolia 服务器在海外,国内可能有延迟

最佳配置

ts
// .vitepress/config.mts
export default defineConfig({
  themeConfig: {
    search: {
      provider: 'algolia',
      options: {
        appId: 'YOUR_APP_ID',
        apiKey: 'YOUR_API_KEY',
        indexName: 'your-index-name',
        // 搜索行为优化
        searchParameters: {
          facetFilters: ['language:zh'],
          hitsPerPage: 10
        },
        // 结果转换
        transformItems(items) {
          return items.map(item => ({
            ...item,
            // 高亮匹配文本
            highlight: item._highlightResult?.hierarchy?.lvl1?.value || item.hierarchy.lvl1
          }))
        },
        // 多语言
        locales: {
          '/zh/': {
            placeholder: '搜索文档',
            translations: {
              button: { buttonText: '搜索' },
              modal: { noResultsText: '没有找到相关结果' }
            }
          }
        }
      }
    }
  }
})

适用场景

  • 中大型文档站(> 200 页)
  • 开源项目文档
  • 需要高质量搜索体验
  • 有国际用户(多语言搜索)
  • 需要搜索分析数据

Pagefind

工作原理

text
┌─────────────┐    构建后       ┌──────────────┐    运行时      ┌──────────────┐
│  VitePress   │ ─────────────→ │  Pagefind    │ ─────────────→ │  浏览器搜索   │
│  构建输出    │   索引生成      │  分片索引     │   按需加载     │  结果匹配     │
└─────────────┘                 └──────────────┘               └──────────────┘

Pagefind 是一个完全静态的搜索方案,在构建完成后生成索引,支持分片按需加载。

优势

  • 免费开源:无需申请
  • 分片加载:大站点索引不会一次性加载
  • 中文支持:内置中文分词
  • 离线可用:静态索引,PWA 友好
  • UI 自定义:提供灵活的 UI 组件

劣势

  • 社区较小:相比 Algolia 生态较小
  • VitePress 集成:需手动集成,非官方支持
  • 搜索质量:介于本地搜索和 Algolia 之间
  • 维护风险:单人维护项目

集成方法

bash
# 安装 Pagefind
npm install -D pagefind
ts
// 构建后运行 Pagefind 索引
// package.json
{
  "scripts": {
    "build": "vitepress build docs && npx pagefind --site docs/.vitepress/dist --output-path public/pagefind",
    "preview": "vitepress preview docs"
  }
}
vue
<!-- .vitepress/theme/components/PagefindSearch.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const searchRef = ref<HTMLDivElement>()
const results = ref<any[]>([])
const query = ref('')

onMounted(async () => {
  // 动态加载 Pagefind
  if (typeof window !== 'undefined') {
    // @ts-expect-error Pagefind 运行时注入
    window.pagefind = await import('/pagefind/pagefind.js')
  }
})

async function handleSearch() {
  if (!window.pagefind || !query.value) {
    results.value = []
    return
  }
  const search = await window.pagefind.search(query.value)
  results.value = await Promise.all(
    search.results.map((r: any) => r.data())
  )
}
</script>

<template>
  <div class="pagefind-search">
    <input
      v-model="query"
      type="text"
      placeholder="搜索文档..."
      @input="handleSearch"
    />
    <ul v-if="results.length" class="search-results">
      <li v-for="result in results" :key="result.url">
        <a :href="result.url" v-html="result.meta?.title" />
        <p v-if="result.excerpt" v-html="result.excerpt" />
      </li>
    </ul>
  </div>
</template>

<style scoped>
.pagefind-search {
  position: relative;
}

.pagefind-search input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  background: var(--vp-c-bg-soft);
  color: var(--vp-c-text-1);
}

.search-results {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  background: var(--vp-c-bg);
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  max-height: 400px;
  overflow-y: auto;
  z-index: 99;
}

.search-results li {
  padding: 8px 12px;
  border-bottom: 1px solid var(--vp-c-divider);
}

.search-results a {
  color: var(--vp-c-brand);
  font-weight: 500;
}
</style>

适用场景

  • 中型文档站(200-1000 页)
  • 需要更好的中文支持但不想用 Algolia
  • 需要离线搜索
  • 希望控制搜索索引大小

自定义搜索方案

Elasticsearch + Vue

适用于已有 Elasticsearch 基础设施的团队:

ts
// composables/useElasticsearch.ts
import { ref } from 'vue'

interface SearchResult {
  title: string
  url: string
  excerpt: string
  score: number
}

export function useElasticsearch(indexUrl: string) {
  const results = ref<SearchResult[]>([])
  const isLoading = ref(false)
  const error = ref<string | null>(null)

  async function search(query: string) {
    if (!query) {
      results.value = []
      return
    }

    isLoading.value = true
    error.value = null

    try {
      const response = await fetch(`${indexUrl}/_search`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          query: {
            multi_match: {
              query,
              fields: ['title^3', 'content^2', 'headings^2'],
              type: 'best_fields',
              fuzziness: 'AUTO'
            }
          },
          highlight: {
            fields: {
              content: { fragment_size: 150, number_of_fragments: 1 }
            }
          },
          size: 10
        })
      })

      const data = await response.json()
      results.value = data.hits.hits.map((hit: any) => ({
        title: hit._source.title,
        url: hit._source.url,
        excerpt: hit.highlight?.content?.[0] || hit._source.content?.slice(0, 150),
        score: hit._score
      }))
    } catch (e) {
      error.value = '搜索请求失败'
      results.value = []
    } finally {
      isLoading.value = false
    }
  }

  return { results, isLoading, error, search }
}

MeiliSearch

适用于需要自托管搜索服务的场景:

bash
# Docker 部署 MeiliSearch
docker run -d \
  -p 7700:7700 \
  -e MEILI_MASTER_KEY=your-master-key \
  getmeili/meilisearch:latest
ts
// composables/useMeiliSearch.ts
import { ref } from 'vue'
import { MeiliSearch } from 'meilisearch'

export function useMeiliSearch(host: string, apiKey: string, indexName: string) {
  const client = new MeiliSearch({ host, apiKey })
  const results = ref<any[]>([])
  const isLoading = ref(false)

  async function search(query: string) {
    if (!query) {
      results.value = []
      return
    }
    isLoading.value = true
    try {
      const searchResults = await client.index(indexName).search(query, {
        attributesToHighlight: ['title', 'content'],
        attributesToCrop: ['content'],
        cropLength: 150,
        limit: 10
      })
      results.value = searchResults.hits
    } finally {
      isLoading.value = false
    }
  }

  return { results, isLoading, search }
}

选型决策树

text
需要搜索功能?
├── 否 → 不配置搜索
└── 是 → 页面数量?
    ├── < 200 页 → 中文内容为主?
    │   ├── 是 → 本地搜索(配置分词)
    │   └── 否 → 本地搜索
    ├── 200-1000 页 → 是开源项目?
    │   ├── 是 → Algolia DocSearch(推荐)
    │   └── 否 → 预算?
    │       ├── 免费 → Pagefind
    │       └── 有预算 → Algolia
    └── > 1000 页 → 自托管 vs 云服务?
        ├── 云服务 → Algolia
        └── 自托管 → Elasticsearch / MeiliSearch

性能对比

索引体积

方案100 页500 页2000 页
本地搜索~50 KB~250 KB~1 MB
Algolia0 KB0 KB0 KB
Pagefind~30 KB(分片)~150 KB(分片)~600 KB(分片)

说明

Algolia 索引存储在云端,不占用站点体积。Pagefind 使用分片加载,初始仅加载核心索引。

搜索延迟

方案首次搜索后续搜索网络请求
本地搜索50-100 ms10-30 ms
Algolia100-300 ms50-150 ms每次搜索
Pagefind50-150 ms10-50 ms首次加载

迁移指南

从本地搜索迁移到 Algolia

ts
// 1. 申请 Algolia DocSearch
// 访问 https://docsearch.algolia.com/apply/

// 2. 修改配置
// 修改前:
export default defineConfig({
  themeConfig: {
    search: {
      provider: 'local'
    }
  }
})

// 修改后:
export default defineConfig({
  themeConfig: {
    search: {
      provider: 'algolia',
      options: {
        appId: 'YOUR_APP_ID',
        apiKey: 'YOUR_API_KEY',
        indexName: 'your-index-name'
      }
    }
  }
})

从 Algolia 迁移到 Pagefind

bash
# 1. 安装 Pagefind
npm install -D pagefind

# 2. 修改构建脚本
# 在 package.json 中添加构建后索引步骤

# 3. 替换搜索组件
# 移除 Algolia 配置,使用自定义 Pagefind 组件

最佳实践

1. 搜索优化通用建议

markdown
<!-- ✅ 好的标题结构:层级清晰 -->
## 安装指南
### 系统要求
### 安装步骤

<!-- ❌ 差的标题结构:缺少层级 -->
## 安装指南
#### 系统要求  <!-- 跳过了三级标题 -->
#### 安装步骤

2. 提升搜索质量的技巧

技巧说明
标题层级一致确保标题层级连续,利于索引
关键词前置重要关键词放在标题和首段
避免重复内容相同内容不要出现在多个页面
使用 alt 文本图片 alt 有助于搜索索引
排除无用页面搜索配置中排除 404、变更日志等

3. 搜索排除配置

ts
// 排除不需要搜索的页面
export default defineConfig({
  themeConfig: {
    search: {
      provider: 'local',
      options: {
        // 排除指定页面
        exclude: [
          '/changelog',
          '/404',
          '/tags/**',
          '/community/team'
        ]
      }
    }
  }
})

4. 搜索体验优化

ts
// Algolia 高级配置:搜索结果优化
export default defineConfig({
  themeConfig: {
    search: {
      provider: 'algolia',
      options: {
        appId: 'YOUR_APP_ID',
        apiKey: 'YOUR_API_KEY',
        indexName: 'your-index-name',
        // 精确匹配权重更高
        searchParameters: {
          exactOnSingleWordQuery: 'attribute',
          attributesToSnippet: ['content:20']
        },
        // 自定义结果排序
        transformItems(items) {
          return items.sort((a, b) => {
            // 官方文档优先
            if (a.url.includes('/guide/') && !b.url.includes('/guide/')) return -1
            if (!a.url.includes('/guide/') && b.url.includes('/guide/')) return 1
            return 0
          })
        }
      }
    }
  }
})

常见问题

Q: 本地搜索中文不生效?

确保 miniSearch 配置了中文分词。可以自定义 _render 函数:

ts
search: {
  provider: 'local',
  options: {
    _render(src, env, md) {
      const html = md.render(src, env)
      return html.replace(/<[^>]+>/g, ' ').trim()
    }
  }
}

Q: Algolia 搜索结果更新延迟?

Algolia 爬虫默认每 24 小时抓取一次。可以在 Algolia Dashboard 手动触发抓取,或配置 Webhook 在部署后自动触发。

Q: 如何在搜索结果中排除某些页面?

ts
// 方式一:frontmatter 排除
---
search: false
---

// 方式二:配置排除(本地搜索)
search: {
  provider: 'local',
  options: { exclude: ['/changelog'] }
}

// 方式三:爬虫配置排除(Algolia)
// 在 DocSearch 配置 JSON 中设置
{
  "exclude_paths": ["changelog", "blog/**"]
}

Q: 大型文档站应该选择哪个方案?

  • 页面 > 2000:推荐 Algolia(搜索质量最好)
  • 页面 500-2000:Algolia 或 Pagefind 均可
  • 内网/离线环境:Pagefind 或本地搜索
  • 自托管需求:MeiliSearch 或 Elasticsearch

相关资源

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献