mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Adds an in-memory trigram-indexed file finder package at `agent/filefinder`, designed to power a future `FindFiles` HTTP handler on the WorkspaceAgent. ## What it does Fast fuzzy file search with VS Code-quality matching across millions of files. Sub-millisecond search latency at 100K files. ## Architecture - **Index**: append-only docs slice with trigram + prefix posting lists - **Snapshot**: lock-free reader view via frozen slice headers + shallow-copied deleted set - **Search pipeline**: trigram intersection → fuzzy fallback (prefix bucket + subsequence) → brute-force scan (capped at 5K docs) - **Scoring**: subsequence match, basename prefix, boundary hits, contiguous runs, depth/length penalties - **Engine**: multi-root with fsnotify watcher (50ms batch coalescing), atomic snapshot publishing ## Benchmarks (10K files) | Query Type | Latency | |---|---| | exact_basename (`handler.go`) | ~43µs | | short_query (`ha`) | ~7µs | | fuzzy_basename (`hndlr`) | ~50µs | | path_structured (`internal/handler`) | ~29µs | | multi_token (`api handler`) | ~15µs | ## File inventory (11 files, 3273 lines) | File | Lines | Purpose | |---|---|---| | `text.go` | 264 | Normalization, trigram extraction, scoring | | `delta.go` | 128 | Index, Snapshot, CRUD operations | | `query.go` | 272 | Query planning, search strategies, top-K merge | | `engine.go` | 323 | Multi-root engine, watcher integration | | `watcher_fs.go` | 201 | fsnotify wrapper with batch coalescing | | `*_test.go` | 2085 | Unit tests, integration tests, benchmarks | --------- Co-authored-by: Coder <coder@users.noreply.github.com>
126 lines
3.1 KiB
Go
126 lines
3.1 KiB
Go
package filefinder
|
|
|
|
import "strings"
|
|
|
|
// FileFlag represents the type of filesystem entry.
|
|
type FileFlag uint16
|
|
|
|
const (
|
|
FlagFile FileFlag = 0
|
|
FlagDir FileFlag = 1
|
|
FlagSymlink FileFlag = 2
|
|
)
|
|
|
|
type doc struct {
|
|
path string
|
|
baseOff int
|
|
baseLen int
|
|
depth int
|
|
flags uint16
|
|
}
|
|
|
|
// Index is an append-only in-memory file index with snapshot support.
|
|
type Index struct {
|
|
docs []doc
|
|
byGram map[uint32][]uint32
|
|
byPrefix1 [256][]uint32
|
|
byPrefix2 map[uint16][]uint32
|
|
byPath map[string]uint32
|
|
deleted map[uint32]bool
|
|
}
|
|
|
|
// Snapshot is a frozen, read-only view of the index at a point in time.
|
|
type Snapshot struct {
|
|
docs []doc
|
|
deleted map[uint32]bool
|
|
byGram map[uint32][]uint32
|
|
byPrefix1 [256][]uint32
|
|
byPrefix2 map[uint16][]uint32
|
|
}
|
|
|
|
// NewIndex creates an empty Index.
|
|
func NewIndex() *Index {
|
|
return &Index{
|
|
byGram: make(map[uint32][]uint32),
|
|
byPrefix2: make(map[uint16][]uint32),
|
|
byPath: make(map[string]uint32),
|
|
deleted: make(map[uint32]bool),
|
|
}
|
|
}
|
|
|
|
// Add inserts a path into the index, tombstoning any previous entry.
|
|
func (idx *Index) Add(path string, flags uint16) uint32 {
|
|
norm := string(normalizePathBytes([]byte(path)))
|
|
if oldID, ok := idx.byPath[norm]; ok {
|
|
idx.deleted[oldID] = true
|
|
}
|
|
id := uint32(len(idx.docs)) //nolint:gosec // Index will never exceed 2^32 docs.
|
|
baseOff, baseLen := extractBasename([]byte(norm))
|
|
idx.docs = append(idx.docs, doc{
|
|
path: norm, baseOff: baseOff, baseLen: baseLen,
|
|
depth: strings.Count(norm, "/"), flags: flags,
|
|
})
|
|
idx.byPath[norm] = id
|
|
for _, g := range extractTrigrams([]byte(norm)) {
|
|
idx.byGram[g] = append(idx.byGram[g], id)
|
|
}
|
|
if baseLen > 0 {
|
|
basename := []byte(norm[baseOff : baseOff+baseLen])
|
|
p1 := prefix1(basename)
|
|
idx.byPrefix1[p1] = append(idx.byPrefix1[p1], id)
|
|
p2 := prefix2(basename)
|
|
idx.byPrefix2[p2] = append(idx.byPrefix2[p2], id)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// Remove marks the entry for path as deleted.
|
|
func (idx *Index) Remove(path string) bool {
|
|
norm := string(normalizePathBytes([]byte(path)))
|
|
id, ok := idx.byPath[norm]
|
|
if !ok {
|
|
return false
|
|
}
|
|
idx.deleted[id] = true
|
|
delete(idx.byPath, norm)
|
|
return true
|
|
}
|
|
|
|
// Has reports whether path exists (not deleted) in the index.
|
|
func (idx *Index) Has(path string) bool {
|
|
_, ok := idx.byPath[string(normalizePathBytes([]byte(path)))]
|
|
return ok
|
|
}
|
|
|
|
// Len returns the number of live (non-deleted) documents.
|
|
func (idx *Index) Len() int { return len(idx.byPath) }
|
|
|
|
func copyPostings[K comparable](m map[K][]uint32) map[K][]uint32 {
|
|
cp := make(map[K][]uint32, len(m))
|
|
for k, v := range m {
|
|
cp[k] = v[:len(v):len(v)]
|
|
}
|
|
return cp
|
|
}
|
|
|
|
// Snapshot returns a frozen read-only view of the index.
|
|
func (idx *Index) Snapshot() *Snapshot {
|
|
del := make(map[uint32]bool, len(idx.deleted))
|
|
for id := range idx.deleted {
|
|
del[id] = true
|
|
}
|
|
var p1Copy [256][]uint32
|
|
for i, ids := range idx.byPrefix1 {
|
|
if len(ids) > 0 {
|
|
p1Copy[i] = ids[:len(ids):len(ids)]
|
|
}
|
|
}
|
|
return &Snapshot{
|
|
docs: idx.docs[:len(idx.docs):len(idx.docs)],
|
|
deleted: del,
|
|
byGram: copyPostings(idx.byGram),
|
|
byPrefix1: p1Copy,
|
|
byPrefix2: copyPostings(idx.byPrefix2),
|
|
}
|
|
}
|