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

204 lines
7.9 KiB
Go

// Package embedding provides pluggable embedding providers for semantic search.
//
// The default build includes the Hugot provider (pure-Go ONNX runtime via
// hugot.NewGoSession) which auto-downloads MiniLM-L6-v2 on first use — no
// external runtime, no manual model placement. The legacy StaticProvider
// (GloVe word vectors) and APIProvider (Ollama/OpenAI) are also always
// available.
//
// Opt-in build tags enable faster transformer backends for users who are
// willing to manage native dependencies:
// - embeddings_onnx — yalue/onnxruntime_go with libonnxruntime on PATH
// - embeddings_gomlx — hugot with XLA/PJRT plugin (~100MB auto-download)
package embedding
import (
"context"
"errors"
"fmt"
)
// ErrBackendNotCompiled marks a local-backend factory that failed only because
// its build tag was not set (the onnx/gomlx stubs). Such a failure is benign
// noise in a default build — callers use errors.Is to log it at debug rather
// than warn even when the chain degrades to the static fallback.
var ErrBackendNotCompiled = errors.New("embedding backend not compiled in")
// Provider generates embedding vectors from text.
type Provider interface {
// Embed returns the embedding vector for the given text.
Embed(ctx context.Context, text string) ([]float32, error)
// EmbedBatch returns embeddings for multiple texts.
EmbedBatch(ctx context.Context, texts []string) ([][]float32, error)
// Dimensions returns the embedding vector size.
Dimensions() int
// Close releases resources.
Close() error
}
// NewHugotProvider exposes the pure-Go Hugot backend (MiniLM-L6-v2)
// directly, without the NewLocalProvider fallback chain. Useful when a
// caller wants a hard error if Hugot can't start (e.g. eval harnesses
// that mustn't silently degrade to static GloVe).
func NewHugotProvider() (Provider, error) { return newHugotProvider() }
// NewHugotProviderWithVariant loads a specific embedder variant from
// any registered HuggingFace repo (MiniLM variants, code-tuned models,
// …). Pass a name returned by KnownHugotVariants (e.g. "fp32",
// "qint8_arm64", "jina_code", "bge_code"). Returns an error if the
// variant name is unknown or the download/load fails.
func NewHugotProviderWithVariant(variant string) (Provider, error) {
v, ok := LookupHugotVariant(variant)
if !ok {
return nil, fmt.Errorf("unknown hugot variant %q (known: %v)", variant, KnownHugotVariants())
}
return newHugotProviderWithSpec(v)
}
// ProviderConfig is the subset of an embedding configuration that
// NewProviderFromConfig needs. It is a local struct — not the
// config.EmbeddingConfig type — so the embedding package stays free of
// an import dependency on internal/config. Callers translate their
// config block into this shape.
type ProviderConfig struct {
// Provider selects the backend: "static" (baked GloVe, the
// default), "local" (best available transformer), or "api" (an
// external embedding endpoint). Empty is treated as "static".
Provider string
// APIURL / APIModel parameterise the "api" provider.
APIURL string
APIModel string
// Variant names a specific local transformer model to load (a key
// from KnownHugotVariants, e.g. "fp32", "bge_small", "jina_code").
// Honoured only when Provider is "local": a non-empty Variant pins
// that exact model via NewHugotProviderWithVariant instead of the
// auto-selected NewLocalProvider backend. Empty preserves the
// existing default-selection behaviour. Ignored for other providers.
Variant string
}
// NewProviderFromConfig constructs an embedding provider from a
// configuration block. The selection logic:
//
// - "static" (or empty) → NewStaticProvider — baked GloVe word
// vectors, zero download, CPU-only. This is the default because
// it makes semantic search work with no setup.
// - "local" → NewLocalProvider — the best available
// transformer backend (Hugot MiniLM auto-downloads on first use).
// When cfg.Variant names a specific model, that exact variant is
// loaded via NewHugotProviderWithVariant instead.
// - "api" → NewAPIProvider against cfg.APIURL.
//
// An unknown provider name is an error so a typo in `.gortex.yaml`
// fails loudly instead of silently degrading.
func NewProviderFromConfig(cfg ProviderConfig) (Provider, error) {
switch cfg.Provider {
case "", "static":
return NewStaticProvider()
case "local":
// A pinned variant loads that exact model; an empty variant
// keeps the existing auto-selection (ONNX → GoMLX → Hugot →
// static) so every prior config behaves identically.
if cfg.Variant != "" {
return NewHugotProviderWithVariant(cfg.Variant)
}
return NewLocalProvider()
case "api":
if cfg.APIURL == "" {
return nil, fmt.Errorf("embedding provider %q requires an api_url", cfg.Provider)
}
return NewAPIProvider(cfg.APIURL, cfg.APIModel), nil
default:
return nil, fmt.Errorf("unknown embedding provider %q (want static, local, or api)", cfg.Provider)
}
}
// NewProviderFromConfigWithReport is NewProviderFromConfig plus a SelectionReport
// for the auto-selected local backend (Provider "local" with no pinned Variant),
// so the caller can log which backend was constructed and which were skipped.
// For every other provider the report is empty.
func NewProviderFromConfigWithReport(cfg ProviderConfig) (Provider, SelectionReport, error) {
if cfg.Provider == "local" && cfg.Variant == "" {
p, report := NewLocalProviderWithReport()
if p == nil {
err := fmt.Errorf("no embedding provider available")
if n := len(report.Attempts); n > 0 {
err = report.Attempts[n-1].Err
}
return nil, report, err
}
return p, report, nil
}
p, err := NewProviderFromConfig(cfg)
return p, SelectionReport{}, err
}
// SelectionAttempt records one backend the local-provider chain tried and the
// error that made it fall through. It exists so a silent degradation to the
// static GloVe fallback becomes observable to the caller.
type SelectionAttempt struct {
Backend string
Err error
}
// SelectionReport describes how NewLocalProviderWithReport chose a backend: the
// backend actually constructed, its dimension, and every rejected attempt.
// Chosen is the backend name (e.g. "hugot", "static"); it is "static" when the
// chain fell all the way through to the GloVe fallback.
type SelectionReport struct {
Chosen string
Dims int
Attempts []SelectionAttempt
}
// NewLocalProviderWithReport returns the best available local embedding provider
// along with a report of every backend it tried. Preference order: ONNX
// (fastest, requires libonnxruntime) → GoMLX (XLA) → Hugot (pure Go, always
// compiled in) → Static (GloVe word vectors fallback). A nil provider means even
// the static fallback failed to construct (its error is the last attempt).
func NewLocalProviderWithReport() (Provider, SelectionReport) {
factories := []struct {
name string
factory func() (Provider, error)
}{
{"onnx", newONNXProvider},
{"gomlx", newGoMLXProvider},
{"hugot", newHugotProvider},
}
var report SelectionReport
for _, nf := range factories {
p, err := nf.factory()
if err == nil {
report.Chosen = nf.name
report.Dims = p.Dimensions()
return p, report
}
report.Attempts = append(report.Attempts, SelectionAttempt{Backend: nf.name, Err: err})
}
// Fallback: static word vectors (always available, no network).
p, err := NewStaticProvider()
if err != nil {
report.Attempts = append(report.Attempts, SelectionAttempt{Backend: "static", Err: err})
return nil, report
}
report.Chosen = "static"
report.Dims = p.Dimensions()
return p, report
}
// NewLocalProvider returns the best available local embedding provider,
// discarding the selection report. See NewLocalProviderWithReport.
func NewLocalProvider() (Provider, error) {
p, report := NewLocalProviderWithReport()
if p == nil {
if n := len(report.Attempts); n > 0 {
return nil, report.Attempts[n-1].Err
}
return nil, fmt.Errorf("no embedding provider available")
}
return p, nil
}