Skip to content

Commit 0eae9ad

Browse files
mmacphersonclaude
andcommitted
feat: add semantic icon search with VLM descriptions and embeddings
Add embedding-based search so users can find Lucide icons by natural language queries ("payment", "hard work", "owl") instead of exact name matching. Architecture: - Gemini 2.5 Flash Lite generates rich text descriptions from rendered icon PNGs + Lucide metadata (tags, categories) at build time - nomic-embed-text-v1.5-Q computes embeddings with asymmetric search_query/search_document prefixes - Descriptions saved as JSONL (durable source of truth), embeddings stored in a separate SQLite search DB - Search DB auto-downloaded on first use, cached locally - Lucide repo auto-cloned for metadata during description generation New modules: - search.py: public API (search_icons, search_available, SearchResult) - build_search.py: VLM + embedding build pipeline with JSONL intermediate - build_clusters.py: HDBSCAN clustering + Gemini Flash theme naming - cli.py: unified `lucide` CLI with subcommands (db, describe, build-search, search, cluster, version) Main DB now includes relational metadata: - icon_tags (12,619 rows), icon_categories (3,309), icon_aliases (248) - All indexed for fast lookup CLI search with inline icon rendering via Kitty graphics protocol (Ghostty, kitty, WezTerm) with white background for visibility. Optional extra: pip install python-lucide[search] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 783d542 commit 0eae9ad

12 files changed

Lines changed: 2305 additions & 186 deletions

File tree

.github/workflows/update-lucide.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,24 @@ jobs:
129129
exit 1
130130
fi
131131
132+
- name: Generate search data
133+
if: steps.version-check.outputs.needs-update == 'true' && steps.check-pr.outputs.pr-exists == 'false'
134+
env:
135+
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
136+
run: |
137+
echo "🔍 Generating semantic search data for new icons..."
138+
make search-data
139+
SEARCH_DB=src/lucide/data/lucide-search.db
140+
if [ -f "$SEARCH_DB" ]; then
141+
DESC_COUNT=$(sqlite3 "$SEARCH_DB" "SELECT COUNT(*) FROM icon_descriptions;")
142+
EMB_COUNT=$(sqlite3 "$SEARCH_DB" "SELECT COUNT(*) FROM icon_embeddings;")
143+
ICON_COUNT=$(sqlite3 src/lucide/data/lucide-icons.db "SELECT COUNT(*) FROM icons;")
144+
echo "✅ Search data: $DESC_COUNT descriptions, $EMB_COUNT embeddings (of $ICON_COUNT icons)"
145+
echo "search-db-size=$(du -h "$SEARCH_DB" | cut -f1)" >> $GITHUB_OUTPUT
146+
else
147+
echo "⚠️ Search database not created (GEMINI_API_KEY may not be set)"
148+
fi
149+
132150
- name: Run tests
133151
if: steps.version-check.outputs.needs-update == 'true' && steps.check-pr.outputs.pr-exists == 'false'
134152
run: |
@@ -143,6 +161,10 @@ jobs:
143161
git config --local user.name "github-actions[bot]"
144162
145163
git add src/lucide/config.py src/lucide/data/lucide-icons.db pyproject.toml .github/lucide-version.json
164+
# Include search DB if it was generated
165+
if [ -f src/lucide/data/lucide-search.db ]; then
166+
git add src/lucide/data/lucide-search.db
167+
fi
146168
git commit -m "feat: Update Lucide icons to v${{ steps.version-check.outputs.latest-version }}
147169
148170
Bumps package version to ${{ steps.version-bump.outputs.new-pkg-version }}.

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,8 @@ cython_debug/
175175

176176
# Claude Code local settings
177177
.claude/
178+
179+
# Beads / Dolt files (added by bd init)
180+
.dolt/
181+
*.db
182+
.beads-credential-key

Makefile

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@
44
UV_CMD := uv
55
PRE_COMMIT_CMD := $(UV_CMD) run pre-commit
66
PYTEST_CMD := $(UV_CMD) run pytest
7-
LUCIDE_DB_CMD := $(UV_CMD) run lucide-db
7+
LUCIDE_CMD := $(UV_CMD) run lucide
88

99
# Get default Lucide tag from the package's config.py
1010
PYTHON_CMD_FOR_TAG := $(UV_CMD) run python -c "from lucide.config import DEFAULT_LUCIDE_TAG; print(DEFAULT_LUCIDE_TAG)"
1111
DEFAULT_LUCIDE_TAG := $(shell $(PYTHON_CMD_FOR_TAG))
1212
# Allow overriding the tag via make argument, e.g., make db TAG=0.500.0
1313
TAG ?= $(DEFAULT_LUCIDE_TAG)
1414
DB_OUTPUT_PATH := src/lucide/data/lucide-icons.db
15+
SEARCH_DB_OUTPUT_PATH := src/lucide/data/lucide-search.db
16+
DESCRIPTIONS_JSONL := src/lucide/data/gemini-icon-descriptions.jsonl
1517
VENV_DIR := .venv
1618

1719
# Phony targets prevent conflicts with files of the same name.
18-
.PHONY: help default env lucide-db test install-hooks run-hooks-all-files check-lucide-version clean nuke
20+
.PHONY: help default env lucide-db describe build-search search-data lucide-db-full test install-hooks run-hooks-all-files check-lucide-version clean nuke
1921

2022
# Default target
2123
default: help
@@ -31,6 +33,10 @@ help:
3133
@echo " lucide-db (Re)builds the Lucide icon database into $(DB_OUTPUT_PATH)."
3234
@echo " Uses TAG=$(TAG). Default TAG is read from src/lucide/config.py (currently $(DEFAULT_LUCIDE_TAG))."
3335
@echo " Example: make lucide-db TAG=0.520.0"
36+
@echo " describe Generate icon descriptions via VLM (requires GEMINI_API_KEY)."
37+
@echo " build-search Build search SQLite DB from descriptions JSONL."
38+
@echo " search-data Run describe + build-search."
39+
@echo " lucide-db-full Build icons database + search data."
3440
@echo " test Run tests using pytest."
3541
@echo " install-hooks Install pre-commit hooks."
3642
@echo " update-hooks Update pre-commit hooks to latest version."
@@ -52,9 +58,21 @@ $(VENV_DIR)/pyvenv.cfg:
5258
lucide-db:
5359
@echo "Building Lucide icon database with tag $(TAG) into $(DB_OUTPUT_PATH)..."
5460
@mkdir -p src/lucide/data # Ensure data directory exists
55-
$(LUCIDE_DB_CMD) -o $(DB_OUTPUT_PATH) -t $(TAG) -v
61+
$(LUCIDE_CMD) db -o $(DB_OUTPUT_PATH) -t $(TAG) -v
5662
@echo "Database build complete: $(DB_OUTPUT_PATH)"
5763

64+
describe:
65+
@echo "Generating icon descriptions..."
66+
$(LUCIDE_CMD) describe --icons-db $(DB_OUTPUT_PATH) -o $(DESCRIPTIONS_JSONL) -v
67+
68+
build-search:
69+
@echo "Building search DB from descriptions..."
70+
$(LUCIDE_CMD) build-search --descriptions-file $(DESCRIPTIONS_JSONL) -o $(SEARCH_DB_OUTPUT_PATH) -v
71+
72+
search-data: describe build-search
73+
74+
lucide-db-full: lucide-db search-data
75+
5876
test:
5977
@echo "Running tests..."
6078
$(PYTEST_CMD)

pyproject.toml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ requires = ["uv_build>=0.8.3,<0.12.0"]
44

55
[dependency-groups]
66
dev = [
7+
"cairosvg>=2.7.0",
8+
"fastembed>=0.4.0",
9+
"hdbscan>=0.8.0",
710
"mypy-extensions>=1.0.0",
811
"mypy>=1.0.0",
12+
"plotly>=5.0.0",
913
"pre-commit>=3.0.0",
1014
"pytest-cov>=4.0.0",
1115
"pytest>=7.0.0",
1216
"ruff>=0.1.0",
1317
"types-setuptools>=80.9.0.20250529",
14-
"typing-extensions>=4.1.0"
18+
"typing-extensions>=4.1.0",
19+
"umap-learn>=0.5.0"
1520
]
1621

1722
[project]
@@ -37,9 +42,13 @@ readme = "README.md"
3742
requires-python = ">=3.10"
3843
version = "0.2.24"
3944

45+
[project.optional-dependencies]
46+
search = ["fastembed>=0.4.0"]
47+
4048
[project.scripts]
4149
check-lucide-version = "lucide.dev_utils:print_version_status"
42-
lucide-db = "lucide.cli:main"
50+
lucide = "lucide.cli:main"
51+
lucide-db = "lucide.cli:main_legacy_db"
4352

4453
[project.urls]
4554
"Bug Tracker" = "https://github.com/mmacpherson/python-lucide/issues"
@@ -126,7 +135,8 @@ section-order = [
126135

127136
[tool.ruff.lint.per-file-ignores]
128137
"**/__init__.py" = ["F401"]
129-
"tests/**/*.py" = ["D", "SLF001"]
138+
"scripts/**/*.py" = ["D", "SLF001", "PLR2004", "PLR0915", "PLC0415", "ERA001", "E741", "C408"]
139+
"tests/**/*.py" = ["D", "SLF001", "PLR2004", "ARG005"]
130140

131141
[tool.ruff.lint.pydocstyle]
132142
convention = "google"

src/lucide/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@
88

99
from .core import create_placeholder_svg, get_icon_list, lucide_icon
1010
from .db import get_db_connection, get_default_db_path
11+
from .search import (
12+
SearchNotAvailableError,
13+
SearchResult,
14+
search_available,
15+
search_icons,
16+
)
1117

1218
__all__ = [
19+
"SearchNotAvailableError",
20+
"SearchResult",
1321
"create_placeholder_svg",
1422
"get_db_connection",
1523
"get_default_db_path",
1624
"get_icon_list",
1725
"lucide_icon",
26+
"search_available",
27+
"search_icons",
1828
]

0 commit comments

Comments
 (0)