项目文件夹

文件
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

166 行
5.0 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package indexer
import (
"unicode/utf8"
"go.uber.org/zap"
"github.com/zzet/gortex/internal/graph"
)
// contentSnippetCap bounds how much of a CONTENT (data_class="content")
// section body is retained on the graph node after its full text has been
// streamed into the dedicated content index. The full body lives in the
// content FTS; the node keeps only this much for display (the why-layer,
// get_symbol_source) — so a repo of hundreds of thousands of sections
// holds ~240 B × N in the graph instead of ~4 KB × N (~17× less text).
const contentSnippetCap = 240
// isContentNode is the indexer-local alias for graph.IsContentNode — the
// shared predicate for a CONTENT section node (KindDoc, data_class=content).
func isContentNode(n *graph.Node) bool {
return graph.IsContentNode(n)
}
// contentBody returns the full section text carried on a content node, or
// "" if absent.
func contentBody(n *graph.Node) string {
if n.Meta == nil {
return ""
}
b, _ := n.Meta["section_text"].(string)
return b
}
// metaInt reads an integer-valued Meta key, tolerating the int / int64 /
// float64 forms a value can take across a gob round-trip.
func metaInt(meta map[string]any, key string) int {
switch v := meta[key].(type) {
case int:
return v
case int64:
return int(v)
case float64:
return int(v)
default:
return 0
}
}
// contentOrdinal returns the section's ordinal — "ordinal" for text /
// office chunks, "page" for PDF pages.
func contentOrdinal(n *graph.Node) int {
if o := metaInt(n.Meta, "ordinal"); o != 0 {
return o
}
return metaInt(n.Meta, "page")
}
// safeTruncate returns s clamped to at most maxBytes, never splitting a
// multi-byte rune.
func safeTruncate(s string, maxBytes int) string {
if len(s) <= maxBytes {
return s
}
cut := maxBytes
for cut > 0 && !utf8.RuneStart(s[cut]) {
cut--
}
return s[:cut]
}
// leanContentNode strips a content node's full section body down to a
// capped snippet once the full text has been captured for the content
// index. The key stays "section_text" so display consumers keep working;
// only the length changes. "content_indexed" marks that the full body
// lives in the content index (so a reader knows the snippet is partial).
func leanContentNode(n *graph.Node) {
body := contentBody(n)
if len(body) <= contentSnippetCap {
return
}
n.Meta["section_text"] = safeTruncate(body, contentSnippetCap)
n.Meta["content_indexed"] = true
}
// collectContentItems pulls one ContentFTSItem per content section node in
// the batch, carrying the FULL body for the content index. Returns nil
// when the batch has no content.
func collectContentItems(nodes []*graph.Node) []graph.ContentFTSItem {
var items []graph.ContentFTSItem
for _, n := range nodes {
if !isContentNode(n) {
continue
}
body := contentBody(n)
if body == "" {
continue
}
items = append(items, graph.ContentFTSItem{
NodeID: n.ID,
FilePath: n.FilePath,
Ordinal: contentOrdinal(n),
Body: body,
})
}
return items
}
// firstContentFilePath returns the FilePath of the first content node in
// the batch (all of one file's content nodes share it), or "" if none. The
// incremental reindex path uses it to wipe a single file's content rows
// before re-streaming.
func firstContentFilePath(nodes []*graph.Node) string {
for _, n := range nodes {
if isContentNode(n) {
return n.FilePath
}
}
return ""
}
// contentSearcher returns the ContentSearcher this index writes content
// section bodies to: the disk sink captured at the shadow swap (so content
// reaches disk even while idx.graph is the in-memory shadow), else
// idx.graph itself when it is a disk store. Returns nil for an in-memory
// store with no content index — in which case content text is left on the
// nodes and falls back to the symbol search (the small-repo / CLI case).
func (idx *Indexer) contentSearcher() graph.ContentSearcher {
if idx.contentSink != nil {
return idx.contentSink
}
if cs, ok := idx.graph.(graph.ContentSearcher); ok {
return cs
}
return nil
}
// streamContentSections is the per-file content path: it streams a parsed
// file's content section bodies into the dedicated content index, then
// leans the nodes to a snippet — so the bulk text never enters the graph,
// the symbol search, or the materialising code passes. Called in the parse
// worker before AddBatch, so the nodes added to the graph are already
// lean. A nil content searcher (in-memory store) leaves the nodes full.
func (idx *Indexer) streamContentSections(nodes []*graph.Node) {
cs := idx.contentSearcher()
if cs == nil {
return
}
items := collectContentItems(nodes)
if len(items) == 0 {
return
}
if err := cs.AppendContent(idx.RepoPrefix(), items); err != nil {
// Keep the full body on the nodes if the append failed, so content
// stays searchable via the symbol-search fallback rather than lost.
idx.logger.Warn("indexer: content index append failed; leaving section text on nodes",
zap.Error(err))
return
}
for _, n := range nodes {
if isContentNode(n) {
leanContentNode(n)
}
}
}