Files
Agent-System-Backend/internal/store/search_postgres.go
T
weichen b9bea5bee0 Filter private knowledge before applying shared result limits
Why:
Higher-ranked private rows could exhaust SQL candidate limits and hide an
otherwise authorized published slice from a member's current-project search.

Evidence:
SELF TestAuthorizedKnowledgeFiltersBeforeTopK failed with no shared hits before
the change and passed after it. SELF go test ./... and the isolated PostgreSQL
Store suite passed; independent replay and current-HEAD CI remain owner gates.

Impact:
Scan the resource-authorized shared fulltext path until enough allowed results
are found. Preserve ordinary legacy retrieval candidate limits and semantics.

Refs:
Dev project-member-access plan r1 Task 1; R5 published knowledge availability
2026-10-13 08:47:19 +00:00

106 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
core "agent-system-core"
)
// searchKnowledgePG:PG 档知识检索(设计 D16)。
// 单查询混合:tsvector('simple' 配置,词级)OR trigram LIKE 子串(中文兜底;
// LIKE 由 pg_trgm GIN 索引加速)。排序:ts_rank 优先,命中时间次之。
func (s *Store) searchKnowledgePG(owner string, q core.RetrievalQuery) ([]core.RetrievalHit, error) {
topK := q.TopK
if topK <= 0 {
topK = 5
}
text := q.Text
rows, err := s.db.Query(s.q(`
SELECT k.id,k.owner,k.project_id,k.source,k.title,k.ref,k.chunk,k.created_at,
ts_rank(k.tsv, websearch_to_tsquery('simple', ?)) AS r
FROM knowledge k
WHERE (k.owner=? OR k.owner='')
AND (? = '' OR k.tsv @@ websearch_to_tsquery('simple', ?) OR k.chunk LIKE ? OR k.title LIKE ?)
ORDER BY r DESC, k.created_at DESC
LIMIT ?`), text, owner, text, text, "%"+text+"%", "%"+text+"%", topK*2)
if err != nil {
return nil, err
}
defer rows.Close()
out := []core.RetrievalHit{}
for rows.Next() {
var d core.KnowledgeDoc
var own, ca string
var rank float64
if err := rows.Scan(&d.ID, &own, &d.ProjectID, &d.Source, &d.Title, &d.Ref, &d.Chunk, &ca, &rank); err != nil {
return nil, err
}
d.Owner = own
d.CreatedAt, _ = parseTime(ca)
if !knowledgeVisible(owner, q, d) {
continue
}
score := rank
if score == 0 {
score = 0.5 // 纯子串命中(trigram/中文兜底)给保底分
}
out = append(out, core.RetrievalHit{Doc: d, Score: score, Kind: "fulltext"})
if len(out) >= topK {
break
}
}
return out, rows.Err()
}
// searchKnowledgePGShared EXT-8 切片 2 共享版:他人 owner 的行 + 谓词后置判定
// (allow 回调由上层注入;排序与自检索同构)。
func (s *Store) searchKnowledgePGShared(requester string, q core.RetrievalQuery, pred knowledgePred) ([]core.RetrievalHit, error) {
return s.searchKnowledgePGSharedCandidates(requester, q, pred, true)
}
func (s *Store) searchKnowledgePGSharedCandidates(requester string, q core.RetrievalQuery, pred knowledgePred, bounded bool) ([]core.RetrievalHit, error) {
topK := q.TopK
if topK <= 0 {
topK = 5
}
text := q.Text
query := `
SELECT k.id,k.owner,k.project_id,k.source,k.title,k.ref,k.chunk,k.created_at,
ts_rank(k.tsv, websearch_to_tsquery('simple', ?)) AS r
FROM knowledge k
WHERE (k.owner<>? AND k.owner<>'')
AND (? = '' OR k.tsv @@ websearch_to_tsquery('simple', ?) OR k.chunk LIKE ? OR k.title LIKE ?)
ORDER BY r DESC, k.created_at DESC`
args := []any{text, requester, text, text, "%" + text + "%", "%" + text + "%"}
if bounded {
query += " LIMIT ?"
args = append(args, topK*2)
}
rows, err := s.db.Query(s.q(query), args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := []core.RetrievalHit{}
for rows.Next() {
var d core.KnowledgeDoc
var own, ca string
var rank float64
if err := rows.Scan(&d.ID, &own, &d.ProjectID, &d.Source, &d.Title, &d.Ref, &d.Chunk, &ca, &rank); err != nil {
return nil, err
}
d.Owner = own
d.CreatedAt, _ = parseTime(ca)
if !pred(d) {
continue
}
score := rank
if score == 0 {
score = 0.5
}
out = append(out, core.RetrievalHit{Doc: d, Score: score, Kind: "fulltext"})
if len(out) >= topK {
break
}
}
return out, rows.Err()
}