Jobly
Gig market made easier
The gig economy is booming, but matching workers with opportunities remains a challenge. Traditional job platforms rely on keyword matchingโif your resume says "plumber" and the job post says "pipe specialist," you might miss a perfect match. We built Jobly to solve this using semantic search, vector embeddings, and RAG (Retrieval-Augmented Generation).
This post explores the algorithms and techniques behind Jobly's intelligent matching system, built for the Hugging Face Winter Hackathon 2025.
# Simple keyword matching
if "plumbing" in worker_skills and "plumbing" in job_requirements:
score = 100 # Perfect match!
else:
score = 0 # No match
Problems:
We implemented three progressively sophisticated matching algorithms:
TF-IDF (Term Frequency-Inverse Document Frequency) is our lightweight baseline that's smarter than keyword matching.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
# Create TF-IDF vectors
vectorizer = TfidfVectorizer(stop_words='english')
# Example texts
worker_text = "experienced plumber pipe repair specialist Rome"
job_text = "looking for plumbing expert to fix leaking pipes Rome"
# Convert to vectors
worker_vec = vectorizer.fit_transform([worker_text])
job_vec = vectorizer.transform([job_text])
# Calculate similarity
similarity = cosine_similarity(worker_vec, job_vec)[0][0]
# Result: 0.73 (73% match)
Term Frequency measures how often a word appears in a document:
TF(word) = (word count) / (total words)
Inverse Document Frequency measures how unique/important a word is:
IDF(word) = log(total_documents / documents_containing_word)
Combined Score:
TF-IDF = TF ร IDF
This means:
โ Fast (~10ms per query) โ No ML model needed โ Works offline โ Better than keyword matching
โ Still misses synonyms โ No semantic understanding โ Order-dependent
On our test set of 50 workers ร 50 gigs:
This is where the magic happens. Instead of comparing words, we compare meanings.
Imagine every text as a point in 384-dimensional space. Similar meanings = nearby points!
"plumber who fixes pipes" โ [0.23, -0.45, 0.67, ..., 0.11] (384 numbers)
"pipe repair specialist" โ [0.21, -0.43, 0.69, ..., 0.13] (384 numbers)
โ
Distance = 0.94 (very close!)
from sentence_transformers import SentenceTransformer
# Load model (runs locally!)
model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2')
# Create embeddings
worker_embedding = model.encode("experienced plumber, pipe repairs")
job_embedding = model.encode("need plumbing expert for leak fix")
# Calculate cosine similarity
from numpy import dot
from numpy.linalg import norm
similarity = dot(worker_embedding, job_embedding) / (
norm(worker_embedding) * norm(job_embedding)
)
# Result: 0.89 (89% semantic match!)
Model stats:
Alternatives we considered:
| Model | Size | Dims | Speed | Quality |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | 80MB | 384 | Fast | Good โ |
| all-mpnet-base-v2 | 420MB | 768 | Medium | Better |
| multi-qa-mpnet | 420MB | 768 | Medium | Best |
We chose all-MiniLM-L6-v2 for the best speed/quality tradeoff for a demo.
The model understands:
Synonyms:
similarity("plumber", "pipe specialist") # 0.82
similarity("gardener", "landscaper") # 0.79
similarity("photographer", "camera specialist") # 0.75
Related concepts:
similarity("lawn mowing", "garden maintenance") # 0.71
similarity("furniture assembly", "IKEA building") # 0.68
Context awareness:
similarity("Python developer", "Python programmer") # 0.95 โ
similarity("Python developer", "Python snake expert") # 0.23 โ
โ Understands synonyms โ Context-aware โ Language variations โ Robust to typos
โ Slower than TF-IDF (~100ms vs 10ms) โ Requires ML model (80MB) โ GPU helps but not required
RAG (Retrieval-Augmented Generation) combines vector search with a structured database.
User Query
โ
[1] Convert to Embedding (HuggingFace)
โ
[2] Vector Search (ChromaDB)
โ
[3] Retrieve Top K (e.g., top 5)
โ
[4] Enrich with Metadata
โ
[5] Calculate Hybrid Score
โ
Results with Explanations
from llama_index.core import VectorStoreIndex, Document, Settings
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
import chromadb
# Setup
embed_model = HuggingFaceEmbedding(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
Settings.embed_model = embed_model
Settings.llm = None # We use Claude via MCP instead
# Create vector store
chroma_client = chromadb.Client()
collection = chroma_client.create_collection("gig_workers")
vector_store = ChromaVectorStore(chroma_collection=collection)
# Create documents
documents = []
for worker in workers:
text = f"""
Name: {worker['name']}
Title: {worker['title']}
Skills: {', '.join(worker['skills'])}
Experience: {worker['experience']}
Location: {worker['location']}
Bio: {worker['bio']}
"""
doc = Document(text=text, metadata=worker)
documents.append(doc)
# Build index
index = VectorStoreIndex.from_documents(
documents,
vector_store=vector_store
)
# Query
query_engine = index.as_query_engine(similarity_top_k=5)
response = query_engine.query(
"Looking for experienced plumber in Rome for pipe repairs"
)
# Results include semantic similarity + metadata
for node in response.source_nodes:
print(f"Match: {node.metadata['name']}")
print(f"Score: {node.score:.2f}")
print(f"Skills: {node.metadata['skills']}")
Benefits:
Alternatives:
We combine three signals:
def calculate_match_score(worker, job, semantic_similarity):
# 1. Semantic similarity (70% weight)
semantic_score = semantic_similarity * 0.7
# 2. Skill overlap (20% weight)
worker_skills = set(s.lower() for s in worker['skills'])
job_skills = set(s.lower() for s in job['required_skills'])
skill_overlap = len(worker_skills & job_skills) / len(job_skills)
skill_score = skill_overlap * 0.2
# 3. Location match (10% weight)
if 'remote' in job['location'].lower():
location_score = 1.0 * 0.1
elif worker['location'].lower() in job['location'].lower():
location_score = 1.0 * 0.1
else:
location_score = 0.5 * 0.1
# Final score (0-100 scale)
final_score = (semantic_score + skill_score + location_score) * 100
return int(final_score)
Why these weights?
We use the Model Context Protocol to make our matching agentic:
@mcp_server.call_tool()
async def call_tool(name: str, arguments: Dict[str, Any]):
if name == "find_matching_workers_rag":
gig_post = arguments["gig_post"]
# Create semantic query
query = f"""
Skills: {', '.join(gig_post['required_skills'])}
Location: {gig_post['location']}
Experience: {gig_post['experience_level']}
"""
# RAG search
query_engine = workers_index.as_query_engine(similarity_top_k=5)
response = query_engine.query(query)
# Calculate hybrid scores
matches = []
for node in response.source_nodes:
worker = node.metadata
score = calculate_match_score(
worker,
gig_post,
node.score
)
matches.append({
"worker": worker,
"score": score,
"semantic_similarity": node.score
})
return matches
The Claude agent then decides:
Based on our testing with sample queries, here are the estimated performance characteristics:
| Metric | TF-IDF | Embeddings | RAG (Full) |
|---|---|---|---|
| Speed | ~10ms | ~100ms | ~120ms |
| Memory Usage | ~5MB | ~200MB | ~250MB |
| Handles Synonyms | โ | โ | โ |
| Context Awareness | โ | โ | โ |
| Metadata Filtering | โ | โ | โ |
| Qualitative Match Quality | Good | Very Good | Excellent |
TF-IDF:
Vector Embeddings:
RAG (Full System):
Query: "Need someone to fix leaking bathroom pipes in Rome"
TF-IDF Results:
Embeddings Results:
RAG Results:
โ Start simple: TF-IDF baseline before embeddings โ Choose lightweight models: all-MiniLM-L6-v2 is sufficient โ Cache everything: Embeddings, queries, results โ Measure constantly: Track precision, speed, memory โ Explain results: Show similarity scores to users
Use TF-IDF when:
Use Embeddings when:
Use RAG when:
Jobly is open source!
๐ Try the demo on HF Spaces ๐ป View the code ๐ Read the docs
# Clone
git clone https://huggingface.co/spaces/MCP-1st-Birthday/Jobly
cd Jobly
# Install
pip install -r requirements.txt
# Run
python app.py
Try modifying:
multi-qa-mpnet-base-v2Building Jobly taught us that semantic search doesn't require expensive APIs or complex infrastructure. With open-source tools like LlamaIndex and HuggingFace, you can build production-grade matching systems that:
The gig economy deserves better than keyword search. With RAG and vector embeddings, we can finally match people with opportunities based on what they can do, not just what words they used.
Built for Hugging Face Winter Hackathon 2024 ๐
Technology:
Special thanks to:
Questions? Feedback? Comment below or open an issue on the Space repository! ๐ฌ
Gig market made easier