# A Meme Search Engine built to self-host in Python, Ruby, and Docker [](https://discord.gg/7xsxU4ZG6A) Use AI to index your memes by their content and text, making them easily retrievable for your meme warfare pleasures. By default, processing from image-to-text extraction, to vector embedding, to search is performed locally. You can also use an OpenAI-compatible vision API for description generation while keeping embeddings and search local.
To start indexing your own memes, first adjust the [compose file](https://github.com/neonwatty/meme-search/blob/main/docker-compose.yml) by adding `volume` mount to the `meme_search` and `image_to_text_generator` services to properly connect your local meme subdirectory to the app.
For example, if suppose (one of your) meme directories was called `new_memes` and was located at the following path on your machine: `/local/path/to/my/memes/new_memes`.
To properly mount this subdirectory to the `meme_search` service adjust the `volumes` portion of its configuration to the following:
```yaml
volumes:
- ./meme_search/memes/:/app/public/memes # <-- example meme directory from the repository
- /local/path/to/my/memes/new_memes/:/rails/public/memes/new_memes # <-- personal meme collection - must be placed inside /rails/public/memes in the container
```
Note: your `new_memes` directory must be mounted internally in the `/rails/public/memes` directory, as shown above.
To properly mount this same subdirectory to the `image_to_text_generator` service adjust the `volumes` portion of its configuration to the following:
```yaml
volumes:
- ./meme_search/memes/:/app/public/memes # <-- example meme directory from the repository
- /local/path/to/my/memes/new_memes/:/app/public/memes/new_memes # <-- personal meme collection - must be placed inside /app/public/memes in the container
...
```
Note: your `new_memes` directory must be mounted internally in the `/app/public/memes` directory, as shown above.
If you are concerned about the application altering your existing meme library, as a precaution you can make the mount read only by adding "ro" to the volume line as follows:
```yaml
volumes:
- ./meme_search/memes/:/app/public/memes # <-- example meme directory from the repository
- /local/path/to/my/memes/new_memes/:/app/public/memes/new_memes:ro
...
```
Read-only mounts can be indexed and searched, but the Delete action in the web UI cannot remove their source files. The UI reports a clear error and keeps the database record when the mount is not writable.
Now restart the app, and register the `new_memes` via the UX by traversing to the `settings -> paths -> create new` as illustrated below. Type in `new_memes` in the field provided and press `enter`.
Once registered in the app, your memes are ready for indexing / tagging / etc.,!
### Model downloads
The image-to-text models used to auto generate descriptions for your memes are all open source, and vary in size.
### Custom bind address and app port
Easily customize the app's port to more easily use the it with tools like [Unraid](https://unraid.net/?srsltid=AfmBOorvWvSZbCHKnqdR__AcllotnsLR6did_FhAaNfUowqqU2IprD1v) or [Portainer](https://www.portainer.io/), or because you already have services running on the default `meme_search` app port `3000`.
To customize the bind address or main app port, copy `.env.example` to `.env` in the repository root and adjust:
```sh
APP_BIND_ADDRESS=127.0.0.1 # use 0.0.0.0 only on a trusted network
APP_PORT=3000
```
This value is automatically detected and loaded into each service via the Compose files. The Postgres service is only exposed on Docker's internal network, so app containers always talk to it at `meme-search-db:5432`.
### Building the app locally with Docker
Docker images are built manually before a release because multi-platform model builds are resource intensive. The release workflow verifies both `latest` images carry the exact release commit revision before it creates versioned tags, preventing a Git tag from pointing at images built from different source.
Maintainers should follow the [`RELEASING.md`](RELEASING.md) checklist.
To build the app - including all services defined in the `docker-compose.yml` file - locally run the local compose file at your terminal as
```sh
docker compose -f docker-compose-local-build.yml up --build
```
For multi-platform builds (AMD64 + ARM64) and pushing to GitHub Container Registry, use the local build script:
```sh
bash scripts/build_and_push.sh
```
This will build the docker images for the app, database, and auto description generator, and start the app at `http://localhost:3000`.
### Running tests
To run tests locally pull the repo and cd into the `meme_search/meme_search/meme_search_app` directory. Install the required gems as
```sh
bundle install
```
Tests can then be run as
```sh
bash run_tests.sh
```
When doing this ensure you have an available Postgres instance running locally on port `5432`.
Run linting tests on the `/app` subdirectory as
```sh
rubocop app
```
to ensure the code is clean and well formatted.
#### Running CI Locally (Optional)
You can run the complete GitHub Actions CI workflow locally using [act](https://github.com/nektos/act):
```bash
# Install act (macOS)
brew install act
# Run all CI jobs
act --container-architecture linux/amd64 -P ubuntu-latest=catthehacker/ubuntu:act-latest
# Run specific job
act -j pro_app_unit_tests --container-architecture linux/amd64 -P ubuntu-latest=catthehacker/ubuntu:act-latest
```
This validates your changes match CI before pushing to GitHub.
#### Docker E2E Tests (Local Validation Only)
**Docker E2E tests validate the complete microservices stack** (Rails + Python + PostgreSQL) in isolated Docker containers. These tests run against fresh Docker builds and test cross-service communication, webhooks, and production-like deployment.
**Current Status**: 6/7 smoke tests passing (85% coverage) - see `playwright-docker/README.md` for details
```bash
# Run all Docker E2E tests
npm run test:e2e:docker
# Run with UI mode (recommended for debugging)
npm run test:e2e:docker:ui
```
**What these tests cover**:
- Complete image processing pipeline (Rails → Python → Rails webhooks)
- Vector search with embedding generation
- Keyword search functionality
- Concurrent processing and job queueing
- Embedding refresh operations
**Important**: These tests **DO NOT run in CI** due to Docker build time (~10-15 minutes) and resource requirements. **Contributors MUST run these tests locally** before submitting PRs that affect:
- Docker configurations
- Cross-service communication
- Image-to-text generation workflow
- Embedding generation
See `playwright-docker/README.md` for comprehensive documentation.
## Discord server
Join our Discord server [](https://discord.gg/7xsxU4ZG6A) to discuss new features, bug fixes, and other open source projects (like [ytgify](https://chromewebstore.google.com/detail/ytgify/dnljofakogbecppbkmnoffppkfdmpfje) - a browser extension for clipping GIFs from YouTube right from the YT Player!).
## Changelog
Meme Search is under active development! See the `CHANGELOG.md` in this repo for a record of the most recent changes.
## Feature requests and contributing
Feature requests and contributions are welcome!
See [the discussion section of this repository](https://github.com/neonwatty/meme-search/discussions) for suggested enhancements to contribute to or weigh in on.
Please see [`CONTRIBUTING.md`](CONTRIBUTING.md) for setup, testing, and contribution guidance. Security-sensitive reports should follow [`SECURITY.md`](SECURITY.md) instead of being filed publicly.
Below is a nice diagram of the repo [generated using gitdiagram](https://github.com/ahmedkhaleel2004/gitdiagram), laying out its main components and interactions.
```mermaid
flowchart TD
%% Global Entities
User["User"]:::user
%% Docker & Compose Orchestration
Docker["Docker & Compose Orchestration"]:::docker
%% Main Services
Rails["Rails Meme Search Application"]:::rails
Python["Image-to-Text Generator (Python)"]:::python
DB["PostgreSQL Database (with pgvector)"]:::database
%% Shared File Volumes Subgraph
subgraph "Shared Meme Files"
PublicMemes["Public Memes"]:::volume
MemeDir["Meme Directory"]:::volume
end
%% Interactions
User -->|"interaction"| Rails
Rails -->|"DBQueryUpdate"| DB
Rails -->|"APIRequest"| Python
Python -->|"APIResponse"| Rails
%% Volume Access
Rails ---|"VolumeMountAccess"| PublicMemes
Python ---|"VolumeMountAccess"| MemeDir
%% Docker Orchestration Links
Docker ---|"orchestrates"| Rails
Docker ---|"orchestrates"| Python
Docker ---|"orchestrates"| DB
%% Click Events
click Rails "https://github.com/neonwatty/meme-search/tree/main/meme_search/meme_search_app"
click Python "https://github.com/neonwatty/meme-search/tree/main/meme_search/image_to_text_generator"
click DB "https://github.com/neonwatty/meme-search/blob/main/meme_search/meme_search_app/config/database.yml"
click Docker "https://github.com/neonwatty/meme-search/blob/main/docker-compose.yml"
click PublicMemes "https://github.com/neonwatty/meme-search/tree/main/meme_search/meme_search_app/public/memes"
click MemeDir "https://github.com/neonwatty/meme-search/tree/main/meme_search/memes"
%% Styles
classDef user fill:#fceabb,stroke:#d79b00,stroke-width:2px;
classDef rails fill:#c8e6c9,stroke:#388e3c,stroke-width:2px;
classDef python fill:#bbdefb,stroke:#1976d2,stroke-width:2px;
classDef database fill:#ffe082,stroke:#f9a825,stroke-width:2px,stroke-dasharray: 5 5;
classDef docker fill:#d1c4e9,stroke:#673ab7,stroke-width:2px,stroke-dasharray: 3 3;
classDef volume fill:#ffcdd2,stroke:#e53935,stroke-width:2px,stroke-dasharray: 2 2;
```