feat: initialize project structure with SQLite database, crawler framework, and LLM-based JD analysis integration
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package filter
|
||||
|
||||
import "context"
|
||||
|
||||
type FilterResult struct {
|
||||
Keep bool
|
||||
Score float64
|
||||
Reason string
|
||||
}
|
||||
|
||||
type JobFilter interface {
|
||||
Name() string
|
||||
Process(ctx context.Context, title, rawContent string) (FilterResult, error)
|
||||
}
|
||||
|
||||
type FilterPipeline struct {
|
||||
filters []JobFilter
|
||||
}
|
||||
|
||||
func NewPipeline(filters ...JobFilter) *FilterPipeline {
|
||||
return &FilterPipeline{filters: filters}
|
||||
}
|
||||
|
||||
func (p *FilterPipeline) Execute(ctx context.Context, title, rawContent string) (bool, float64) {
|
||||
var totalScore float64
|
||||
for _, f := range p.filters {
|
||||
res, err := f.Process(ctx, title, rawContent)
|
||||
if err != nil || !res.Keep {
|
||||
return false, 0
|
||||
}
|
||||
totalScore += res.Score
|
||||
}
|
||||
return true, totalScore
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHardKeywordFilter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fk := NewHardKeywordFilter([]string{"DevOps", "No Sponsorship"})
|
||||
|
||||
// Should fail due to DevOps
|
||||
res1, err := fk.Process(ctx, "Senior DevOps Engineer", "We build pipelines.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if res1.Keep {
|
||||
t.Errorf("expected Keep=false for DevOps, got true")
|
||||
}
|
||||
|
||||
// Should pass
|
||||
res2, err := fk.Process(ctx, "Senior Backend Engineer", "We build scalable APIs in Go.")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !res2.Keep {
|
||||
t.Errorf("expected Keep=true for Backend Engineer, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterPipeline(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
hardFilter := NewHardKeywordFilter([]string{"Unpaid", "No Sponsorship"})
|
||||
scoringFilter := NewScoringFilter([]KeywordWeight{
|
||||
{Keyword: "Go", Weight: 30.0},
|
||||
{Keyword: "Backend", Weight: 20.0},
|
||||
})
|
||||
|
||||
pipeline := NewPipeline(hardFilter, scoringFilter)
|
||||
|
||||
// Test job 1: Unpaid job -> rejected by hard filter
|
||||
keep1, score1 := pipeline.Execute(ctx, "Backend Intern", "Unpaid position working with Go.")
|
||||
if keep1 {
|
||||
t.Errorf("expected job 1 to be filtered out, got keep=true")
|
||||
}
|
||||
if score1 != 0 {
|
||||
t.Errorf("expected score 0 for rejected job, got %f", score1)
|
||||
}
|
||||
|
||||
// Test job 2: Qualified job -> passes both filters and accumulates score
|
||||
keep2, score2 := pipeline.Execute(ctx, "Senior Go Backend Engineer", "Building high throughput services.")
|
||||
if !keep2 {
|
||||
t.Errorf("expected job 2 to pass filter pipeline, got keep=false")
|
||||
}
|
||||
if score2 != 50.0 {
|
||||
t.Errorf("expected score 50.0 (Go + Backend), got %f", score2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HardKeywordFilter struct {
|
||||
Blacklist []string
|
||||
}
|
||||
|
||||
func NewHardKeywordFilter(blacklist []string) *HardKeywordFilter {
|
||||
if len(blacklist) == 0 {
|
||||
blacklist = []string{
|
||||
"no sponsorship",
|
||||
"us citizen only",
|
||||
"unpaid",
|
||||
"volunteer",
|
||||
}
|
||||
}
|
||||
return &HardKeywordFilter{Blacklist: blacklist}
|
||||
}
|
||||
|
||||
func (f *HardKeywordFilter) Name() string {
|
||||
return "HardKeywordFilter"
|
||||
}
|
||||
|
||||
func (f *HardKeywordFilter) Process(ctx context.Context, title, rawContent string) (FilterResult, error) {
|
||||
combined := strings.ToLower(title + " " + rawContent)
|
||||
|
||||
for _, kw := range f.Blacklist {
|
||||
term := strings.ToLower(strings.TrimSpace(kw))
|
||||
if term != "" && strings.Contains(combined, term) {
|
||||
return FilterResult{
|
||||
Keep: false,
|
||||
Score: 0,
|
||||
Reason: fmt.Sprintf("Contains blacklisted keyword: %q", kw),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return FilterResult{
|
||||
Keep: true,
|
||||
Score: 0,
|
||||
Reason: "Passed hard keyword check",
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type KeywordWeight struct {
|
||||
Keyword string
|
||||
Weight float64
|
||||
}
|
||||
|
||||
type ScoringFilter struct {
|
||||
Weights []KeywordWeight
|
||||
}
|
||||
|
||||
func NewScoringFilter(weights []KeywordWeight) *ScoringFilter {
|
||||
if len(weights) == 0 {
|
||||
weights = []KeywordWeight{
|
||||
{Keyword: "go", Weight: 25.0},
|
||||
{Keyword: "golang", Weight: 25.0},
|
||||
{Keyword: "backend", Weight: 15.0},
|
||||
{Keyword: "distributed systems", Weight: 20.0},
|
||||
{Keyword: "microservices", Weight: 15.0},
|
||||
{Keyword: "sqlite", Weight: 10.0},
|
||||
{Keyword: "postgres", Weight: 10.0},
|
||||
{Keyword: "api", Weight: 10.0},
|
||||
{Keyword: "full stack", Weight: 10.0},
|
||||
{Keyword: "rest", Weight: 5.0},
|
||||
{Keyword: "grpc", Weight: 15.0},
|
||||
}
|
||||
}
|
||||
return &ScoringFilter{Weights: weights}
|
||||
}
|
||||
|
||||
func (s *ScoringFilter) Name() string {
|
||||
return "ScoringFilter"
|
||||
}
|
||||
|
||||
func (s *ScoringFilter) Process(ctx context.Context, title, rawContent string) (FilterResult, error) {
|
||||
combined := strings.ToLower(title + " " + rawContent)
|
||||
var totalScore float64
|
||||
var matched []string
|
||||
|
||||
for _, kw := range s.Weights {
|
||||
term := strings.ToLower(strings.TrimSpace(kw.Keyword))
|
||||
if term != "" && strings.Contains(combined, term) {
|
||||
totalScore += kw.Weight
|
||||
matched = append(matched, kw.Keyword)
|
||||
}
|
||||
}
|
||||
|
||||
return FilterResult{
|
||||
Keep: true,
|
||||
Score: totalScore,
|
||||
Reason: fmt.Sprintf("Matched %d keywords: %v", len(matched), matched),
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user