package matching import ( "context" "github.com/shopspring/decimal" "github.com/missionworkforce/missionmatch/internal/modules/matching/engine" "github.com/missionworkforce/missionmatch/internal/platform/embeddings" ) // 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 ctx context.Context } // NewEmbeddingSkillMatcher wraps an embedder as an engine.SkillMatcher. func NewEmbeddingSkillMatcher(e embeddings.Embedder) *EmbeddingSkillMatcher { return &EmbeddingSkillMatcher{embedder: e, ctx: context.Background()} } // Match satisfies engine.SkillMatcher. It returns the candidate skill side and the similarity; the // engine owns the required-side identity and the match flags. func (m *EmbeddingSkillMatcher) Match(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(m.ctx, required.Name) if err != nil { return engine.SkillMatch{}, false } thr, _ := threshold.Float64() bestSim := -2.0 var best engine.CandidateSkill found := false for _, cs := range cands { cv, err := m.embedder.Embed(m.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 || bestSim < thr { return engine.SkillMatch{}, false } return engine.SkillMatch{ CandidateSkillID: best.SkillID, CandidateName: best.Name, Similarity: decimal.NewFromFloat(bestSim).Round(4), 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 }