| | |
| | | import ( |
| | | "database/sql" |
| | | "encoding/json" |
| | | "strings" |
| | | "sort" |
| | | ) |
| | | |
| | | // FTSResult 全文搜索结果 |
| | | type FTSResult struct { |
| | | ID int64 |
| | |
| | | title, |
| | | content, |
| | | tags, |
| | | entities, |
| | | content='nodes', |
| | | content_rowid='id' |
| | | entities |
| | | ) |
| | | `) |
| | | return err |
| | |
| | | return nil, nil |
| | | } |
| | | |
| | | // 构建 FTS5 查询 |
| | | query := strings.Join(keywords, " OR ") |
| | | // 对每个关键词单独搜索,然后合并结果(去重) |
| | | // 这样即使某个关键词匹配不到,其他关键词也能找到结果 |
| | | seen := make(map[int64]bool) |
| | | var allResults []FTSResult |
| | | |
| | | rows, err := s.db.Query(` |
| | | SELECT n.id, n.path, n.title, n.section, fts.rank |
| | | FROM nodes_fts fts |
| | | JOIN nodes n ON n.id = fts.rowid |
| | | WHERE nodes_fts MATCH ? |
| | | ORDER BY fts.rank |
| | | LIMIT ? |
| | | `, query, limit) |
| | | if err != nil { |
| | | return nil, err |
| | | } |
| | | defer rows.Close() |
| | | |
| | | var results []FTSResult |
| | | for rows.Next() { |
| | | var r FTSResult |
| | | if err := rows.Scan(&r.ID, &r.Path, &r.Title, &r.Section, &r.Rank); err != nil { |
| | | return nil, err |
| | | for _, kw := range keywords { |
| | | rows, err := s.db.Query(` |
| | | SELECT n.id, n.path, n.title, n.section, fts.rank |
| | | FROM nodes_fts fts |
| | | JOIN nodes n ON n.id = fts.rowid |
| | | WHERE nodes_fts MATCH ? |
| | | ORDER BY fts.rank |
| | | LIMIT ? |
| | | `, kw, limit) |
| | | if err != nil { |
| | | // 单个关键词搜索失败,跳过继续 |
| | | continue |
| | | } |
| | | results = append(results, r) |
| | | |
| | | for rows.Next() { |
| | | var r FTSResult |
| | | if err := rows.Scan(&r.ID, &r.Path, &r.Title, &r.Section, &r.Rank); err != nil { |
| | | rows.Close() |
| | | continue |
| | | } |
| | | // 去重 |
| | | if !seen[r.ID] { |
| | | seen[r.ID] = true |
| | | allResults = append(allResults, r) |
| | | } |
| | | } |
| | | rows.Close() |
| | | } |
| | | return results, nil |
| | | |
| | | // 按 rank 排序(FTS5 的 rank 越小越好) |
| | | sort.Slice(allResults, func(i, j int) bool { |
| | | return allResults[i].Rank < allResults[j].Rank |
| | | }) |
| | | |
| | | // 限制返回数量 |
| | | if limit > 0 && len(allResults) > limit { |
| | | allResults = allResults[:limit] |
| | | } |
| | | |
| | | return allResults, nil |
| | | } |
| | | |
| | | // GetNodeContent 获取节点内容 |