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

159 lines
5.0 KiB
Go

package search
import "sync"
// Swappable wraps a Backend and lets a single in-place swap be performed
// concurrently with reads. Used by the indexer to upgrade from the
// in-memory BM25 backend to Bleve once the corpus crosses AutoThreshold,
// without making every call site re-thread a new Backend reference and
// without holding the indexer's lock during the (potentially seconds-long)
// re-population of Bleve.
//
// Callers see a stable *Swappable; reads delegate to whichever inner
// backend is currently active. Swap atomically replaces the inner
// backend and closes the previous one.
type Swappable struct {
mu sync.RWMutex
inner Backend
}
// NewSwappable wraps b. Panics if b is nil — every Indexer must start
// with a real backend, even if it's the in-memory NewAuto() default.
func NewSwappable(b Backend) *Swappable {
if b == nil {
panic("search.NewSwappable: nil backend")
}
return &Swappable{inner: b}
}
// Swap installs the new backend and closes the old one. Safe to call
// concurrently with reads; the swap itself is brief (one pointer write
// under the write lock) so reads queued during the swap return promptly
// against the new backend.
func (s *Swappable) Swap(b Backend) {
s.mu.Lock()
old := s.inner
s.inner = b
s.mu.Unlock()
if old != nil && old != b {
old.Close()
}
}
// Inner returns the currently-active backend. Used internally to test
// upgrade outcomes; production code should always go through the
// Backend interface methods on Swappable itself.
func (s *Swappable) Inner() Backend {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner
}
// --- Backend interface ------------------------------------------------
func (s *Swappable) Add(id string, fields ...string) {
s.mu.RLock()
defer s.mu.RUnlock()
s.inner.Add(id, fields...)
}
func (s *Swappable) Remove(id string) {
s.mu.RLock()
defer s.mu.RUnlock()
s.inner.Remove(id)
}
func (s *Swappable) Search(query string, limit int) []SearchResult {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.Search(query, limit)
}
// SearchChannels delegates to the inner backend when it implements
// ChannelSearcher, so a HybridBackend wrapped in a Swappable still
// exposes per-channel rank data to the rerank pipeline.
func (s *Swappable) SearchChannels(query string, limit int) (textResults []SearchResult, vectorIDs []string) {
s.mu.RLock()
defer s.mu.RUnlock()
if cs, ok := s.inner.(ChannelSearcher); ok {
return cs.SearchChannels(query, limit)
}
return s.inner.Search(query, limit), nil
}
// SearchChannelsTimed delegates to a backend that supports the
// per-phase timing breakdown (today only HybridBackend). Falls back
// to SearchChannels — and a zero-valued ChannelTimings — when the
// inner backend doesn't know how to split phases.
func (s *Swappable) SearchChannelsTimed(query string, limit int) ([]SearchResult, []string, ChannelTimings) {
s.mu.RLock()
defer s.mu.RUnlock()
type timer interface {
SearchChannelsTimed(query string, limit int) ([]SearchResult, []string, ChannelTimings)
}
if cst, ok := s.inner.(timer); ok {
return cst.SearchChannelsTimed(query, limit)
}
if cs, ok := s.inner.(ChannelSearcher); ok {
text, vec := cs.SearchChannels(query, limit)
return text, vec, ChannelTimings{}
}
return s.inner.Search(query, limit), nil, ChannelTimings{}
}
// SearchSymbolBundles forwards to the inner backend when it implements
// SymbolBundleSearcherBackend (production wiring: a
// SymbolSearcherBackend whose store is the disk Store, or a
// HybridBackend whose text backend is the same). Returns nil when the
// inner backend doesn't expose bundles — the engine treats nil as
// "no bundle support" and falls back to the per-call Search +
// GetNodesByIDs + GetIn/OutEdgesByNodeIDs path.
func (s *Swappable) SearchSymbolBundles(query string, limit int) []SymbolBundle {
s.mu.RLock()
defer s.mu.RUnlock()
if bs, ok := s.inner.(SymbolBundleSearcherBackend); ok {
return bs.SearchSymbolBundles(query, limit)
}
return nil
}
// VectorChannelOnly forwards to the inner backend when it implements
// the vector-only channel pull (today: HybridBackend). Lets the
// engine fetch the vector channel without re-running text BM25 —
// the bundle path already has the text hits. Returns (nil, zero
// timings) when the inner backend isn't vector-aware.
func (s *Swappable) VectorChannelOnly(query string, limit int) ([]string, ChannelTimings) {
s.mu.RLock()
defer s.mu.RUnlock()
type vco interface {
VectorChannelOnly(query string, limit int) ([]string, ChannelTimings)
}
if v, ok := s.inner.(vco); ok {
return v.VectorChannelOnly(query, limit)
}
return nil, ChannelTimings{}
}
func (s *Swappable) Count() int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.inner.Count()
}
func (s *Swappable) Close() {
s.mu.Lock()
defer s.mu.Unlock()
if s.inner != nil {
s.inner.Close()
s.inner = nil
}
}
// SizeBytes delegates to the inner backend's SizeBytes implementation
// if it provides one; otherwise zero.
func (s *Swappable) SizeBytes() uint64 {
s.mu.RLock()
defer s.mu.RUnlock()
return BackendSize(s.inner)
}