Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / internal/modules/matching/skillmatcher.go |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | internal/modules/matching/skillmatcher.go |
| Original SHA-256 | 846190180bf8293e714922a639fe441d3cec829fce9becb7c840ff7337337d2a |
| Git blob SHA | f80f1ae93599392d90389c99ea33b81b11483718 |
| Reading-copy SHA-256 | 846190180bf8293e714922a639fe441d3cec829fce9becb7c840ff7337337d2a |
Every recorded occurrence
These 2 records contain identical original source bytes. Their paths and revisions remain part of the archive.
| Original record | Branch or archive | Revision / identifier | Original location |
|---|---|---|---|
| Janady13/missionmatch / internal/modules/matching/skillmatcher.go | feature/opportunity-to-outcome | 720d47fae476b0d38477ae4401697d372b1ea2bc | Open original |
| Janady13/missionmatch / internal/modules/matching/skillmatcher.go | feature/aioe-upgrade | 2178e7298d0e5d9af83afa12402fd587f0ba6ed8 | Open original |
Recovered source transcript
Download complete source text ↓190 lines · 7.4 KB
package matching
import (
"context"
"sync"
"github.com/google/uuid"
"github.com/pgvector/pgvector-go"
"github.com/shopspring/decimal"
"github.com/missionworkforce/missionmatch/internal/modules/matching/engine"
"github.com/missionworkforce/missionmatch/internal/platform/embeddings"
"github.com/missionworkforce/missionmatch/internal/platform/tenant"
)
// EmbeddingSkillMatcher implements engine.SkillMatcher using the embedding provider: it embeds the
// required skill name and each candidate skill name and picks the best cosine match at/above the
// engine's threshold. Vectors from the provider are L2-normalized, so cosine == dot product.
//
// This is the semantic ("synonyms / adjacent skills") layer the engine defers to AFTER exact match.
// With the deterministic offline embedder, unrelated skills score near zero, so it never invents a
// false semantic match, exact matching still dominates. Determinism follows from the embedder's.
type EmbeddingSkillMatcher struct {
embedder embeddings.Embedder
}
// NewEmbeddingSkillMatcher wraps an embedder as an engine.SkillMatcher.
func NewEmbeddingSkillMatcher(e embeddings.Embedder) *EmbeddingSkillMatcher {
return &EmbeddingSkillMatcher{embedder: e}
}
// Match satisfies engine.SkillMatcher. It returns the candidate skill side and the similarity; the
// engine owns the required-side identity and the match flags. ctx is the request context, so an
// embedding provider call respects the request deadline and cancellation (it is never stored).
//
// The float dot product picks the single best candidate (ordering ties are astronomically unlikely
// and never change the met/unmet outcome), but the at or above threshold DECISION is made in decimal
// on the similarity rounded to a fixed scale, so the boundary outcome is reproducible across
// architectures, on the engine's determinism path.
func (m *EmbeddingSkillMatcher) Match(ctx context.Context, required engine.RequiredSkill, cands []engine.CandidateSkill, threshold decimal.Decimal) (engine.SkillMatch, bool) {
if m == nil || m.embedder == nil || len(cands) == 0 {
return engine.SkillMatch{}, false
}
rv, err := m.embedder.Embed(ctx, required.Name)
if err != nil {
return engine.SkillMatch{}, false
}
bestSim := -2.0
var best engine.CandidateSkill
found := false
for _, cs := range cands {
cv, err := m.embedder.Embed(ctx, cs.Name)
if err != nil || len(cv) != len(rv) {
continue
}
if sim := dot(rv, cv); sim > bestSim {
bestSim, best, found = sim, cs, true
}
}
if !found {
return engine.SkillMatch{}, false
}
sim := decimal.NewFromFloat(bestSim).Round(4)
if sim.LessThan(threshold) {
return engine.SkillMatch{}, false
}
return engine.SkillMatch{
CandidateSkillID: best.SkillID,
CandidateName: best.Name,
Similarity: sim,
Matched: true,
}, true
}
func dot(a, b []float32) float64 {
var s float64
for i := range a {
s += float64(a[i]) * float64(b[i])
}
return s
}
// StoredSkillMatcher is the LIVE semantic skill matcher. It satisfies engine.SkillMatcher using the
// BACKFILLED skill embeddings in the global skills table (the same vector(384) column the HNSW index
// covers), instead of embedding skill name strings at request time. Embedding name strings at request time
// was the dead live path: with the fake default embedder it produced fake similarities, and it ignored the
// populated column entirely. This matcher reads the stored vectors once into an in memory cache (the skills
// ontology is a small, global, bounded table) and resolves a required skill against a candidate's skills by
// cosine over the stored vectors.
//
// The matcher's job is known skill to known skill (the required skill and the candidate's skills are both
// ontology entries with ids), which is a direct cosine over two stored vectors, not a nearest neighbor
// search; the HNSW index powers the UNBOUNDED adjacency search (ai.SkillAdjacency / KAG), which is already
// live. Both read the same populated column, so the vector layer is now live across the board.
//
// The engine still does exact match first; this is only consulted for a required skill the candidate does
// not hold exactly. A required or candidate skill with no stored embedding simply yields no semantic match
// (exact matching is unaffected), so a partial backfill degrades gracefully rather than inventing matches.
type StoredSkillMatcher struct {
db *tenant.DB
mu sync.RWMutex
cache map[string][]float32 // skill id -> stored, L2 normalized embedding
}
// NewStoredSkillMatcher builds the matcher over the database that holds the global skills table. The
// pgvector codec is registered on the pool, so the embedding column scans into a pgvector.Vector.
func NewStoredSkillMatcher(db *tenant.DB) *StoredSkillMatcher {
return &StoredSkillMatcher{db: db}
}
// Match resolves the required skill against the candidate's skills by cosine over the stored embeddings.
func (m *StoredSkillMatcher) Match(ctx context.Context, required engine.RequiredSkill, cands []engine.CandidateSkill, threshold decimal.Decimal) (engine.SkillMatch, bool) {
if m == nil || !m.db.Ready() || len(cands) == 0 {
return engine.SkillMatch{}, false
}
cache := m.ensureLoaded(ctx)
rv, ok := cache[required.SkillID]
if !ok {
return engine.SkillMatch{}, false // the required skill has no stored embedding; exact match still applies
}
bestSim := -2.0
var best engine.CandidateSkill
found := false
for _, cs := range cands {
cv, ok := cache[cs.SkillID]
if !ok || len(cv) != len(rv) {
continue
}
if sim := dot(rv, cv); sim > bestSim {
bestSim, best, found = sim, cs, true
}
}
if !found {
return engine.SkillMatch{}, false
}
sim := decimal.NewFromFloat(bestSim).Round(4)
if sim.LessThan(threshold) {
return engine.SkillMatch{}, false
}
return engine.SkillMatch{CandidateSkillID: best.SkillID, CandidateName: best.Name, Similarity: sim, Matched: true}, true
}
// ensureLoaded reads every embedded skill into the cache once. skills is a global, no RLS table, so a plain
// pool read is correct and needs no tenant scope. A read failure or a missing embedding degrades to no
// semantic match, never a fake one. Reload picks up a fresh backfill on a server restart.
func (m *StoredSkillMatcher) ensureLoaded(ctx context.Context) map[string][]float32 {
m.mu.RLock()
if m.cache != nil {
c := m.cache
m.mu.RUnlock()
return c
}
m.mu.RUnlock()
m.mu.Lock()
defer m.mu.Unlock()
if m.cache != nil {
return m.cache
}
cache := map[string][]float32{}
// Acquire rather than reading the pool directly: under per tenant routing this type holds a routing
// DB whose Pool is nil, and reading it returned an EMPTY cache with no error. Semantic skill matching
// then silently reported no match for every candidate, changing the Skills component of an AEDT score
// (a quarter of the total) with nothing logged and nothing failed. A scoring engine that is required to
// be bit stable must not depend on which deployment mode a node happens to run in.
leaf, release, aerr := m.db.Acquire(ctx)
if aerr != nil || leaf.Pool == nil {
m.cache = cache
return cache
}
defer release()
rows, err := leaf.Pool.Query(ctx, "SELECT id, embedding FROM skills WHERE embedding IS NOT NULL")
if err != nil {
m.cache = cache // mark loaded; exact matching still works
return cache
}
defer rows.Close()
for rows.Next() {
var id uuid.UUID
var vec pgvector.Vector
if err := rows.Scan(&id, &vec); err != nil {
continue
}
cache[id.String()] = vec.Slice()
}
m.cache = cache
return cache
}