feat: search --hop walks FROM_FILE to Person. (#30)
Tests / Test (push) Failing after 4s
Tests / Release (semver) (push) Skipped

Parser no longer errors; hop 1 returns File, hop 3 reaches Person.
Rebuild writes Leaf-[:FROM_FILE]->File so the walk is not empty
on a fresh index (Gitea #17).
This commit is contained in:
2026-08-14 10:51:35 +01:00
committed by GitHub
co-authored by GitHub
parent 9d1a3f3c70
commit a88dbb490c
17 changed files with 261 additions and 59 deletions
+63 -5
View File
@@ -56,6 +56,12 @@ func runSearch(args []string) int {
fmt.Fprintf(os.Stderr, "search: %v\n", err)
return 1
}
if opt.Hop > 0 {
if err := attachHops(hits, opt.Hop); err != nil {
fmt.Fprintf(os.Stderr, "hop: %v\n", err)
return 1
}
}
results := hits
for i := range results {
@@ -108,6 +114,44 @@ func searchHits(query, root, repo string, limit int) ([]Hit, error) {
return rank.RankAndFilter(fts, vec, root, repo, limit), nil
}
func attachHops(hits []Hit, n int) error {
if conn == nil {
return fmt.Errorf("brain not open")
}
for i := range hits {
var hops []rank.HopNode
for d := 1; d <= n; d++ {
stmt, err := conn.Prepare(rank.HopStmt(d))
if err != nil {
return err
}
res, err := conn.Execute(stmt, map[string]any{"id": hits[i].ID})
stmt.Close()
if err != nil {
return err
}
for res.HasNext() {
row, err := res.Next()
if err != nil {
return err
}
vals, err := row.GetAsSlice()
if err != nil || len(vals) < 3 {
continue
}
hops = append(hops, rank.HopNode{
ID: fmt.Sprint(vals[0]),
Label: rank.HopLabel(d),
Name: fmt.Sprint(vals[1]),
Depth: int(asInt(vals[2])),
})
}
}
hits[i].Hops = hops
}
return nil
}
func b2i(err error) int {
if err != nil {
return 1
@@ -183,11 +227,12 @@ type jsonOut struct {
}
type jsonHit struct {
ID string `json:"id"`
Text string `json:"text"`
Root string `json:"root"`
Score float64 `json:"score"`
Snippet string `json:"snippet,omitempty"`
ID string `json:"id"`
Text string `json:"text"`
Root string `json:"root"`
Score float64 `json:"score"`
Snippet string `json:"snippet,omitempty"`
Hops []rank.HopNode `json:"hops,omitempty"`
}
func toJSONOut(hits []Hit, query, rootFilter string, web *rank.SecondSource) *jsonOut {
@@ -199,6 +244,7 @@ func toJSONOut(hits []Hit, query, rootFilter string, web *rank.SecondSource) *js
Root: h.Root,
Score: h.Score,
Snippet: h.Snippet,
Hops: h.Hops,
}
}
return &jsonOut{
@@ -222,6 +268,18 @@ func resultsToDicts(hits []Hit) []any {
if h.Snippet != "" {
d = append(d, KV{"snippet", h.Snippet})
}
if len(h.Hops) > 0 {
nodes := make([]any, len(h.Hops))
for j, n := range h.Hops {
nodes[j] = Dict{
{"id", n.ID},
{"label", n.Label},
{"name", n.Name},
{"depth", n.Depth},
}
}
d = append(d, KV{"hops", nodes})
}
out[i] = d
}
return out