Build an Intelligent Supplier Search Tool
Build a semantic search interface for a 200+ supplier database using Python.
Introduction
30 Second Summary
Finding the right supplier is one of the most time-consuming tasks in any company's operations. Employees often know what they need but not the exact company name or keyword to search for.
In this project, you will build a web-based supplier search tool that understands the meaning behind a query, not just exact keywords. Type something like "sustainable packaging with fast delivery in Europe" and get relevant results instantly.
What You'll Build
An intelligent supplier search interface powered by semantic search and structured filters, running entirely on your local machine with no paid APIs.
By the end of this project, you'll have:
- A supplier dataset with 200+ entries containing rich metadata like shipping time, CO2 emissions, production capacity, and costs.
- A local vector database powered by ChromaDB that stores supplier embeddings and handles meaning-based search automatically.
- A Streamlit web interface with a search bar, sidebar filters for industry and shipping time, and detailed result cards.
- Secret Mission: Add a supplier comparison view that lets users select and compare suppliers side by side.
Are there any prerequisites?
You need basic Python knowledge (variables, functions, lists). No AI or machine learning experience is required.
Set Up Your Python Environment
Before you can build an intelligent search tool, you need the right foundation. This step gets your development environment ready with Python, a virtual environment, and the two key libraries that power this project.
You'll install Streamlit (for the web interface) and ChromaDB (for the vector database). Both run locally on your machine with zero cost.
In this step, get ready to:
- Install Python 3.10+ and set up a virtual environment.
- Install Streamlit and ChromaDB.
- Create the project folder structure with all required files.
Install Python 3.10+ and create a virtual environment
Streamlit requires Python 3.10 or higher. Let's confirm you have the right version installed.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
- Check your Python version by running this command:
python3 --version
✔️ I see version 3.10 or higher
You're all set. Move on to creating your project folder below.
ⓧ I see a version lower than 3.10
Your system has an older Python version that won't work with Streamlit 1.57.0. You need to install Python 3.10 or newer.
On macOS, the system may ship with an older Python. Even after installing a newer version, python3 might still point to the old one.
- Download the latest Python from python.org/downloads and run the installer.
- After installing, close and reopen your terminal.
- Run python3 --version again to confirm the new version shows up.
- If macOS still shows the old version, try running python3.10 --version or python3.11 --version to use the specific binary directly.
ⓧ Command not found
Python is not installed on your machine yet. Let's fix that.
- Download Python 3.10+ from python.org/downloads.
- Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install Now.
- Close and reopen your terminal after installation.
- Run python3 --version again to confirm you see 3.10 or higher.
Now let's create your project folder and set up an isolated Python environment inside it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir supplier-search
cd supplier-search
- Create a virtual environment by running this command:
python3 -m venv venv
What is a virtual environment?
A virtual environment is an isolated Python installation specific to your project. It keeps the packages you install here separate from your system Python, so different projects can use different library versions without conflicts.
- Activate the virtual environment:
macOS
source venv/bin/activate
Windows
.\venv\Scripts\Activate.ps1
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, PowerShell may block the activation script by default. Run this command first, then try activating again:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
On macOS, make sure you're in the supplier-search folder and that the venv folder exists (run ls to check).
help me activate my Python virtual environment
Install Streamlit and ChromaDB
- Install both project dependencies by running this command:
pip install streamlit chromadb
What does this install?
Streamlit is a Python framework that turns scripts into interactive web apps with zero HTML or CSS. You'll use it to build the search interface.
ChromaDB is a vector database that stores text as mathematical embeddings and finds similar content by meaning. It handles the semantic search behind the scenes.
The installation may take a minute. ChromaDB pulls in several dependencies including the embedding model it uses for semantic search.
- Verify Streamlit installed correctly by running this command:
streamlit hello
A new browser tab should open with the Streamlit demo app. This confirms everything is working.
- Go back to your terminal and press Ctrl+C to stop the demo app.
Browser didn't open?
If the browser didn't open automatically, check your terminal output for a URL (it will look like http://localhost:8501). Copy and paste that URL into your browser manually.
If you see a 'command not found' error, make sure your virtual environment is still active (you should see (venv) in your prompt).
help me get streamlit hello working
Create the project file structure
Your project needs three files. You'll populate them in the upcoming steps, but creating them now gives you the complete structure to work with.
- Create the three project files by running these commands (make sure you're inside the supplier-search folder):
macOS
touch suppliers.csv load_data.py app.py
Windows
New-Item suppliers.csv, load_data.py, app.py
- Confirm the files exist by running:
ls
You should see app.py, load_data.py, suppliers.csv, and the venv folder listed in the output.
What is each file for?
- suppliers.csv will hold your supplier dataset with 200+ entries.
- load_data.py will read the CSV and load it into the ChromaDB vector database.
- app.py will be the Streamlit web interface for searching suppliers.
Don't see your files?
Make sure you're in the supplier-search folder. Run pwd (macOS) or cd (Windows) to see your current location. You should see a path ending in supplier-search.
help me find my project files
Your environment is ready and your project structure is in place. Next up, you'll populate the supplier dataset with real data that your search tool can work with.
Prepare Your Supplier Dataset
Your Python environment is ready with all the packages installed. Now it's time to build the dataset your search tool will query.
A search engine is only as good as the data behind it. In this step, you'll create a structured supplier dataset with rich text descriptions for semantic search and numeric fields for filtering.
In this step, get ready to:
- Create the CSV file structure with sample supplier data.
- Populate the full 200+ row dataset.
- Understand which fields power semantic search vs. structured filtering.
Create the CSV file structure with sample data
Your supplier dataset lives in a single CSV file. Each row represents one supplier, and each column captures a different attribute like location, shipping time, or a text description.
- Open suppliers.csv in your code editor (it should be empty right now).
- Paste the following sample data into the file:
id,company_name,industry,location,shipping_time_days,co2_emissions_kg,production_capacity,previous_relationship,design_capabilities,compatible_materials,cost_rating,shipping_cost_rating,description
1,TechParts Global,Electronics,Shenzhen China,7,45,high,yes,custom PCB design and rapid prototyping,FR4 aluminum copper flex circuits,medium,low,Manufacturer of circuit boards and electronic components specializing in custom PCB design rapid prototyping and small-batch production for consumer electronics and IoT devices
2,PackRight Solutions,Packaging,Rotterdam Netherlands,3,12,medium,yes,custom box design and structural engineering,cardboard biodegradable plastics recycled paper,low,low,Sustainable packaging solutions including biodegradable materials and custom box design for e-commerce fulfillment with carbon-neutral shipping options
3,SwiftLog Express,Logistics,Frankfurt Germany,1,8,high,no,route optimization and supply chain consulting,temperature-sensitive goods hazardous materials oversized freight,medium,medium,International freight forwarding and last-mile delivery services across Europe with temperature-controlled transport and real-time tracking
4,GreenMat Industries,Raw Materials,Sao Paulo Brazil,14,30,high,yes,material formulation and sustainability consulting,recycled PET HDPE bio-based polymers natural rubber,low,high,Supplier of recycled plastics and sustainable raw materials for manufacturing with ISO 14001 certified environmental management systems
5,PrecisionTool Co,Manufacturing Equipment,Osaka Japan,21,60,low,no,custom tooling design and automation integration,steel titanium aluminum carbide,high,high,High-precision CNC machines and industrial tooling equipment with maintenance contracts operator training and 5-axis machining capabilities
6,CloudServe Asia,IT Services,Singapore,0,2,high,yes,cloud architecture design and security auditing,AWS Azure GCP hybrid cloud,medium,low,Cloud infrastructure management and cybersecurity solutions for enterprise clients with 24/7 support SOC2 compliance and disaster recovery planning
7,BioClean Supplies,Cleaning Products,Toronto Canada,5,15,medium,no,custom formulation for specific environments,food-safe chemicals pharmaceutical-grade solvents enzymes,medium,medium,Industrial cleaning chemicals and sanitization equipment for food processing pharmaceutical environments and cleanroom facilities
8,SteelForge United,Raw Materials,Pittsburgh USA,10,85,high,yes,custom alloy development and metallurgical testing,carbon steel stainless steel aluminum alloys titanium,medium,medium,Premium steel alloys and metal fabrication services for construction automotive and aerospace industries with ASTM certified materials
9,QuickChip Semiconductors,Electronics,Taipei Taiwan,5,20,high,yes,ASIC design and chip customization,silicon germanium gallium arsenide,high,low,Semiconductor wafers and microchip components with short lead times flexible minimum order quantities and full custom ASIC design services
10,NordicWood Timber,Raw Materials,Stockholm Sweden,8,5,medium,no,architectural wood engineering and custom profiling,pine spruce oak birch engineered wood CLT,low,medium,Sustainably sourced timber and engineered wood products FSC certified for construction and furniture manufacturing with carbon-negative production
11,AeroFreight International,Logistics,Dubai UAE,2,55,high,yes,customs brokerage and trade compliance,air cargo sea freight rail overland express,high,high,Air cargo and express delivery services connecting Asia Europe and Africa with customs brokerage dangerous goods handling and bonded warehousing
12,SmartPack Automation,Packaging,Milan Italy,30,25,low,no,robotic system design and production line integration,cardboard shrink wrap foam pallets crates,high,medium,Automated packaging machinery and robotic palletizing systems for high-volume production lines with AI-powered quality inspection
13,DataLink Networks,IT Services,Austin USA,0,3,medium,yes,network architecture and SD-WAN deployment,fiber optic copper wireless 5G,medium,low,Network infrastructure design and managed IT services for mid-market companies including SD-WAN deployment zero-trust security and 99.99% uptime SLA
14,ChemFlow Industries,Raw Materials,Mumbai India,12,40,high,no,polymer formulation and compound development,polyethylene polypropylene ABS nylon PVC specialty polymers,low,medium,Industrial chemicals and specialty polymers for plastics manufacturing with global shipping capabilities and custom compound development
15,EuroParts Distribution,Electronics,Munich Germany,2,8,high,yes,component sourcing and BOM optimization,connectors sensors resistors capacitors ICs,medium,low,Electronic component distribution with extensive inventory of connectors sensors and passive components same-day shipping within Europe and BOM sourcing services
What do these columns mean?
- id is a unique identifier for each supplier.
- company_name, industry, and location are categorical fields for filtering.
- shipping_time_days and co2_emissions_kg are numeric fields you can filter with sliders.
- description is the rich text field that powers semantic search. It contains natural language details about what the supplier does.
- design_capabilities and compatible_materials are also text fields that get combined into the searchable document later.
- Save suppliers.csv.
- Open the file in your editor and confirm you see the header row plus 15 data rows.
Seeing garbled columns or misaligned data?
Make sure no extra commas snuck into the data. The description fields in this CSV deliberately avoid commas so that no quoting is needed.
If you see fewer than 12 columns in a row, check that nothing was accidentally deleted during the paste.
Populate the full 200+ dataset
Fifteen rows are enough to validate the structure, but a real search tool needs volume. You need at least 200 suppliers so that semantic search can show meaningful differentiation between results.
Why 200+ rows?
With only 15 rows, every query returns almost everything. A larger dataset forces the search engine to rank results by relevance. This is where semantic search proves its value over simple keyword matching.
You have two options for getting to 200+ rows.
✔️ I'll generate sample data with Claude
Use Claude (or any AI assistant) to generate realistic supplier data that follows the same structure.
- Open Claude and give it the following prompt:
Generate 185 more rows of supplier data in CSV format (no header row) following this exact column structure:
id,company_name,industry,location,shipping_time_days,co2_emissions_kg,production_capacity,previous_relationship,design_capabilities,compatible_materials,cost_rating,shipping_cost_rating,description
Rules:
- IDs should start at 16 and go up sequentially
- industry must be one of: Electronics, Packaging, Logistics, Raw Materials, Manufacturing Equipment, IT Services, Cleaning Products
- location should be realistic city + country combinations
- shipping_time_days: integer between 0 and 30
- co2_emissions_kg: integer between 1 and 100
- production_capacity: low, medium, or high
- previous_relationship: yes or no
- cost_rating: low, medium, or high
- shipping_cost_rating: low, medium, or high
- description should be 1-2 sentences of natural language (NO commas in this field)
- design_capabilities and compatible_materials should be realistic for the industry (NO commas within these fields, use spaces to separate items)
Here are the first 3 rows as examples:
1,TechParts Global,Electronics,Shenzhen China,7,45,high,yes,custom PCB design and rapid prototyping,FR4 aluminum copper flex circuits,medium,low,Manufacturer of circuit boards and electronic components specializing in custom PCB design rapid prototyping and small-batch production for consumer electronics and IoT devices
2,PackRight Solutions,Packaging,Rotterdam Netherlands,3,12,medium,yes,custom box design and structural engineering,cardboard biodegradable plastics recycled paper,low,low,Sustainable packaging solutions including biodegradable materials and custom box design for e-commerce fulfillment with carbon-neutral shipping options
3,SwiftLog Express,Logistics,Frankfurt Germany,1,8,high,no,route optimization and supply chain consulting,temperature-sensitive goods hazardous materials oversized freight,medium,medium,International freight forwarding and last-mile delivery services across Europe with temperature-controlled transport and real-time tracking
What does this prompt do?
The prompt gives Claude the exact column structure, valid values for each categorical field, and formatting rules (no commas inside fields). The example rows show the pattern Claude should follow.
- Copy the generated rows from Claude's response.
- Paste them at the bottom of your suppliers.csv file, directly after row 15.
- Save suppliers.csv.
Quick row count check
Open your terminal and run wc -l suppliers.csv (macOS/Linux) or open the file in your editor and check the line count at the bottom. You should see at least 201 lines (1 header + 200 data rows).
ⓧ I have real supplier data to use
If you have real supplier data from your company, you can use that instead.
- Export your data as a CSV from whatever tool you use (Google Sheets, Excel, your ERP system).
- Make sure the columns match the exact header row in suppliers.csv: id, company_name, industry, location, shipping_time_days, co2_emissions_kg, production_capacity, previous_relationship, design_capabilities, compatible_materials, cost_rating, shipping_cost_rating, description.
- Remove any commas from the description, design_capabilities, and compatible_materials fields. The CSV parser will misread commas inside these fields.
- Make sure you have at least 200 rows of data.
- Save the file as suppliers.csv in your supplier-search folder, replacing the sample data.
Claude generated data with commas inside fields?
AI-generated CSV data sometimes includes commas in description fields, which breaks the column alignment. Ask Claude to regenerate with the explicit rule: "Do not use any commas within any field value."
You can also do a quick find-and-replace in your editor to remove stray commas from description text.
Understand semantic search vs. structured filtering fields
Not every column in your dataset serves the same purpose. Your search tool will use two different strategies depending on the field type.
How will each field be used?
Fields split into two groups based on how the search engine processes them:
Semantic search fields are text-rich columns that get converted into vector embeddings. The embedding model reads the natural language and understands meaning. Your searchable document will combine: description, design_capabilities, compatible_materials, company_name, industry, and location.
Structured filtering fields are numeric or categorical values that support exact comparisons. These get stored as metadata for filtering: shipping_time_days, co2_emissions_kg, production_capacity, previous_relationship, cost_rating, and shipping_cost_rating.
Embedding models do not understand numbers well. A query like "shipping under 5 days" will not reliably filter by the number 5. That is why numeric fields are handled separately with explicit filters.
Why not just search everything with embeddings?
Embeddings capture meaning from language. They are great for "sustainable packaging for online stores" because they understand synonyms and context.
But "shipping under 3 days" is a numeric constraint, not a meaning-based query. Structured filters handle these constraints precisely. Combining both gives you the best of both worlds.
✔️ Awesome, I've got everything!
Great. Double check that your suppliers.csv has at least 200 data rows and that the first 15 rows match the sample data above.
ⓧ I'd like to double check the full code
Here are the first 15 rows of your CSV file. Your file should have these 15 rows plus the additional rows you generated, for a total of 200+ data rows.
id,company_name,industry,location,shipping_time_days,co2_emissions_kg,production_capacity,previous_relationship,design_capabilities,compatible_materials,cost_rating,shipping_cost_rating,description
1,TechParts Global,Electronics,Shenzhen China,7,45,high,yes,custom PCB design and rapid prototyping,FR4 aluminum copper flex circuits,medium,low,Manufacturer of circuit boards and electronic components specializing in custom PCB design rapid prototyping and small-batch production for consumer electronics and IoT devices
2,PackRight Solutions,Packaging,Rotterdam Netherlands,3,12,medium,yes,custom box design and structural engineering,cardboard biodegradable plastics recycled paper,low,low,Sustainable packaging solutions including biodegradable materials and custom box design for e-commerce fulfillment with carbon-neutral shipping options
3,SwiftLog Express,Logistics,Frankfurt Germany,1,8,high,no,route optimization and supply chain consulting,temperature-sensitive goods hazardous materials oversized freight,medium,medium,International freight forwarding and last-mile delivery services across Europe with temperature-controlled transport and real-time tracking
4,GreenMat Industries,Raw Materials,Sao Paulo Brazil,14,30,high,yes,material formulation and sustainability consulting,recycled PET HDPE bio-based polymers natural rubber,low,high,Supplier of recycled plastics and sustainable raw materials for manufacturing with ISO 14001 certified environmental management systems
5,PrecisionTool Co,Manufacturing Equipment,Osaka Japan,21,60,low,no,custom tooling design and automation integration,steel titanium aluminum carbide,high,high,High-precision CNC machines and industrial tooling equipment with maintenance contracts operator training and 5-axis machining capabilities
6,CloudServe Asia,IT Services,Singapore,0,2,high,yes,cloud architecture design and security auditing,AWS Azure GCP hybrid cloud,medium,low,Cloud infrastructure management and cybersecurity solutions for enterprise clients with 24/7 support SOC2 compliance and disaster recovery planning
7,BioClean Supplies,Cleaning Products,Toronto Canada,5,15,medium,no,custom formulation for specific environments,food-safe chemicals pharmaceutical-grade solvents enzymes,medium,medium,Industrial cleaning chemicals and sanitization equipment for food processing pharmaceutical environments and cleanroom facilities
8,SteelForge United,Raw Materials,Pittsburgh USA,10,85,high,yes,custom alloy development and metallurgical testing,carbon steel stainless steel aluminum alloys titanium,medium,medium,Premium steel alloys and metal fabrication services for construction automotive and aerospace industries with ASTM certified materials
9,QuickChip Semiconductors,Electronics,Taipei Taiwan,5,20,high,yes,ASIC design and chip customization,silicon germanium gallium arsenide,high,low,Semiconductor wafers and microchip components with short lead times flexible minimum order quantities and full custom ASIC design services
10,NordicWood Timber,Raw Materials,Stockholm Sweden,8,5,medium,no,architectural wood engineering and custom profiling,pine spruce oak birch engineered wood CLT,low,medium,Sustainably sourced timber and engineered wood products FSC certified for construction and furniture manufacturing with carbon-negative production
11,AeroFreight International,Logistics,Dubai UAE,2,55,high,yes,customs brokerage and trade compliance,air cargo sea freight rail overland express,high,high,Air cargo and express delivery services connecting Asia Europe and Africa with customs brokerage dangerous goods handling and bonded warehousing
12,SmartPack Automation,Packaging,Milan Italy,30,25,low,no,robotic system design and production line integration,cardboard shrink wrap foam pallets crates,high,medium,Automated packaging machinery and robotic palletizing systems for high-volume production lines with AI-powered quality inspection
13,DataLink Networks,IT Services,Austin USA,0,3,medium,yes,network architecture and SD-WAN deployment,fiber optic copper wireless 5G,medium,low,Network infrastructure design and managed IT services for mid-market companies including SD-WAN deployment zero-trust security and 99.99% uptime SLA
14,ChemFlow Industries,Raw Materials,Mumbai India,12,40,high,no,polymer formulation and compound development,polyethylene polypropylene ABS nylon PVC specialty polymers,low,medium,Industrial chemicals and specialty polymers for plastics manufacturing with global shipping capabilities and custom compound development
15,EuroParts Distribution,Electronics,Munich Germany,2,8,high,yes,component sourcing and BOM optimization,connectors sensors resistors capacitors ICs,medium,low,Electronic component distribution with extensive inventory of connectors sensors and passive components same-day shipping within Europe and BOM sourcing services
Your dataset is ready. Next up, you'll load this CSV into a ChromaDB vector database and run your first semantic queries against it.
Build the Vector Database
Your supplier dataset is ready with 200+ rows of structured data. Now you need a way to search it by meaning, not just keywords.
ChromaDB is a vector database that converts text into numerical representations called embeddings. It handles that conversion automatically, so you can feed it plain text and immediately query by similarity.
In this step, get ready to:
- Initialize a ChromaDB persistent client and create a collection for suppliers.
- Write a data loader that builds rich searchable documents and stores structured metadata.
- Test the database with sample queries to verify semantic search is working.
Initialize ChromaDB and create a supplier collection
ChromaDB's PersistentClient stores data on disk so it survives between script runs. You will point it at a local folder and create a named collection to hold all your supplier embeddings.
- Open load_data.py in your editor and add the following code:
import chromadb
import csv
def load_suppliers():
client = chromadb.PersistentClient(path="./supplier_db")
# Delete existing collection if it exists to start fresh
try:
client.delete_collection("suppliers")
except ValueError:
pass
collection = client.create_collection(
name="suppliers",
metadata={"description": "Supplier database for semantic search"}
)
What does this code do?
- chromadb.PersistentClient(path="./supplier_db") creates a database that saves to a supplier_db folder in your project directory. Without PersistentClient, data would disappear when the script ends.
- The try/except block deletes any existing collection so you can re-run the script cleanly during development.
- create_collection(name="suppliers") creates a fresh collection to hold your supplier documents and their embeddings.
- Save load_data.py.
You will not see a change yet. The next chunk adds the data loading logic that this function needs.
Seeing an import error?
Make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt. If not, re-activate it before continuing.
help me fix my ChromaDB import error in load_data.py
Load supplier data into the collection
ChromaDB needs two things from each supplier: a document (text that gets converted into an embedding for similarity search) and metadata (structured fields you can filter on later).
The document combines a supplier's descriptive text fields into one rich string. Numeric fields like shipping time go into metadata only, since embeddings do not understand numbers well.
- In load_data.py, add the following code directly below the collection = client.create_collection(...) block, still inside the load_suppliers() function:
suppliers = []
with open("suppliers.csv", "r") as f:
reader = csv.DictReader(f)
for row in reader:
suppliers.append(row)
ids = []
documents = []
metadatas = []
for supplier in suppliers:
# Build a rich searchable document combining all text fields
document = (
f"{supplier['company_name']}. "
f"Industry: {supplier['industry']}. "
f"Located in {supplier['location']}. "
f"Design capabilities: {supplier['design_capabilities']}. "
f"Compatible materials: {supplier['compatible_materials']}. "
f"{supplier['description']}"
)
ids.append(supplier["id"])
documents.append(document)
metadatas.append({
"company_name": supplier["company_name"],
"industry": supplier["industry"],
"location": supplier["location"],
"shipping_time_days": int(supplier["shipping_time_days"]),
"co2_emissions_kg": int(supplier["co2_emissions_kg"]),
"production_capacity": supplier["production_capacity"],
"previous_relationship": supplier["previous_relationship"],
"design_capabilities": supplier["design_capabilities"],
"compatible_materials": supplier["compatible_materials"],
"cost_rating": supplier["cost_rating"],
"shipping_cost_rating": supplier["shipping_cost_rating"],
})
collection.add(
ids=ids,
documents=documents,
metadatas=metadatas,
)
print(f"Loaded {collection.count()} suppliers into the database.")
return collection
What does this code do?
This code has three stages: read, transform, and store.
- Read: csv.DictReader opens your CSV and turns each row into a Python dictionary, using the header row as keys.
- Transform: The document string combines company name, industry, location, design capabilities, materials, and description into one passage. This gives the embedding model rich semantic content to search against.
- Metadata: Structured fields like shipping_time_days and co2_emissions_kg are cast to int so ChromaDB can filter on them numerically (e.g. "show me suppliers with shipping under 5 days").
- Store: collection.add() sends all documents and metadata to ChromaDB in one batch. ChromaDB automatically generates embeddings for each document using its built-in model.
Why combine text fields into one document?
Embeddings capture the meaning of text as a whole. By combining company name, capabilities, materials, and description into a single string, ChromaDB can match a query like "sustainable packaging for e-commerce" against all of those fields at once.
Numeric fields (shipping time, CO2 emissions) go into metadata instead. Embeddings treat numbers as text characters, not quantities. Metadata filtering handles numeric comparisons properly.
- Save load_data.py.
You will not see a change yet. The next chunk adds test queries so you can run the script and see results.
Test the database with sample queries
To verify that semantic search is working, you will add a test block that runs two queries. One is a plain natural-language query. The other adds a metadata filter to narrow results.
- In load_data.py, add the following code at the bottom of the file, outside the load_suppliers() function (no indentation):
if __name__ == "__main__":
collection = load_suppliers()
results = collection.query(
query_texts=["eco-friendly packaging for online stores"],
n_results=3,
)
print("\nTest query: 'eco-friendly packaging for online stores'")
print("Top 3 results:")
for i, metadata in enumerate(results["metadatas"][0]):
distance = results["distances"][0][i]
print(f" {i+1}. {metadata['company_name']} (distance: {distance:.4f})")
results_filtered = collection.query(
query_texts=["fast shipping electronics"],
n_results=3,
where={"previous_relationship": "yes"},
)
print("\nTest query: 'fast shipping electronics' (existing relationships only)")
print("Top 3 results:")
for i, metadata in enumerate(results_filtered["metadatas"][0]):
distance = results_filtered["distances"][0][i]
print(f" {i+1}. {metadata['company_name']} - {metadata['shipping_time_days']} days (distance: {distance:.4f})")
What does this code do?
- if __name__ == "__main__": ensures this block only runs when you execute the file directly (not when it is imported by another script later).
- The first query searches for "eco-friendly packaging for online stores" across all suppliers. ChromaDB returns the 3 closest matches by meaning, even if no supplier description uses those exact words.
- The second query adds where={"previous_relationship": "yes"} to filter results down to suppliers your company has worked with before. This shows how metadata filtering combines with semantic search.
- Lower distance values mean higher similarity. A distance of 0 would mean an exact match.
- Save load_data.py.
- Run the data loader by executing this command in your terminal:
python load_data.py
First run takes a moment
The first time ChromaDB processes documents, it downloads an embedding model (approximately 90MB). This only happens once. Subsequent runs use the cached model and complete much faster.
You should see output showing the loaded supplier count followed by test query results. The first query should return packaging-related suppliers, and the second should return electronics suppliers with existing relationships.
Seeing a FileNotFoundError?
Make sure suppliers.csv is in the same folder as load_data.py. Also confirm your terminal is inside the supplier-search folder when you run the command.
If you see a ValueError about casting to int, check that your CSV does not have empty values in the shipping_time_days or co2_emissions_kg columns. Every row needs a valid number in those fields.
help me debug my load_data.py script
✔️ Awesome, I've got everything!
Great. Double check you have saved load_data.py and that your terminal shows the loaded count plus test query results.
ⓧ I'd like to double check the full code
Here is the complete load_data.py file. Compare it with yours:
import chromadb
import csv
def load_suppliers():
client = chromadb.PersistentClient(path="./supplier_db")
# Delete existing collection if it exists to start fresh
try:
client.delete_collection("suppliers")
except ValueError:
pass
collection = client.create_collection(
name="suppliers",
metadata={"description": "Supplier database for semantic search"}
)
suppliers = []
with open("suppliers.csv", "r") as f:
reader = csv.DictReader(f)
for row in reader:
suppliers.append(row)
ids = []
documents = []
metadatas = []
for supplier in suppliers:
# Build a rich searchable document combining all text fields
document = (
f"{supplier['company_name']}. "
f"Industry: {supplier['industry']}. "
f"Located in {supplier['location']}. "
f"Design capabilities: {supplier['design_capabilities']}. "
f"Compatible materials: {supplier['compatible_materials']}. "
f"{supplier['description']}"
)
ids.append(supplier["id"])
documents.append(document)
metadatas.append({
"company_name": supplier["company_name"],
"industry": supplier["industry"],
"location": supplier["location"],
"shipping_time_days": int(supplier["shipping_time_days"]),
"co2_emissions_kg": int(supplier["co2_emissions_kg"]),
"production_capacity": supplier["production_capacity"],
"previous_relationship": supplier["previous_relationship"],
"design_capabilities": supplier["design_capabilities"],
"compatible_materials": supplier["compatible_materials"],
"cost_rating": supplier["cost_rating"],
"shipping_cost_rating": supplier["shipping_cost_rating"],
})
collection.add(
ids=ids,
documents=documents,
metadatas=metadatas,
)
print(f"Loaded {collection.count()} suppliers into the database.")
return collection
if __name__ == "__main__":
collection = load_suppliers()
results = collection.query(
query_texts=["eco-friendly packaging for online stores"],
n_results=3,
)
print("\nTest query: 'eco-friendly packaging for online stores'")
print("Top 3 results:")
for i, metadata in enumerate(results["metadatas"][0]):
distance = results["distances"][0][i]
print(f" {i+1}. {metadata['company_name']} (distance: {distance:.4f})")
results_filtered = collection.query(
query_texts=["fast shipping electronics"],
n_results=3,
where={"previous_relationship": "yes"},
)
print("\nTest query: 'fast shipping electronics' (existing relationships only)")
print("Top 3 results:")
for i, metadata in enumerate(results_filtered["metadatas"][0]):
distance = results_filtered["distances"][0][i]
print(f" {i+1}. {metadata['company_name']} - {metadata['shipping_time_days']} days (distance: {distance:.4f})")
Your vector database is loaded and semantic search is working. Next up, you will build the Streamlit web interface that lets anyone search this database through a browser.
Build the Streamlit Search Interface
Your supplier database is loaded and ready to accept queries. The missing piece is a way for employees to actually search it without writing Python code every time.
In this step, you'll build a full web interface using Streamlit. The app will include a search bar, sidebar filters, and result cards that show ranked suppliers with similarity scores.
In this step, get ready to:
- Create the app layout with a search input, sidebar filters, and a database connection.
- Connect the search bar to ChromaDB queries and build the filter logic.
- Display ranked results with similarity scores, detail expanders, and color-coded match quality.
Set up the app shell and database connection
Every Streamlit app starts with page configuration and any resource connections it needs. You'll set up the page title, connect to your existing ChromaDB database, and cache the connection so it persists across user interactions.
- Open app.py in your editor and add the following code:
import streamlit as st
import chromadb
st.set_page_config(page_title="Supplier Search", page_icon="🔍", layout="wide")
st.title("🔍 Intelligent Supplier Search")
st.markdown("Search for suppliers using natural language. Combine with filters to narrow results.")
@st.cache_resource
def get_collection():
client = chromadb.PersistentClient(path="./supplier_db")
return client.get_collection("suppliers")
collection = get_collection()
What does this code do?
- st.set_page_config sets the browser tab title, icon, and uses a wide layout to give results more room.
- @st.cache_resource tells Streamlit to create the database connection once and reuse it. Without this, Streamlit would reconnect to ChromaDB on every single user interaction (every keystroke, every filter change).
- get_collection() connects to the same ./supplier_db directory your data loader created and retrieves the suppliers collection.
- Save app.py.
Build the sidebar filters and search input
Streamlit's sidebar lets you add filters that don't take up space from the main results area. You'll create dropdowns and sliders that employees can use to narrow results by industry, relationship history, and shipping speed.
- Add the following code below the collection = get_collection() line in app.py:
# Sidebar filters
st.sidebar.header("Filters")
industry_filter = st.sidebar.selectbox(
"Industry",
["All", "Electronics", "Packaging", "Logistics", "Raw Materials",
"Manufacturing Equipment", "IT Services", "Cleaning Products"],
)
relationship_filter = st.sidebar.selectbox(
"Previous Relationship",
["All", "Yes", "No"],
)
max_shipping_days = st.sidebar.slider(
"Max Shipping Time (days)",
min_value=0,
max_value=30,
value=30,
)
num_results = st.sidebar.slider("Number of results", min_value=1, max_value=20, value=10)
# Main search
query = st.text_input(
"What are you looking for?",
placeholder="e.g., sustainable packaging with fast delivery in Europe",
)
What does this code do?
- st.sidebar.selectbox creates dropdown menus in the sidebar. The first argument is the label, the second is the list of options.
- st.sidebar.slider creates draggable sliders. The shipping time slider defaults to 30 (no filtering), so all suppliers show up until the user drags it lower.
- st.text_input creates the main search bar in the center of the page. The placeholder text gives employees an example of what to type.
- Save app.py.
- Launch the app by running this command in your terminal:
streamlit run app.py
- Your browser should open automatically to http://localhost:8501. You should see the title, a search bar, and sidebar filters on the left.
App not opening in your browser?
- Check the terminal output for the local URL. If your browser didn't open automatically, copy and paste the URL (usually http://localhost:8501) into your browser manually.
- Make sure you're running the command from inside the supplier-search folder where app.py lives.
- Make sure your virtual environment is still active (you should see the environment name in your terminal prompt).
Why isn't my Streamlit app starting?
Write the search query logic
When a user types a query, the app needs to translate their sidebar filter selections into ChromaDB's metadata filter format, then execute the semantic search. ChromaDB uses operators like $lte (less than or equal) and $and to combine multiple filter conditions.
- Add the following code below the query = st.text_input(...) block in app.py:
if query:
# Build metadata filter
where_filters = {}
if industry_filter != "All":
where_filters["industry"] = industry_filter
if relationship_filter != "All":
where_filters["previous_relationship"] = relationship_filter.lower()
if max_shipping_days < 30:
where_filters["shipping_time_days"] = {"$lte": max_shipping_days}
# Combine multiple filters with $and if needed
where_clause = None
if len(where_filters) == 1:
key = list(where_filters.keys())[0]
where_clause = {key: where_filters[key]}
elif len(where_filters) > 1:
where_clause = {"$and": [{k: v} for k, v in where_filters.items()]}
query_params = {
"query_texts": [query],
"n_results": num_results,
}
if where_clause:
query_params["where"] = where_clause
results = collection.query(**query_params)
st.markdown(f"### Results for: *{query}*")
st.markdown(f"Showing {len(results['ids'][0])} matches out of {collection.count()} suppliers")
What does this code do?
- where_filters builds a dictionary of active filters. If the user left a dropdown on "All", that filter is skipped entirely.
- ChromaDB requires different syntax for one filter vs. multiple filters. A single filter is passed directly, but multiple filters must be wrapped in a $and clause.
- The where parameter is only added to the query when at least one filter is active. This lets the search work with or without filters.
- Save app.py. If your Streamlit app is still running, it will offer to rerun automatically.
Seeing an error when you type a search query?
- Make sure the if query: line is at the same indentation level as the sidebar code above it (no extra indentation).
- Check that supplier_db/ exists in your project folder. If you deleted it, re-run load_data.py to recreate the database.
Help me debug my ChromaDB query in Streamlit.
Display ranked results with similarity scores
Now you'll render each result as a card with three columns: company info, key metrics, and a color-coded similarity score. The score converts ChromaDB's raw distance into a percentage so employees can quickly judge match quality.
- Add the following code inside the if query: block, directly below the two st.markdown lines you just added:
for i, metadata in enumerate(results["metadatas"][0]):
distance = results["distances"][0][i]
similarity = max(0, 1 - distance)
score_percentage = int(similarity * 100)
col1, col2, col3 = st.columns([3, 2, 1])
with col1:
st.markdown(f"**{metadata['company_name']}**")
st.markdown(f"📁 {metadata['industry']} | 📍 {metadata['location']}")
with col2:
relationship_icon = "✅" if metadata["previous_relationship"] == "yes" else "❌"
st.markdown(f"🚚 {metadata['shipping_time_days']} days | 🌱 {metadata['co2_emissions_kg']} kg CO2")
st.markdown(f"⚡ Capacity: {metadata['production_capacity']} | {relationship_icon} Existing relationship")
with col3:
if score_percentage >= 70:
st.markdown(f"🟢 **{score_percentage}%**")
elif score_percentage >= 40:
st.markdown(f"🟡 **{score_percentage}%**")
else:
st.markdown(f"🔴 **{score_percentage}%**")
with st.expander(f"Details: {metadata['company_name']}"):
st.markdown(f"**Design Capabilities:** {metadata['design_capabilities']}")
st.markdown(f"**Compatible Materials:** {metadata['compatible_materials']}")
st.markdown(f"**Cost Rating:** {metadata['cost_rating']} | **Shipping Cost:** {metadata['shipping_cost_rating']}")
st.divider()
What does this code do?
- similarity = max(0, 1 - distance) converts ChromaDB's distance (lower = more similar) into a 0-100% score. A distance of 0 means a perfect match (100%).
- st.columns([3, 2, 1]) creates a three-column layout where the first column is widest (company info), the middle shows metrics, and the right column shows the score.
- The color-coded score (green at 70%+, yellow at 40%+, red below 40%) gives employees an instant visual signal about match quality.
- st.expander hides detailed information (design capabilities, materials, costs) behind a clickable toggle so results stay scannable.
Finally, add the empty-state message that shows when no query has been entered yet.
- Add this else block at the same indentation level as if query: (no indentation):
else:
st.info("Enter a search query above to find relevant suppliers.")
st.markdown(f"**{collection.count()} suppliers** in the database ready to search.")
- Save app.py.
- In your browser, click **Always rerun** if Streamlit asks, or press **R** to rerun the app manually.
- You should see the info message showing how many suppliers are in the database. Type a query like sustainable packaging and results should appear with colored similarity scores.
Results not displaying after you type a query?
- Check that the results display loop is indented inside the if query: block. Every line in the for loop should be at least 4 spaces (one level) inside the if.
- Make sure the else: is at the same level as if query: (no indentation). If it's indented, Python treats it as part of another block.
- If you see results but no similarity scores, verify the results["distances"][0][i] line uses square brackets, not parentheses.
Help me fix my Streamlit results display.
✔️ Awesome, I've got everything!
Great. Double check you've saved app.py and that your app is running in the browser with no errors.
ⓧ I'd like to double check the full code
Here's the complete app.py file. Compare it line by line with yours:
import streamlit as st
import chromadb
st.set_page_config(page_title="Supplier Search", page_icon="🔍", layout="wide")
st.title("🔍 Intelligent Supplier Search")
st.markdown("Search for suppliers using natural language. Combine with filters to narrow results.")
@st.cache_resource
def get_collection():
client = chromadb.PersistentClient(path="./supplier_db")
return client.get_collection("suppliers")
collection = get_collection()
# Sidebar filters
st.sidebar.header("Filters")
industry_filter = st.sidebar.selectbox(
"Industry",
["All", "Electronics", "Packaging", "Logistics", "Raw Materials",
"Manufacturing Equipment", "IT Services", "Cleaning Products"],
)
relationship_filter = st.sidebar.selectbox(
"Previous Relationship",
["All", "Yes", "No"],
)
max_shipping_days = st.sidebar.slider(
"Max Shipping Time (days)",
min_value=0,
max_value=30,
value=30,
)
num_results = st.sidebar.slider("Number of results", min_value=1, max_value=20, value=10)
# Main search
query = st.text_input(
"What are you looking for?",
placeholder="e.g., sustainable packaging with fast delivery in Europe",
)
if query:
# Build metadata filter
where_filters = {}
if industry_filter != "All":
where_filters["industry"] = industry_filter
if relationship_filter != "All":
where_filters["previous_relationship"] = relationship_filter.lower()
if max_shipping_days < 30:
where_filters["shipping_time_days"] = {"$lte": max_shipping_days}
# Combine multiple filters with $and if needed
where_clause = None
if len(where_filters) == 1:
key = list(where_filters.keys())[0]
where_clause = {key: where_filters[key]}
elif len(where_filters) > 1:
where_clause = {"$and": [{k: v} for k, v in where_filters.items()]}
query_params = {
"query_texts": [query],
"n_results": num_results,
}
if where_clause:
query_params["where"] = where_clause
results = collection.query(**query_params)
st.markdown(f"### Results for: *{query}*")
st.markdown(f"Showing {len(results['ids'][0])} matches out of {collection.count()} suppliers")
for i, metadata in enumerate(results["metadatas"][0]):
distance = results["distances"][0][i]
similarity = max(0, 1 - distance)
score_percentage = int(similarity * 100)
col1, col2, col3 = st.columns([3, 2, 1])
with col1:
st.markdown(f"**{metadata['company_name']}**")
st.markdown(f"📁 {metadata['industry']} | 📍 {metadata['location']}")
with col2:
relationship_icon = "✅" if metadata["previous_relationship"] == "yes" else "❌"
st.markdown(f"🚚 {metadata['shipping_time_days']} days | 🌱 {metadata['co2_emissions_kg']} kg CO2")
st.markdown(f"⚡ Capacity: {metadata['production_capacity']} | {relationship_icon} Existing relationship")
with col3:
if score_percentage >= 70:
st.markdown(f"🟢 **{score_percentage}%**")
elif score_percentage >= 40:
st.markdown(f"🟡 **{score_percentage}%**")
else:
st.markdown(f"🔴 **{score_percentage}%**")
with st.expander(f"Details: {metadata['company_name']}"):
st.markdown(f"**Design Capabilities:** {metadata['design_capabilities']}")
st.markdown(f"**Compatible Materials:** {metadata['compatible_materials']}")
st.markdown(f"**Cost Rating:** {metadata['cost_rating']} | **Shipping Cost:** {metadata['shipping_cost_rating']}")
st.divider()
else:
st.info("Enter a search query above to find relevant suppliers.")
st.markdown(f"**{collection.count()} suppliers** in the database ready to search.")
Your search interface is fully built. Next, you'll put it through its paces with different query styles and filter combinations to see how semantic search compares to keyword matching.
Test Your Intelligent Search
Your code is complete and the vector database is loaded with supplier data. Now it is time to launch the app and see how semantic search compares to traditional keyword matching.
The real power of this tool becomes clear when you type queries that describe what you need rather than the exact company name. A query like "sustainable packaging" will surface relevant suppliers even if those exact words do not appear in their company names.
In this step, get ready to:
- Run the Streamlit app and search with natural language queries.
- Combine search queries with sidebar filters to narrow results.
- Test edge cases like empty queries, broad queries, and filter-only searches.
Launch the app and test natural language queries
Semantic search finds results based on meaning, not exact keyword matches. You will test this by typing queries that describe what you need in plain English.
- Make sure your terminal is inside the supplier-search folder and your virtual environment is active.
- Launch your Streamlit app by running this command:
streamlit run app.py
Your browser opens automatically to http://localhost:8501 showing the Intelligent Supplier Search interface.
What should I see?
The page displays a title, a search bar, and a left sidebar with filter controls. Below the search bar you should see an info message and a count of your suppliers in the database.
Seeing a connection error or blank page?
Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt.
If you see an error about the collection not being found, run python load_data.py again to rebuild the database.
help me debug why my Streamlit app is not loading
Now try your first semantic search query.
- Click the search bar and type:
sustainable packaging manufacturer with fast shipping in Europe
- Press Enter to run the search.
You should see results ranked by a similarity percentage. Suppliers related to packaging, sustainability, and European locations appear at the top, even if the query words do not match their names exactly.
- Try a second query to see the difference:
low-cost electronics supplier with high production capacity
Notice how the results shift to electronics manufacturers with low cost ratings and high capacity. The search understands the intent behind your words.
Why does this work without exact keyword matches?
ChromaDB converted each supplier's description into a vector embedding. Your query also becomes a vector. The database finds suppliers whose vectors are closest to your query vector in meaning-space.
This is why "sustainable packaging" matches a supplier described as "biodegradable materials and custom box design for e-commerce fulfillment" even though those exact words differ.
Combine search queries with sidebar filters
Semantic search narrows by meaning, but sometimes you also need hard constraints. The sidebar filters let you layer structured conditions on top of natural language queries.
- Type this query into the search bar:
fast shipping electronics
- In the left sidebar, change the Previous Relationship dropdown to Yes.
- Observe how the results now only show suppliers you have worked with before.
- Drag the Max Shipping Time (days) slider down to 7.
- Watch the results update to exclude any supplier with shipping times above 7 days.
The combination of semantic search plus metadata filtering gives users precise control over their results.
- Try one more combination. Set the Industry dropdown to Raw Materials and search for:
eco-friendly supplier with low carbon footprint
- Notice how results are constrained to the Raw Materials industry while still ranked by semantic relevance to your query.
Reset your filters between tests
Set all dropdowns back to All and the shipping slider back to 30 before each new test. This ensures you see the full effect of each individual filter.
Verify edge cases
A robust search tool handles unexpected inputs gracefully. Testing edge cases confirms your app behaves correctly in all situations.
- Clear the search bar completely so it is empty.
- Verify the page shows an info message and a count of total suppliers in the database.
This confirms the empty-state logic in your code is working.
- Now try a very specific query:
CNC machines with 5-axis capabilities and operator training
- You should see PrecisionTool Co ranked highly since its description mentions those exact capabilities.
- Next, try a broad query:
supplier
- Because every entry is a supplier, results will be spread across all industries with relatively similar scores.
- Finally, test a filter-only search. Set the Industry filter to Electronics but leave the search bar empty.
- Observe that the app still shows the info message. Filters only apply when combined with a search query.
Why don't filters work without a query?
The app's logic wraps filtering inside the if query: block. Without a query, the collection.query() call never runs, so filters are not applied.
This is a design choice. In the Secret Mission, you could extend the app to support browse-with-filters mode.
Your intelligent search tool is working. You have proven that semantic search surfaces relevant suppliers by meaning, structured filters narrow results by hard constraints, and the app handles edge cases gracefully.
Ready to push this further? In the Secret Mission, you will add a supplier comparison feature that lets users select multiple results and view them side by side.
Secret mission
Add a Supplier Comparison View
Your search tool already helps employees find suppliers. But what happens when they find 3 great options and need to decide? Build a comparison feature that lets users select suppliers from their search results and view them side-by-side with highlighted best values in each category.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources on disk, pause them to come back later, or delete them entirely.
Resources you used:
- Project folder (contains suppliers.csv, load_data.py, app.py, virtual environment, and supplier_db database folder).
- Cached embedding model (the all-MiniLM-L6-v2 model downloaded by Sentence Transformers, stored in ~/.cache/huggingface/hub).
Keep everything running
No action needed. Choose this if you are still actively building or plan to extend the project soon.
- Your project folder stays on your Desktop with all files intact.
- The supplier_db folder retains your indexed supplier data so you can search immediately next time.
- The cached all-MiniLM-L6-v2 model (~90 MB) remains in ~/.cache/huggingface/hub. Other Hugging Face projects may reuse this cache.
Pause - I'll come back to this later
Stop any running processes to free up memory. Your files stay on disk so you can pick up where you left off.
- If the Streamlit app is still running in your terminal, press Ctrl+C to stop it.
- If your virtual environment is active, deactivate it by running:
deactivate
- Your project folder and database remain on disk for next time.
Delete - I don't want to use this again
Remove all project resources and start fresh. This frees up disk space but you will need to rebuild from scratch if you return.
- If the Streamlit app is still running, press Ctrl+C to stop it.
- If your virtual environment is active, deactivate it by running:
deactivate
- Delete your entire project folder (including all code, CSV data, virtual environment, and the ChromaDB database) by running:
macOS
rm -rf ~/Desktop/supplier-search
Windows
rmdir /s /q %USERPROFILE%\Desktop\supplier-search
- Delete the cached embedding model to reclaim ~90 MB of space by running:
macOS
rm -rf ~/.cache/huggingface/hub/models--sentence-transformers--all-MiniLM-L6-v2
Windows
rmdir /s /q %USERPROFILE%\.cache\huggingface\hub\models--sentence-transformers--all-MiniLM-L6-v2
- Confirm deletion by running:
ls ~/Desktop/supplier-search
- You should see a "No such file or directory" message confirming the folder is gone.
That's a Wrap!
That's a Wrap!
Nice work! You just built an intelligent supplier search tool that finds results by meaning, not just keywords.
You've learned how to:
- Built a ChromaDB vector database populated with 200+ suppliers and rich metadata, powered by automatic semantic embeddings that understand meaning.
- Created a Streamlit web interface that combines natural language search with structured sidebar filters for industry, location, and shipping time.
- Tested semantic search queries and saw how they outperform keyword matching for finding the right supplier.
- Added a supplier comparison view that lets users select and compare suppliers side by side.
Ready to quiz yourself?