Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint (ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install ruff
- run: ruff check DeepImageSearch tests scripts

test:
name: Test (${{ matrix.os }}, py${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install CPU-only torch
# The default torch wheel pulls CUDA on Linux; the CPU index keeps CI small and fast.
run: pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
if: runner.os == 'Linux'

- name: Install package and test dependencies
# The extras are installed so the Chroma/Qdrant/LangChain/MCP suites
# actually run instead of skipping. Postgres is covered with a fake
# driver, so no server is needed.
run: pip install -e ".[dev,chroma,qdrant,langchain,mcp,llm]" pytest-cov

- name: Run tests
run: pytest --cov=DeepImageSearch --cov-report=xml --cov-report=term-missing --cov-fail-under=90

- name: Upload coverage
uses: actions/upload-artifact@v4
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
with:
name: coverage
path: coverage.xml

build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build twine
- run: python -m build
- name: Check package metadata
run: twine check dist/*
77 changes: 77 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Release

# Publishes to PyPI when a v* tag is pushed:
# python scripts/bump_version.py 3.0.3
# git commit -am "Bump version to 3.0.3" && git push
# git tag v3.0.3 && git push --tags
#
# Authentication uses PyPI Trusted Publishing (OIDC) — no API token is stored
# in GitHub secrets. One-time setup on PyPI, under the project's
# "Publishing" settings, add a trusted publisher with:
# Owner: TechyNilesh
# Repository: DeepImageSearch
# Workflow: release.yml
# Environment: pypi
# Then create a GitHub environment named "pypi" (Settings > Environments).

on:
push:
tags: ["v*"]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
name: Build and verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Check the tag matches the packaged version
# Publishing 3.0.2 from a v3.0.3 tag is unrecoverable — PyPI does not
# allow reuploading a version — so fail before the build instead.
if: startsWith(github.ref, 'refs/tags/v')
run: |
tag_version="${GITHUB_REF_NAME#v}"
pkg_version="$(python -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
echo "tag=$tag_version pyproject=$pkg_version"
if [ "$tag_version" != "$pkg_version" ]; then
echo "::error::Tag $GITHUB_REF_NAME does not match pyproject version $pkg_version. Run scripts/bump_version.py first."
exit 1
fi

- name: Install build tooling
run: pip install build twine

- name: Build distributions
run: python -m build

- name: Check package metadata
run: twine check dist/*

- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # required for trusted publishing
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Publish
uses: pypa/gh-action-pypi-publish@release/v1
45 changes: 45 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Byte-compiled / cache
__pycache__/
*.py[cod]
*$py.class

# Distribution / packaging
build/
dist/
*.egg-info/
*.egg
.eggs/

# Virtual environments
.venv/
venv/
env/
ENV/

# Testing / coverage
.pytest_cache/
.coverage
.coverage.*
coverage.xml
htmlcov/
.tox/
.nox/

# Linting / type checking
.ruff_cache/
.mypy_cache/

# Jupyter
.ipynb_checkpoints/

# Index artefacts written by DeepImageSearch at runtime
metadata-files/
*.faiss
image_records.json

# OS / editor cruft
.DS_Store
Thumbs.db
*.swp
.idea/
.vscode/
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,49 @@ All notable changes to DeepImageSearch will be documented in this file.

---

## [Unreleased]

### Fixed
- **`index_type="ivf"` segfaulted the interpreter on every use.** Two causes:
the FAISS quantizer was garbage-collected while the index still pointed at it,
and FAISS's OpenMP k-means clashed with torch's runtime on macOS.
- **macOS:** searching no longer aborts with `OMP: Error #15`. `torch` and
`faiss-cpu` vendor separate copies of `libomp.dylib`; DeepImageSearch now sets
`KMP_DUPLICATE_LIB_OK` at import and pins FAISS to one thread when it detects
the duplicate (see `DEEPIMAGESEARCH_FAISS_THREADS`). Import `DeepImageSearch`
before `torch`/`faiss` for it to take effect.
- **`create_langchain_tool()` raised `NameError` on every call** — a pydantic
field shadowed the `k` parameter inside the class body, so the LangChain
integration could never be constructed.
- **MCP server failed to start against mcp >= 2.0**, which renamed `FastMCP` to
`MCPServer`; both import paths are now supported.
- **Qdrant search failed against qdrant-client >= 1.12**, which removed
`QdrantClient.search()` in favour of `query_points()`.
- `ChromaStore.add()` rejected vectors added without metadata, and returned
`None` instead of `{}` for their metadata on search.
- `FAISSStore.delete()` no longer raises `ValueError` when every vector is deleted.
- `PostgresMetadataStore.__del__` no longer raises `AttributeError` when the
connection was never established.
- `DeepImageSearch.__version__` now matches the packaged version (was `3.0.0`).

### Added
- Test suite (`tests/`, 353 tests, 98% coverage) covering every module: loader,
metadata stores, all four vector stores, embedding backends, captioner, agent
tools, the SearchEngine facade, and the v2 `Search_Setup` shim. No model
downloads and no servers — heavy dependencies are faked or run in-process.
- GitHub Actions CI: lint, tests on Python 3.10–3.13 across Linux/macOS/Windows
with all extras installed and a 90% coverage floor, and a packaging check.
- `CONTRIBUTING.md`, `CITATION.cff`, `.gitignore`.
- Release workflow publishing to PyPI from a `v*` tag via Trusted
Publishing (OIDC, no stored token), guarded by a tag/version match check.
- `scripts/bump_version.py`, which updates all three places the version is
recorded and fails loudly if any of them stops matching.
- `# SPDX-License-Identifier: MIT` header on every source file.

### Removed
- `setup.cfg`, which referenced a non-existent `README.rst`; packaging is
handled entirely by `pyproject.toml`.

## [3.0.0] - 2026-03-29

### Complete rewrite for the agentic RAG / LLM era.
Expand Down
25 changes: 25 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
cff-version: 1.2.0
title: "DeepImageSearch: AI-Based Image Search Engine"
message: "If you use this software, please cite it using these metadata."
type: software
authors:
- family-names: Verma
given-names: Nilesh
email: me@nileshverma.com
repository-code: "https://github.com/TechyNilesh/DeepImageSearch"
url: "https://github.com/TechyNilesh/DeepImageSearch"
abstract: >-
DeepImageSearch is an AI-powered image search engine with multimodal
embeddings (CLIP, SigLIP, EVA-CLIP, timm), text-to-image and hybrid
search, pluggable vector stores (FAISS, ChromaDB, Qdrant), LLM
captioning, and agentic RAG integration via MCP and LangChain tools.
keywords:
- image search
- multimodal embeddings
- CLIP
- vector search
- information retrieval
- machine learning
license: MIT
version: 3.0.2
date-released: "2026-04-01"
112 changes: 112 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Contributing to DeepImageSearch

Thanks for your interest in improving DeepImageSearch. Bug reports, documentation
fixes, new backends, and test coverage are all welcome.

## Development setup

```bash
git clone https://github.com/TechyNilesh/DeepImageSearch.git
cd DeepImageSearch
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
```

Add the extras you need for the backend you are working on, e.g.
`pip install -e ".[dev,chroma,qdrant]"`.

## Running the checks

```bash
pytest # full suite, no model downloads, ~1s
pytest --cov=DeepImageSearch # with coverage
ruff check DeepImageSearch tests # lint
```

CI runs the same three commands on Python 3.10–3.13 across Linux, macOS, and
Windows. A pull request should be green on all of them.

**macOS note:** `torch` and `faiss-cpu` each vendor their own copy of
`libomp.dylib`. Loading both is unsupported and fails two ways: the process
aborts with `OMP: Error #15`, or — once that abort is suppressed — FAISS's
OpenMP-parallel routines segfault instead (IVF k-means training is the usual
casualty). `DeepImageSearch/_openmp.py` handles both: it sets
`KMP_DUPLICATE_LIB_OK` at import time and pins FAISS to a single thread when it
detects two distinct runtimes.

Two caveats. It only takes effect if `DeepImageSearch` is imported *before*
`torch` or `faiss`. And single-threaded FAISS is slower on large indexes — the
module's docstring documents how to remove the duplicate runtime for real, after
which `DEEPIMAGESEARCH_FAISS_THREADS=0` restores full multithreading.

## Writing tests

Tests must not download model weights — CI runs twelve jobs and cannot afford
it. Use the `embedding` fixture from `tests/conftest.py`, a deterministic
stand-in for CLIP that supports both text and image queries, or monkeypatch the
backend as `tests/test_embeddings.py` does. Anything genuinely requiring weights
belongs behind an explicit opt-in marker.

## Adding a backend

The pluggable layers are all defined by abstract base classes:

| Extension point | Base class | Existing implementations |
|---|---|---|
| Vector store | `DeepImageSearch/vectorstores/base.py` | FAISS, ChromaDB, Qdrant |
| Metadata store | `DeepImageSearch/metadatastore/base.py` | JSON, PostgreSQL |
| Embedding | `DeepImageSearch/core/embeddings.py` | CLIP, timm, custom callable |

To add one:

1. Subclass the relevant base and implement every abstract method.
2. Import it in the package's `__init__.py` inside a `try/except ImportError`
so the dependency stays optional, and append it to `__all__`.
3. Declare the dependency as a new extra in `pyproject.toml`.
4. Add tests. The interface-conformance tests at the bottom of
`tests/test_vectorstores.py` and `tests/test_metadata_store.py` show the
pattern; skip the suite when the optional dependency is absent.
5. Document it in `Documents/` and add a demo to `Demo/` if it changes usage.

## Pull requests

- Branch from `main` and keep each PR to one logical change.
- Every new source file needs the `# SPDX-License-Identifier: MIT` header.
- Update `CHANGELOG.md` under an "Unreleased" heading.
- Version numbers live in `pyproject.toml`, `DeepImageSearch/__init__.py`, and
`CITATION.cff`; `tests/test_package.py` asserts all three agree.

## Releasing (maintainers)

Releases publish to PyPI from a `v*` tag via
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no API token
lives in GitHub secrets.

```bash
python scripts/bump_version.py 3.0.3 # updates all three version locations
# add a 3.0.3 section to CHANGELOG.md
git commit -am "Bump version to 3.0.3"
git tag v3.0.3
git push && git push --tags # triggers .github/workflows/release.yml
```

The workflow refuses to build if the tag does not match the version in
`pyproject.toml`, because a wrong version cannot be re-uploaded to PyPI.

One-time setup, if it has not been done yet:

1. On PyPI, under the project's **Publishing** settings, add a trusted publisher —
owner `TechyNilesh`, repository `DeepImageSearch`, workflow `release.yml`,
environment `pypi`.
2. In GitHub **Settings → Environments**, create an environment named `pypi`.
Adding a required reviewer there gives you a manual approval gate before any
upload.

## Reporting bugs

Open an issue at https://github.com/TechyNilesh/DeepImageSearch/issues with your
OS, Python version, DeepImageSearch version, the backend in use, and a minimal
reproduction.

By contributing you agree that your contributions are licensed under the MIT
License.
Loading
Loading