Search Service¶
Carbon Connect uses Meilisearch as its primary full-text search engine with an automatic PostgreSQL fallback for resilience.
Source: backend/app/services/meilisearch_service.py
Architecture¶
flowchart LR
A[API Request] --> B{Meilisearch<br/>Available?}
B -->|Yes| C[Meilisearch<br/>sub-100ms]
B -->|No| D[PostgreSQL ILIKE<br/>Fallback]
C --> E[Search Results]
D --> E The search_grants_with_fallback() function transparently tries Meilisearch first and falls back to PostgreSQL if Meilisearch is unavailable or returns an error.
Meilisearch Integration¶
Index Configuration¶
The service manages a grants index with the following configuration:
Searchable Attributes¶
Fields that Meilisearch indexes for full-text search, listed in priority order:
titledescriptionsummarythematic_areascarbon_categories
Filterable Attributes¶
Fields that support faceted filtering:
countriesnace_codescompany_sizesstatussourceis_carbon_focusedcarbon_categoriesis_activegrant_typeeu_taxonomy_aligned
Sortable Attributes¶
Fields that support sorting:
deadlinefunding_amount_minfunding_amount_maxcreated_atupdated_at
Index Management¶
from backend.app.services.meilisearch_service import MeilisearchService
service = MeilisearchService()
# Create or update the grants index
await service.setup_grants_index()
# Index a batch of grants
await service.index_grants(grants)
# Delete a grant from the index
await service.delete_grant(grant_id)
# Full reindex
await service.reindex_all_grants(db_session)
Search API¶
The search function supports filtering, pagination, and sorting:
results = await service.search_grants(
query="renewable energy",
filters={
"countries": ["DE", "FR"],
"is_carbon_focused": True,
"status": "active",
},
sort=["deadline:asc"],
page=1,
per_page=20,
)
Fallback Strategy¶
search_grants_with_fallback()¶
This is the primary search function used by the API. It provides automatic degradation:
async def search_grants_with_fallback(
query: str,
db: AsyncSession,
filters: dict | None = None,
sort_by: str = "deadline",
page: int = 1,
per_page: int = 20,
) -> SearchResult:
"""
Search grants with Meilisearch, falling back to PostgreSQL.
1. Try Meilisearch for sub-100ms full-text search
2. If Meilisearch fails, fall back to PostgreSQL ILIKE
3. Return normalized results from either source
"""
When Fallback Activates¶
The PostgreSQL fallback is used when:
- Meilisearch service is not configured (no
MEILI_HOSTset) - Meilisearch is unreachable (connection error)
- Meilisearch returns an error (index not found, invalid filter)
- Request timeout (configurable, default 5 seconds)
PostgreSQL Fallback Implementation¶
The fallback uses ILIKE queries on title and description columns:
SELECT * FROM grants
WHERE (title ILIKE '%query%' OR description ILIKE '%query%')
AND is_active = true
AND country = ANY(:countries)
ORDER BY deadline ASC
LIMIT :per_page OFFSET :offset
Performance Difference
Meilisearch delivers sub-100ms search latency for 100k+ documents. PostgreSQL ILIKE queries are slower but still functional for moderate dataset sizes. The fallback ensures the search feature remains available even without Meilisearch.
API Endpoints¶
| Method | Endpoint | Description | Search Engine |
|---|---|---|---|
GET | /api/v1/grants/search | Full-text search with facets | Meilisearch (with fallback) |
GET | /api/v1/grants/search/simple | Simple text search | PostgreSQL ILIKE only |
GET | /api/v1/grants | List grants with filters | PostgreSQL |
Index Maintenance¶
The Meilisearch index is maintained through scheduled tasks:
| Task | Schedule | Description |
|---|---|---|
reindex_search | Saturday 5:00 AM UTC | Full rebuild of the search index |
| Grant sync tasks | Various | Incremental updates after new grants are fetched |
Manual Reindex¶
To manually trigger a full reindex:
# Via Celery task
poetry run celery -A backend.app.worker.celery_app call \
backend.app.worker.tasks.maintenance_tasks.reindex_search
# Via Python
from backend.app.services.meilisearch_service import MeilisearchService
service = MeilisearchService()
await service.reindex_all_grants(db_session)
Configuration¶
| Environment Variable | Description | Default |
|---|---|---|
MEILI_HOST | Meilisearch URL | http://localhost:7700 |
MEILI_MASTER_KEY | Meilisearch API key | Dev key (change in prod) |
Local Development¶
Meilisearch runs as a Docker container: