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
200 lines
4.2 KiB
Go
200 lines
4.2 KiB
Go
package embedding
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// StaticProvider computes embeddings by averaging pre-trained word vectors.
|
|
// It provides basic semantic search — understands "validate" ≈ "check" but
|
|
// has no contextual understanding. Always available, zero external dependencies.
|
|
type StaticProvider struct {
|
|
vectors map[string][]float32
|
|
dims int
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewStaticProvider creates a provider using built-in word vectors.
|
|
func NewStaticProvider() (*StaticProvider, error) {
|
|
p := &StaticProvider{
|
|
vectors: make(map[string][]float32),
|
|
dims: 50, // default GloVe 50d; overridden by loadVectors
|
|
}
|
|
if err := p.loadVectors(); err != nil {
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (p *StaticProvider) Embed(_ context.Context, text string) ([]float32, error) {
|
|
tokens := tokenizeForEmbedding(text)
|
|
return p.averageVectors(tokens), nil
|
|
}
|
|
|
|
func (p *StaticProvider) EmbedBatch(_ context.Context, texts []string) ([][]float32, error) {
|
|
results := make([][]float32, len(texts))
|
|
for i, text := range texts {
|
|
tokens := tokenizeForEmbedding(text)
|
|
results[i] = p.averageVectors(tokens)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func (p *StaticProvider) Dimensions() int { return p.dims }
|
|
func (p *StaticProvider) Close() error { return nil }
|
|
|
|
func (p *StaticProvider) averageVectors(tokens []string) []float32 {
|
|
result := make([]float32, p.dims)
|
|
count := 0
|
|
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
|
|
for _, tok := range tokens {
|
|
vec, ok := p.vectors[tok]
|
|
if !ok {
|
|
continue
|
|
}
|
|
for i, v := range vec {
|
|
result[i] += v
|
|
}
|
|
count++
|
|
}
|
|
|
|
if count == 0 {
|
|
return result // zero vector
|
|
}
|
|
|
|
// Average and normalize.
|
|
for i := range result {
|
|
result[i] /= float32(count)
|
|
}
|
|
return normalize(result)
|
|
}
|
|
|
|
func normalize(v []float32) []float32 {
|
|
var norm float64
|
|
for _, x := range v {
|
|
norm += float64(x) * float64(x)
|
|
}
|
|
norm = math.Sqrt(norm)
|
|
if norm < 1e-10 {
|
|
return v
|
|
}
|
|
for i := range v {
|
|
v[i] /= float32(norm)
|
|
}
|
|
return v
|
|
}
|
|
|
|
// tokenizeForEmbedding splits text into lowercase tokens suitable for
|
|
// word vector lookup. Splits on camelCase, underscores, dots, slashes.
|
|
func tokenizeForEmbedding(text string) []string {
|
|
var tokens []string
|
|
var current strings.Builder
|
|
|
|
flush := func() {
|
|
if current.Len() >= 2 {
|
|
tokens = append(tokens, current.String())
|
|
}
|
|
current.Reset()
|
|
}
|
|
|
|
prev := rune(0)
|
|
for _, r := range text {
|
|
switch {
|
|
case r >= 'A' && r <= 'Z':
|
|
// camelCase boundary: flush before uppercase
|
|
if prev >= 'a' && prev <= 'z' {
|
|
flush()
|
|
}
|
|
current.WriteRune(r + 32) // toLower
|
|
case r >= 'a' && r <= 'z':
|
|
current.WriteRune(r)
|
|
case r >= '0' && r <= '9':
|
|
current.WriteRune(r)
|
|
default:
|
|
flush()
|
|
}
|
|
prev = r
|
|
}
|
|
flush()
|
|
|
|
return tokens
|
|
}
|
|
|
|
// loadVectors loads GloVe word vectors from the embedded data file.
|
|
func (p *StaticProvider) loadVectors() error {
|
|
if len(vectorData) == 0 {
|
|
return nil // no embedded vectors, empty vocabulary
|
|
}
|
|
|
|
gz, err := gzip.NewReader(bytes.NewReader(vectorData))
|
|
if err != nil {
|
|
return fmt.Errorf("decompress vectors: %w", err)
|
|
}
|
|
defer gz.Close()
|
|
|
|
data, err := io.ReadAll(gz)
|
|
if err != nil {
|
|
return fmt.Errorf("read vectors: %w", err)
|
|
}
|
|
|
|
if len(data) < 8 {
|
|
return fmt.Errorf("vector data too short")
|
|
}
|
|
|
|
wordCount := binary.LittleEndian.Uint32(data[0:4])
|
|
dims := binary.LittleEndian.Uint32(data[4:8])
|
|
p.dims = int(dims)
|
|
|
|
offset := 8
|
|
for i := uint32(0); i < wordCount; i++ {
|
|
if offset+2 > len(data) {
|
|
break
|
|
}
|
|
wordLen := int(binary.LittleEndian.Uint16(data[offset : offset+2]))
|
|
offset += 2
|
|
|
|
if offset+wordLen > len(data) {
|
|
break
|
|
}
|
|
word := string(data[offset : offset+wordLen])
|
|
offset += wordLen
|
|
|
|
vecBytes := int(dims) * 4
|
|
if offset+vecBytes > len(data) {
|
|
break
|
|
}
|
|
vec := make([]float32, dims)
|
|
for j := range vec {
|
|
vec[j] = math.Float32frombits(binary.LittleEndian.Uint32(data[offset+j*4 : offset+j*4+4]))
|
|
}
|
|
offset += vecBytes
|
|
|
|
p.vectors[word] = vec
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetVectors allows injecting word vectors for testing.
|
|
func (p *StaticProvider) SetVectors(vecs map[string][]float32) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.vectors = vecs
|
|
if len(vecs) > 0 {
|
|
for _, v := range vecs {
|
|
p.dims = len(v)
|
|
break
|
|
}
|
|
}
|
|
}
|