Using meilisearch with a fastapi frontend

Installation

pip install fastapi uvicorn firebase-admin httpx

Server Code

import os
import time
from typing import Optional, Dict, List
from fastapi import FastAPI, Header, HTTPException, status
from pydantic import BaseModel
import firebase_admin
from firebase_admin import credentials, auth
import httpx

# 1. Initialize Firebase Admin SDK for local, high-speed token verification
# Assumes GOOGLE_APPLICATION_CREDENTIALS environment variable is set
firebase_admin.initialize_app()

app = FastAPI()

# Configuration Settings from environment variables
MEILISEARCH_URL = os.getenv("MEILISEARCH_URL", "http://127.0.0.1:7700")
MEILI_SEARCH_KEY = os.getenv("MEILI_SEARCH_KEY")
APP_SECRET_KEY = os.getenv("APP_SECRET_KEY") # Matches Flutter Envied key

# In-memory dictionary tracking user timestamps for rate limiting
# Key: uid (string), Value: List of timestamps (floats)
rate_limit_cache: Dict[str, List[float]] = {}

# Define request schema validation using Pydantic
class SearchRequest(BaseModel):
    q: str
    limit: Optional[int] = 20
    offset: Optional[int] = 0
    filter: Optional[str] = ""

@app.post("/search")
async def secure_search(
    request_data: SearchRequest,
    x_app_key: Optional[str] = Header(None),
    authorization: Optional[str] = Header(None)
):
    # 1. App Secret Validation (Blocks unauthorized API scripts)
    if not x_app_key or x_app_key != APP_SECRET_KEY:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, 
            detail="Unauthorized Application Key"
        )

    # 2. Firebase Token Structure Check (Accepts both logged-in & anonymous tokens)
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, 
            detail="Missing or malformed session token"
        )
    
    id_token = authorization.split("Bearer ")[1]
    
    try:
        # Decodes and verifies the signature locally against public keys cached in memory
        decoded_token = auth.verify_id_token(id_token)
        uid = decoded_token["uid"]
    except Exception:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED, 
            detail="Invalid session token"
        )

    # 3. User-Based Rate Limiting (Using Firebase uid)
    now = time.time()
    user_requests = rate_limit_cache.get(uid, [])
    
    # Filter out requests older than 1 minute (60 seconds)
    active_requests = [t for t in user_requests if now - t < 60.0]
    
    if len(active_requests) >= 60: # Limit: 60 searches per minute per user
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="Too many search requests. Please slow down."
        )
        
    active_requests.append(now)
    rate_limit_cache[uid] = active_requests

    # 4. Input Length Sanitisation
    if len(request_data.q) > 100:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Query string exceeds maximum length limit"
        )

    # 5. Non-blocking Asynchronous Request to local Meilisearch
    async with httpx.AsyncClient() as client:
        try:
            meili_response = await client.post(
                f"{MEILISEARCH_URL}/indexes/products/search",
                headers={
                    "Content-Type": "application/json",
                    "Authorization": f"Bearer {MEILI_SEARCH_KEY}"
                },
                json={
                    "q": request_data.q,
                    "limit": request_data.limit,
                    "offset": request_data.offset,
                    "filter": request_data.filter
                },
                timeout=5.0
            )
            return meili_response.json()
        except httpx.RequestError:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Search engine interface error"
            )

Deployment

uvicorn main:app --host 127.0.0.1 --port 8000 --workers 4

Flutter Client

  • Ensure you adjust your header key layout in your Dart code slightly to match Python’s typical header parsing (x-app-key in Flutter maps straight to Python’s parameter variable x_app_key).