Search Starter
受众:开发者 摘要:统一搜索聚合入口 + Provider SPI +
/api/v1/searchREST 接口 + CMS / Blog / Forum / Mall 内置 provider + 热词 / 建议。
何时使用
| 场景 | 建议 |
|---|---|
| 跨业务模块的公开内容统一搜索 | GET /api/v1/search?keyword=... |
| 业务模块自己贡献搜索能力 | 实现 SearchProvider SPI 并注册为 Spring Bean |
| 热词、搜索建议、自动补全 | GET /api/v1/search/{hot-keywords,suggestions} |
| 语义检索、RAG 文档召回、相似内容推荐 | 使用 SemanticSearchManager、VectorStore 和 SearchEmbeddingProvider |
| 单一模块内部高级查询 | 直接用模块自己的 Service / Repository,不走 Search |
第一阶段不引入 Elasticsearch / OpenSearch 等重型搜索引擎;适合中等数据量的公开内容聚合。
机制说明
Search 分成两层:
| 层级 | 职责 |
|---|---|
| Search starter | SearchManager、Provider SPI、聚合排序、热词和建议 |
| Search module | /api/v1/search/** REST 入口,把 Web 请求转换为 starter 请求 |
聚合流程:
- 前端请求
/api/v1/search,传入keyword、types、sort、page、size。 SearchPageRequest转换为 starter 的SearchRequest。SearchManager按types选择已注册 Provider,向 CMS / Blog / Forum / Mall 等模块收集公开结果。- Provider 只返回当前模块可公开展示的数据。
- 聚合层按
score、时间或热度进行排序和分页。 - Search module 把
SearchResultItem转换为SearchResultItemVO,前端直接展示title、summary、url、imageUrl、sourceName。
热词和建议默认使用内存统计,适合快速体验和单实例预览;多实例正式统计可以启用 JDBC 持久化热词统计,后续更复杂的搜索分析仍可通过自定义统计 Provider 接入 Redis 或搜索引擎。
源码机制骨架
| 类型 | 职责 |
|---|---|
SearchManager | 统一搜索门面,收集 Provider 候选结果、排序、分页、热词和建议 |
SearchProvider | 业务模块搜索贡献点,只返回本模块可公开展示的数据 |
SearchRequest | 聚合搜索请求,负责关键词、类型、排序和候选请求归一化 |
SearchProviderResult | 单个 Provider 响应,区分候选记录和 Provider 内部总量 |
SearchResultItem | 前端可展示的标准搜索结果项,Provider 扩展字段放 metadata |
SearchStats | 搜索关键词统计 SPI,默认实现为内存统计 |
SearchScores | SQL Provider 第一阶段可复用的轻量关键词打分工具 |
SemanticSearchManager | 语义检索门面,负责文本 embedding、向量入库、删除和相似度检索 |
VectorStore | 向量库 SPI,默认实现为内存向量库,可替换为 pgvector / OpenSearch / AI Gateway 等实现 |
SearchEmbeddingProvider | 文本向量化 SPI,应用可接入授权的 OpenAI-compatible、AI Gateway 或本地模型 |
VectorDocument / VectorSearchRequest / VectorSearchResult | 向量文档、检索请求和排序结果模型 |
默认 DefaultSearchManager 会先按当前页需要的候选数量向每个 Provider 拉取数据,再统一做全局排序和分页。这样可以避免单个 Provider 过早分页导致其它模块的高分结果被截断。
语义检索
Search starter 内置轻量 Vector Store / Semantic Search contract,用于在不引入重型搜索引擎的情况下补齐 RAG 文档召回、相似内容推荐和开放语义检索的基础能力。
@Component
public class AiGatewayEmbeddingProvider implements SearchEmbeddingProvider {
@Override
public List<Double> embed(String text) {
// 调用当前系统授权的 AI Gateway / OpenAI-compatible embedding 服务
}
}VectorDocument indexed = semanticSearchManager.index(VectorDocument.builder("knowledge", documentId)
.type("article")
.title("Spring Open Search")
.content(markdown)
.metadata("source", "docs")
.build());
List<VectorSearchResult> results = semanticSearchManager.search(VectorSearchRequest.builder("knowledge")
.query("如何扩展搜索 Provider")
.type("article")
.limit(5)
.minScore(0.5D)
.build());如果调用方已经有向量,也可以直接传入 vector(...),此时不需要 SearchEmbeddingProvider:
semanticSearchManager.search(VectorSearchRequest.builder("knowledge")
.vector(List.of(0.12D, 0.34D, 0.56D))
.includeVector(false)
.build());默认 InMemoryVectorStore 只适合本地开发、测试和轻量预览。生产环境可以注册自己的 VectorStore Bean 替换默认实现,接入 pgvector、OpenSearch、Milvus、Qdrant、云向量库或 AI Gateway 的 Vector Store relay。
检索质量:metadata filter / 重排 / 引用质量
- metadata filter:
VectorSearchRequest支持metadataFilter(精确匹配),按tenant、来源、可见性等元数据收窄召回。 - 重排:
RerankerSPI 在向量召回之后调整顺序与分数。默认IdentityReranker不改变顺序;spring.open.search.semantic.rerank.enabled=true启用LexicalOverlapReranker(向量分与「查询词 ∩ 文档词项」重合度按lexical-weight融合,确定性、无外部依赖)。需要更高精度时注入自定义RerankerBean(cross-encoder / LLM 重排)替换。 - 引用质量:
VectorSearchResult携带vectorScore(原始向量分)、rerankScore(重排后非空)和CitationQuality(HIGH/MEDIUM/LOW,按有效分数与high-quality-score/medium-quality-score阈值划分),供 RAG 引用按置信展示或过滤。
生产向量库(JDBC,默认关闭)
JdbcVectorStore 是比内存更可生产(持久化 + 多实例共享)的基线 VectorStore,默认关闭。启用方式:spring.open.search.semantic.jdbc.enabled=true + classpath 含 spring-jdbc + 存在 DataSource;启用后优先于内存向量库。
- 向量以 CSV、metadata 以 JSON 持久化到消费方迁移创建的表,固定列名
namespace / id / type / title / content / metadata_json / vector_csv,主键(namespace, id),表名经白名单校验防注入。 - 按
namespace暴力计算余弦相似度,支持type与 metadata 过滤。不是 ANN 向量库:大规模语料请通过VectorStoreSPI 接入 pgvector / OpenSearch / 专用向量库。 spring-jdbc与jackson-databind为 optional 依赖(任何 Spring Boot 应用已具备),不污染最小 starter。设计决策见 ADR-050(RAG 生产向量库基线)。
快速开始
spring:
open:
search:
enabled: truecurl 'http://localhost:8080/api/v1/search?keyword=spring&page=1&size=10'应用引入 spring-open-module-search 即可启用 /api/v1/search 入口。业务模块只需提供 Provider,不依赖其他业务模块。
后台入口
后台菜单 统一搜索(/search)复用同一组 /api/v1/search/** 接口,展示 CMS / Blog / Forum / Mall 聚合结果、热词和搜索建议。菜单权限为 search:view,只提供只读运营检查能力,不写业务数据,也不替代各业务模块自己的后台管理页。
REST API
GET /api/v1/search?keyword=spring&page=1&size=10
GET /api/v1/search?keyword=spring&types=blog&types=forum&sort=latest
GET /api/v1/search/hot-keywords?limit=10
GET /api/v1/search/suggestions?keyword=spr&limit=10查询参数:
| 参数 | 说明 |
|---|---|
keyword | 搜索关键词,可为空;为空时按 Provider 返回公开内容 |
types | 搜索类型,可多选:cms / blog / forum / mall |
sort | 排序:relevance / latest / popular |
page | 页码,从 1 开始 |
size | 每页数量 |
响应字段:
| 字段 | 说明 |
|---|---|
type | 内容类型(与 types 一致) |
id | 业务 ID |
title | 标题 |
summary | 摘要 |
url | 前端建议跳转路径 |
imageUrl | 封面或图片 |
sourceName | 来源名称 |
sourcePath | 来源分组(分类、板块、栏目) |
score | Provider 给出的相关性分数 |
metadata | Provider 补充的元数据 |
配置项
spring:
open:
search:
enabled: true # 启用开关
max-size: 50 # 单次搜索最大分页大小
max-candidate-size: 200 # 每个 Provider 参与全局排序前最多返回的候选数
hot-keywords-limit: 10 # 热词默认返回数量
track-statistics: true # 是否记录热词统计
stats:
jdbc:
enabled: false # 是否启用 JDBC 持久化热词统计
table: search_keyword_stat # 热词统计表名
semantic:
max-results: 20 # 语义检索默认返回数量
min-score: 0.0 # 默认最小相似度分数
include-vectors: false # 语义检索结果是否默认包含原始向量| 配置 | 默认值 | 说明 |
|---|---|---|
enabled | true | 关闭后 AutoConfiguration 跳过 |
max-size | 50 | 单次搜索最大分页大小 |
max-candidate-size | 200 | Provider 候选数上限 |
hot-keywords-limit | 10 | 热词默认数量 |
track-statistics | true | 热词统计开关 |
stats.jdbc.enabled | false | 是否启用 JDBC 持久化热词统计 |
stats.jdbc.table | search_keyword_stat | JDBC 热词统计表名 |
semantic.max-results | 20 | 语义检索默认返回数量 |
semantic.min-score | 0.0 | 语义检索默认最小相似度分数 |
semantic.include-vectors | false | 语义检索结果是否默认包含原始向量 |
热词统计默认使用内存实现,适合快速体验和单实例。生产多实例场景可设置 spring.open.search.stats.jdbc.enabled=true 使用 JDBC 统计,Search module 默认提供 search_keyword_stat 表;若只单独使用 starter,需要由消费方迁移创建同名表。更复杂的搜索分析仍可通过自定义 SearchStats Bean 替换默认实现。
Provider
业务模块通过实现 SearchProvider SPI 贡献搜索能力,并注册为 Spring Bean:
@Component
public class BlogSearchProvider implements SearchProvider {
@Override
public String type() {
return SearchTypes.BLOG;
}
@Override
public SearchProviderResult search(SearchRequest request) {
// 调用本模块自己的公开查询能力,返回统一 SearchResultItem
}
}Provider 实现边界:
type()必须返回稳定的小写类型,建议复用SearchTypes或模块内稳定常量。search(...)只查询当前模块数据,不直接调用其它业务模块。- Provider 必须过滤草稿、未发布、下架、删除、举报不可见和当前用户无权查看的数据。
- Provider 返回的
records是候选结果,聚合层会统一排序和分页;Provider 不应该把前端分页当成全局分页。 metadata只放展示、跳转或轻量上下文需要的非敏感字段。
内置 Provider:
| 类型 | 来源 |
|---|---|
cms | CMS 已发布内容 |
blog | Blog 已发布文章 |
forum | Forum 可见主题 |
mall | Mall 已发布商品 |
设计约定
- Search starter 只提供聚合排序、Provider SPI、热词统计和语义检索 contract
- 语义检索只提供
SemanticSearchManager、VectorStore和 embedding contract,不强绑 AI Provider 或向量数据库 - 业务模块只贡献自己的 Provider,不依赖其他业务模块
- 聚合入口在
spring-open-module-search,不需要时可以移除该模块依赖 - 用户侧入口为 PC
/search、App/pages/search/index;后台只读运营入口为 Admin/search - 第一阶段不引入 Elasticsearch / OpenSearch
- 后续扩展:数据库搜索统计、纠错词库、同义词、拼音搜索、搜索日志、搜索引擎 Provider
注意事项
keyword为空时返回各 Provider 默认公开内容;不要把 Provider 当成无限制公开数据导出口- 全局排序按
score综合后再分页;max-candidate-size越大全局排序越准但延迟越高 - 热词只统计
track-statistics=true时;统计为内存视角,多实例不汇总 - 业务 Provider 必须只返回当前可见的公开内容,不要把草稿、未发布、被举报、被删除内容混入
- 语义检索默认内存向量库不是持久化能力;生产环境应替换为真实
VectorStore - 不要把未经授权的外部文本发送给 embedding provider;AI / 向量服务凭证仍由上游模块和运维配置负责
验证命令
./mvnw -pl spring-open-starters/spring-open-starter-search -am test -DskipITs -DskipFrontend -Dsurefire.failIfNoSpecifiedTests=false
./mvnw -pl spring-open-modules/spring-open-module-search -am test -DskipITs
./mvnw -pl spring-open-application -am test -DskipITs -DskipFrontend相关
- 业务模块:cms.md / blog.md / forum.md / mall.md
- 模块剥离规则:modules.md
- Provider 扩展模型:provider.md