1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
| package index
|
| import (
| "database/sql"
| "encoding/json"
| "sort"
| )
| // FTSResult 全文搜索结果
| type FTSResult struct {
| ID int64
| Path string
| Title string
| Section string
| Rank float64
| }
|
| // CreateFTS 创建 FTS5 虚拟表
| func (s *Store) CreateFTS() error {
| _, err := s.db.Exec(`
| CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
| title,
| content,
| tags,
| entities,
| content='nodes',
| content_rowid='id'
| )
| `)
| return err
| }
|
| // PopulateFTS 填充 FTS 索引
| func (s *Store) PopulateFTS() error {
| _, err := s.db.Exec(`
| INSERT INTO nodes_fts(rowid, title, content, tags, entities)
| SELECT id, title, content_fts, tags, entities FROM nodes
| `)
| return err
| }
|
| // FTSSearch 全文搜索
| func (s *Store) FTSSearch(keywords []string, limit int) ([]FTSResult, error) {
| if len(keywords) == 0 {
| return nil, nil
| }
|
| // 对每个关键词单独搜索,然后合并结果(去重)
| // 这样即使某个关键词匹配不到,其他关键词也能找到结果
| seen := make(map[int64]bool)
| var allResults []FTSResult
|
| 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
| }
|
| 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()
| }
|
| // 按 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 获取节点内容
| func (s *Store) GetNodeContent(id int64) (string, []string, []string, error) {
| var content, tagsJSON, entitiesJSON string
| err := s.db.QueryRow("SELECT content_fts, tags, entities FROM nodes WHERE id = ?", id).
| Scan(&content, &tagsJSON, &entitiesJSON)
| if err == sql.ErrNoRows {
| return "", nil, nil, nil
| }
| if err != nil {
| return "", nil, nil, err
| }
|
| // 解析 JSON 数组
| var tags, entities []string
| if tagsJSON != "" {
| if err := json.Unmarshal([]byte(tagsJSON), &tags); err != nil {
| tags = nil
| }
| }
| if entitiesJSON != "" {
| if err := json.Unmarshal([]byte(entitiesJSON), &entities); err != nil {
| entities = nil
| }
| }
|
| return content, tags, entities, nil
| }
|
|