Files
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

228 lines
6.4 KiB
Go
Raw Permalink 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 search
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync/atomic"
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
"github.com/blevesearch/bleve/v2/mapping"
// Register default KV store.
_ "github.com/blevesearch/bleve/v2/index/upsidedown/store/gtreap"
)
// BleveBackend wraps Bleve for full-text search over code symbols.
// Better for large repos (50k+ symbols) and multi-repo mode.
type BleveBackend struct {
index bleve.Index
count atomic.Int64
diskPath string // non-empty when the index is disk-backed (scorch)
}
// DiskPath returns the directory containing the on-disk index, or ""
// when the backend is running fully in memory.
func (b *BleveBackend) DiskPath() string { return b.diskPath }
// DiskBytes walks the index directory and sums file sizes. Zero when
// in-memory. Called at most once per `gortex daemon status` invocation.
func (b *BleveBackend) DiskBytes() uint64 {
if b.diskPath == "" {
return 0
}
var total uint64
_ = filepath.Walk(b.diskPath, func(_ string, info os.FileInfo, err error) error {
if err != nil || info == nil || info.IsDir() {
return nil
}
total += uint64(info.Size())
return nil
})
return total
}
// symbolDoc is the document structure indexed by Bleve.
type symbolDoc struct {
Name string `json:"name"`
Path string `json:"path"`
Signature string `json:"signature"`
// Combined field for broader matching.
All string `json:"all"`
}
// NewBleve creates a Bleve-backed search index (in-memory via
// upsidedown + gtreap). Heavy but self-contained — no disk writes.
func NewBleve() (*BleveBackend, error) {
indexMapping := buildMapping()
idx, err := bleve.NewMemOnly(indexMapping)
if err != nil {
return nil, err
}
return &BleveBackend{index: idx}, nil
}
// NewBleveDisk creates a disk-backed Bleve index under dir using the
// scorch storage engine. The dir is created if missing; any existing
// index at the same path is removed first because we always rebuild
// from scratch (we don't have incremental-write semantics yet).
// Scorch is ~10-20× more memory-efficient than the in-memory
// upsidedown+gtreap store at the cost of disk IO on write, which only
// matters during initial index construction.
func NewBleveDisk(dir string) (*BleveBackend, error) {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("bleve disk dir: %w", err)
}
// Use a fixed child name so a caller passing a shared parent dir
// doesn't overwrite neighbouring state.
indexPath := filepath.Join(dir, "bleve.scorch")
if _, err := os.Stat(indexPath); err == nil {
if rmErr := os.RemoveAll(indexPath); rmErr != nil {
return nil, fmt.Errorf("clearing old bleve index at %s: %w", indexPath, rmErr)
}
}
indexMapping := buildMapping()
idx, err := bleve.New(indexPath, indexMapping)
if err != nil {
return nil, fmt.Errorf("opening bleve disk index at %s: %w", indexPath, err)
}
return &BleveBackend{index: idx, diskPath: indexPath}, nil
}
func buildMapping() *mapping.IndexMappingImpl {
indexMapping := bleve.NewIndexMapping()
// Custom analyzer: unicode tokenizer + lowercase.
// We pre-tokenize camelCase in Add(), so the analyzer just needs
// to handle the space-separated tokens we give it.
err := indexMapping.AddCustomAnalyzer("code", map[string]any{
"type": custom.Name,
"tokenizer": unicode.Name,
"token_filters": []string{
lowercase.Name,
},
})
if err != nil {
// Fallback to default analyzer.
return indexMapping
}
// Document mapping.
docMapping := bleve.NewDocumentMapping()
nameField := bleve.NewTextFieldMapping()
nameField.Analyzer = "code"
nameField.Store = false
docMapping.AddFieldMappingsAt("name", nameField)
pathField := bleve.NewTextFieldMapping()
pathField.Analyzer = "code"
pathField.Store = false
docMapping.AddFieldMappingsAt("path", pathField)
sigField := bleve.NewTextFieldMapping()
sigField.Analyzer = "code"
sigField.Store = false
docMapping.AddFieldMappingsAt("signature", sigField)
allField := bleve.NewTextFieldMapping()
allField.Analyzer = "code"
allField.Store = false
docMapping.AddFieldMappingsAt("all", allField)
indexMapping.DefaultMapping = docMapping
indexMapping.DefaultAnalyzer = "code"
return indexMapping
}
func (b *BleveBackend) Add(id string, fields ...string) {
// Pre-tokenize camelCase and rejoin with spaces so Bleve's
// unicode tokenizer can split them.
var parts []string
for _, f := range fields {
tokens := NormalizeFTSTokens(Tokenize(f))
parts = append(parts, strings.Join(tokens, " "))
}
doc := symbolDoc{
All: strings.Join(parts, " "),
}
if len(parts) > 0 {
doc.Name = parts[0]
}
if len(parts) > 1 {
doc.Path = parts[1]
}
if len(parts) > 2 {
doc.Signature = parts[2]
}
if err := b.index.Index(id, doc); err == nil {
b.count.Add(1)
}
}
func (b *BleveBackend) Remove(id string) {
if err := b.index.Delete(id); err == nil {
b.count.Add(-1)
}
}
func (b *BleveBackend) Search(query string, limit int) []SearchResult {
// Pre-tokenize the query for camelCase splitting.
tokens := NormalizeFTSTokens(TokenizeQuery(query))
if len(tokens) == 0 {
return nil
}
q := strings.Join(tokens, " ")
searchReq := bleve.NewSearchRequest(bleve.NewQueryStringQuery(q))
searchReq.Size = limit
res, err := b.index.Search(searchReq)
if err != nil || res.Total == 0 {
return nil
}
out := make([]SearchResult, 0, len(res.Hits))
for _, hit := range res.Hits {
out = append(out, SearchResult{
ID: hit.ID,
Score: hit.Score,
})
}
return out
}
func (b *BleveBackend) Count() int {
return int(b.count.Load())
}
// SizeBytes approximates Bleve's in-memory footprint. Bleve (with the
// default upsidedown + gtreap KV store) is much hungrier than the
// fields-and-postings accounting would suggest — gtreap's immutable
// persistent trees retain copy-on-write versions, and the upsidedown
// row layout expands every symbol into many keys. Calibrated against
// heap profiles on a ~65k-symbol index: live Bleve heap was ~2.0 GiB,
// i.e. ~32 KiB per document. Earlier estimates of ~2 KiB/doc were off
// by 16× and were the root cause of the large "other" bucket users
// saw in `gortex daemon status`.
func (b *BleveBackend) SizeBytes() uint64 {
return uint64(b.count.Load()) * 32768
}
func (b *BleveBackend) Close() {
if b.index != nil {
_ = b.index.Close()
}
}