search

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 26, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

Package search 提供全文搜索。

切词在 Go 侧完成(中日韩按二元组,见 tokenize.go),结果存进独立的 post_search 表, 检索走 GIN 索引——全程不依赖任何 PostgreSQL 扩展,装机即用。

索引由后台对账维护:按 posts.updated_at 找出落后的行重建,不在内容模块的写路径上挂钩子。 对外只暴露 Searcher 接口,将来换成 meilisearch 一类外部引擎时调用方不动。

Index

Constants

View Source
const Name = "search"

Name 是模块名,也是迁移版本表的后缀(goose_db_version_search)。

Variables

This section is empty.

Functions

func TSQuery

func TSQuery(text string) string

TSQuery 把用户输入转成 to_tsquery('simple', …) 能解析的查询串;无可用词元时返回空串。

同一段 CJK 文字切出的二元组用短语算子 `<->` 相连:「东京都」切成 东京 / 京都, 若用 `&` 连接,「京都的东京饭店」这种八竿子打不着的文字也会命中;`<->` 要求两个 二元组在原文中相邻,效果等价于子串匹配。段与段之间用 `&`,即各段都要出现。

只有一个汉字的段落对不上二元组索引(索引里是「中文」而不是「中」),故退化为 前缀匹配 `'中':*`——它能命中所有以该字开头的二元组。

func Tokenize

func Tokenize(text string) []string

Tokenize 把一段文本切成检索用的词元。

中日韩文字按**二元组**切分(「全文搜索」→ 全文 / 文搜 / 搜索),拉丁字母与数字 按连续串切分并转小写,其余字符一律当分隔符。

不引分词词典:词典要么体积可观、要么需要持续更新,而一旦没收录某个新词, 整篇文章就再也搜不到——二元组不会有这种「静默漏召回」。代价是「东京都」这类 查询会连带命中「京都」,由查询侧的短语匹配收窄(见 TSQuery)。

Types

type Doc

type Doc struct {
	ID        int64     `bun:"id"`
	Title     string    `bun:"title"`
	Excerpt   string    `bun:"excerpt"`
	Content   string    `bun:"content"`
	UpdatedAt time.Time `bun:"updated_at"`
}

Doc 是一条待索引的内容。

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler 提供搜索接口。

func NewHandler

func NewHandler(service *Service) *Handler

NewHandler 构造 Handler。

func (*Handler) Register

func (h *Handler) Register(console, public huma.API)

Register 挂载接口。

搜索是匿名可用的公开能力,只返回已发布且公开的内容; 索引状态与重建属站点维护,走 Console 并复用 settings:manage—— 为一个维护端点单开一条权限串,只会让角色编辑器多一个没人看得懂的选项。

type Hit

type Hit struct {
	ID          int64      `bun:"id"           json:"id"`
	Type        string     `bun:"type"         json:"type" enum:"post,page"`
	Title       string     `bun:"title"        json:"title"`
	Slug        string     `bun:"slug"         json:"slug"`
	Excerpt     string     `bun:"excerpt"      json:"excerpt"`
	PublishedAt *time.Time `bun:"published_at" json:"publishedAt"`
	Score       float64    `bun:"score"        json:"score" doc:"相关度,只在同一次查询内可比"`
}

Hit 是一条搜索结果。

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module 是搜索模块。

func New

func New() *Module

New 构造模块。

func (*Module) Migrations

func (m *Module) Migrations() fs.FS

Migrations 实现 app.Migrator。

func (*Module) Name

func (m *Module) Name() string

Name 实现 app.Module。

func (*Module) Register

func (m *Module) Register(a *app.App) error

Register 实现 app.Module:只做装配,不访问数据库。

func (*Module) Routes

func (m *Module) Routes(r app.Router)

Routes 实现 app.RouteProvider。

func (*Module) Start

func (m *Module) Start(ctx context.Context) error

Start 实现 app.Starter:启动索引对账,ctx 取消时退出。

type Params

type Params struct {
	// Query 是用户输入的原始关键词。
	Query string
	// Types 限定内容类型(post / page);为空表示不限。
	Types []string
	Page  api.PageParams
}

Params 是一次搜索的入参。

type Result

type Result struct {
	Hits  []Hit
	Total int
}

Result 是一次搜索的结果。

type Searcher

type Searcher interface {
	Search(ctx context.Context, params *Params) (*Result, error)
}

Searcher 是搜索能力的抽象。

前台与接口都只依赖这个接口,将来换成 meilisearch 一类外部引擎时, 换掉实现即可,调用方不动。

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service 是基于 PostgreSQL tsvector 的搜索实现。

func From

func From(a *app.App) *Service

From 取回已装配的搜索服务;未装配时返回 nil。

func NewService

func NewService(store *Store, logger *slog.Logger) *Service

NewService 构造 Service。

func (*Service) Reindex

func (s *Service) Reindex(ctx context.Context) (int, error)

Reindex 把全部内容标记为待索引,由后台对账逐批重建,返回待重建的条数。

func (*Service) Run

func (s *Service) Run(ctx context.Context)

Run 持续把过期的索引补齐,直到 ctx 取消。

不在内容模块的写路径上挂钩子,而是按 posts.updated_at 对账:写路径有七八条 (创建、改稿、发布、撤回、进回收站、恢复、定时发布扫描……),将来还会加, 漏挂一处的表现是「那部分内容永远搜不到」且没有任何报错。对账则天然覆盖全部写法, 连批量导入与直接改库都跟得上,代价只是最多 reconcileInterval 的延迟。

func (*Service) Search

func (s *Service) Search(ctx context.Context, params *Params) (*Result, error)

Search 按相关度返回命中的内容;关键词切不出任何词元时返回空结果而不是报错。

func (*Service) SearchPostIDs

func (s *Service) SearchPostIDs(ctx context.Context, query string, limit, offset int) (
	ids []int64, total int, err error,
)

SearchPostIDs 按相关度返回文章 ID 与总数,供主题前台取回完整视图。

实现 theme.Searcher:前台只要「哪些、什么顺序」,作者与分类标签由主题自己补齐。 固定只搜文章——搜索页展示的是文章流,独立页面(关于、联系我们)混进去只会干扰; 要搜页面的调用方走 Public 接口的 type 参数。

func (*Service) Stats

func (s *Service) Stats(ctx context.Context) (Stats, error)

Stats 返回索引进度。

type Stats

type Stats struct {
	Total   int `json:"total"   doc:"内容总数"`
	Indexed int `json:"indexed" doc:"已建索引的条数"`
	Pending int `json:"pending" doc:"索引落后于内容、等待重建的条数"`
}

Stats 是索引的进度快照。

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store 负责索引的读写。

func NewStore

func NewStore(db *bun.DB) *Store

NewStore 构造 Store。

func (*Store) Index

func (s *Store) Index(ctx context.Context, row *Doc) error

Index 为一条内容写入索引。

indexed_at 写的是**这次读到的** updated_at 而不是 now():若内容在读出之后又被改过, 它的 updated_at 会更大,这一行仍然算过期,下一轮自然会重建。写 now() 则会把 那次修改吞掉,索引从此停在旧内容上。

func (*Store) MarkAllStale

func (s *Store) MarkAllStale(ctx context.Context) (int, error)

MarkAllStale 清空索引,返回待重建的条数。切词规则改动后用它整体重建。

func (*Store) Search

func (s *Store) Search(ctx context.Context, tsquery string, types []string, limit, offset int) (
	[]Hit, int, error,
)

Search 按相关度分页返回命中的内容,只包含前台可见的条目。

tsquery 由调用方给出(见 TSQuery),空串时不应走到这里。

func (*Store) Stale

func (s *Store) Stale(ctx context.Context, limit int) ([]Doc, error)

Stale 取出索引落后于内容的行,最多 limit 条。

不排序:调用方会一批批取到取空为止,先处理哪些并不重要,而 ORDER BY 会平白 多一次排序——这条查询每两秒就要跑一次。

func (*Store) Stats

func (s *Store) Stats(ctx context.Context) (Stats, error)

Stats 返回索引进度。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL