# Search architecture and deployment

This document describes how **typeahead search**, **full search results**, and the **Meilisearch index** fit together across the Bagisto/Laravel host (`markatty`), the **VPS search stack** (`vps-search`), and the **Flutter store app** (`markattystoreapp`).

---

## High-level architecture

```mermaid
flowchart LR
  subgraph flutter [Flutter app]
    UI[Search UI]
    FZ[fuzzysuggest]
    SS[searchsuggestion]
    APN[allproductnames]
  end

  subgraph laravel [Laravel / Bagisto host]
    MS[MobikulController]
    MA[MobikulApi getSearchSuggestion]
    EXP[exportProductsForSearch]
  end

  subgraph vps [VPS / Docker search stack]
    API[search-api Node]
    MEI[(Meilisearch)]
  end

  UI --> FZ --> MS
  FZ --> API
  MS --> API
  UI --> SS --> MA
  MA --> API
  MA --> DB[(MySQL product_flat)]
  UI --> APN --> EXP
  APN --> DB
  API --> MEI
  API --> EXP
```

- **Typeahead (names only):** Flutter → Laravel `fuzzysuggest` → VPS `GET /suggest` → optional name hydration from DB if the API returns only `product_ids`.
- **After user picks a suggestion or submits:** Flutter → Laravel `searchsuggestion` → VPS `GET /suggest` for product IDs → Laravel loads full product rows from `product_flat` (same response shape as before).
- **Local / offline-style fallback:** Flutter loads **`allproductnames`** (large list), builds an on-device index, and uses fuzzy matching if the fuzzy API fails or returns nothing.

---

## Search process (runtime)

### 1. Typeahead: `mobikulhttp/extra/fuzzysuggest`

| Layer | Responsibility |
|--------|----------------|
| **Flutter** | `SearchService.getSearchSuggestions()` calls `ApiClient.getFuzzySuggest(storeId, q, locale, limit, token, currency)`. Expects JSON `{ success, suggestions: string[] }`. |
| **Laravel** | `MobikulController@fuzzySuggest` validates `storeId`, `q`, `locale`, `limit`. If `SEARCH_SUGGEST_*` is configured, calls VPS `GET /suggest` with Bearer `SEARCH_SUGGEST_TOKEN`. Maps `suggestions[].label` to strings, or hydrates labels from `product_flat` when the VPS returns only `product_ids`. |
| **VPS `search-api`** | Queries Meilisearch (staged search, first-letter variants, re-ranking). Returns `suggestions` (`product_id` + `label`) and `product_ids`. |

**Flutter code (reference):** `markattystoreapp/lib/screens/search/data/services/search_service.dart`, `lib/core/api/ApiClient.dart`, `lib/core/networking/Api_constants.dart` (`MOBIKUL_EXTRAS_FUZZY_SUGGEST`).

**Laravel code (reference):** `Code/html2/packages/Webkul/MobikulApiTransformer/Http/Controllers/Shop/Extra/MobikulController.php` (`fuzzySuggest`), `config/services.php` (`search_suggest`).

---

### 2. Full search results: `mobikulhttp/extra/searchsuggestion`

| Layer | Responsibility |
|--------|----------------|
| **Flutter** | Uses existing Mobikul search API with `searchQuery`, `storeId`, `locale`, etc., after the user selects a suggestion or runs search. |
| **Laravel** | `MobikulApi::getSearchSuggestion()` optionally calls the same VPS `GET /suggest` when search suggest is enabled, reads `product_ids` (or derives IDs from `suggestions`), then loads paginated `product_flat` rows for the channel/locale so the API response shape stays unchanged. |
| **Fallback** | If the VPS call fails or returns no IDs, logic falls back to the original database search. |

**Laravel code (reference):** `MobikulApi.php` (`getSearchSuggestion`).

---

### 3. Product name list: `mobikulhttp/extra/allproductnames`

| Layer | Responsibility |
|--------|----------------|
| **Flutter** | `DynamicSuggestionsManager` / `SearchService.getAllProductNames()` — loads all names for the store (cached), used to build `SearchIndexManager` and local fuzzy fallback. Still used when opening search and when API typeahead is unavailable. |
| **Laravel** | Existing Mobikul extra route returning all product names for a `storeId`. |

**Flutter code (reference):** `markattystoreapp/lib/screens/search/utils/dynamic_suggestions_manager.dart`, `my_search_delegate.dart` (preload).

---

## Indexing process (offline from the app’s perspective)

1. **Export** — VPS indexer (or any client) calls the **protected** Laravel endpoint:
   - `GET /mobikulhttp/extra/internal/search-export/products`
   - Query: `storeId` (required), optional `limit`, `cursor`, `updated_since`
   - Header: `Authorization: Bearer <SEARCH_EXPORT_TOKEN>` (same value as Laravel `SEARCH_EXPORT_TOKEN` / VPS `EXPORT_BEARER_TOKEN`).

2. **Fold & upsert** — `vps-search/search-api` (`sync.js`) folds EN/AR rows per product, builds n-gram fields, upserts into Meilisearch index (default name `products`).

3. **Trigger sync** — `POST /sync` on `search-api` with Bearer `SUGGEST_BEARER_TOKEN` (must match Laravel `SEARCH_SUGGEST_TOKEN`).

**Laravel code (reference):** `MobikulController@exportProductsForSearch`, route in `Http/routes.php`, middleware `validateSearchExportToken`. The export action is excluded from the normal Mobikul API header check so the export token alone can authenticate.

**VPS code (reference):** `vps-search/docker-compose.yml`, `vps-search/search-api/src/sync.js`, `server.js`, `meili.js`, `ranking.js`, `queryVariants.js`.

---

## Environment variables

### Laravel (`Code/html2/.env`)

| Variable | Purpose |
|----------|---------|
| `SEARCH_SUGGEST_ENABLED` | `true` to call the VPS from `fuzzysuggest` and `getSearchSuggestion`. |
| `SEARCH_SUGGEST_BASE_URL` | Origin of `search-api` (no trailing slash), e.g. `https://search.example.com` or `http://host.docker.internal:8080` when Laravel runs **inside Docker** and search-api is published on the host. |
| `SEARCH_SUGGEST_TOKEN` | Bearer secret; must equal VPS `SUGGEST_BEARER_TOKEN`. |
| `SEARCH_EXPORT_TOKEN` | Bearer secret for `/internal/search-export/products`; must equal VPS `EXPORT_BEARER_TOKEN`. |

Configured in `config/services.php` under `search_suggest`.

### VPS (`vps-search/.env`)

| Variable | Purpose |
|----------|---------|
| `MEILI_MASTER_KEY` | Meilisearch API key. |
| `EXPORT_BASE_URL` | Base URL of the Bagisto host (what `sync` uses to fetch export). On Docker Desktop, often `http://host.docker.internal` with… |
| `EXPORT_HOST_HEADER` | …`Host: your-shop-hostname` so the correct vhost is hit when using `host.docker.internal`. |
| `EXPORT_BEARER_TOKEN` | Must match Laravel `SEARCH_EXPORT_TOKEN`. |
| `SUGGEST_BEARER_TOKEN` | Must match Laravel `SEARCH_SUGGEST_TOKEN`. |
| `SYNC_STORE_ID` | Channel/store id for sync (Bagisto `storeId`). |

After changing Laravel `.env`, run `php artisan config:clear` (or avoid `config:cache` until values are final).

---

## Local development (Docker)

1. **Bagisto stack:** `Code/html2` — see `DOCKER_SETUP.md`. The `app` service includes `extra_hosts: host.docker.internal:host-gateway` so `SEARCH_SUGGEST_BASE_URL=http://host.docker.internal:8080` reaches **search-api** bound to port 8080 on the host.

2. **Search stack:** `vps-search` — `docker compose up -d` starts Meilisearch (`7700`) and `search-api` (`8080`).

3. **Important:** From **inside** the PHP container, `http://127.0.0.1:8080` or `http://search.127.0.0.1.nip.io:8080` usually points at the **container**, not your Mac. Use `host.docker.internal` for `SEARCH_SUGGEST_BASE_URL` when Laravel is in Docker.

4. **Reindex:**  
   `curl -X POST "http://<search-api-host>:8080/sync" -H "Authorization: Bearer <SUGGEST_BEARER_TOKEN>"`

5. **Smoke test suggest:**  
   `curl "http://<search-api-host>:8080/suggest?q=test&storeId=<id>&locale=en&limit=10" -H "Authorization: Bearer <SUGGEST_BEARER_TOKEN>"`

---

## Deployment checklist

### Laravel (shared host)

1. Deploy code including `fuzzysuggest`, `exportProductsForSearch`, `MobikulApi` VPS branch, routes, middleware, and `config/services.php`.
2. Set `SEARCH_SUGGEST_*` and `SEARCH_EXPORT_TOKEN` in production `.env`.
3. Point `SEARCH_SUGGEST_BASE_URL` at the **public** search-api URL (HTTPS in production).
4. `php artisan config:clear` or `php artisan config:cache` after env changes.
5. Confirm routes exist, e.g. `php artisan route:list | grep -E 'fuzzysuggest|search-export'`.

### VPS (Meilisearch + search-api)

1. Set `.env` (`MEILI_MASTER_KEY`, `EXPORT_*`, `SUGGEST_BEARER_TOKEN`, `SYNC_STORE_ID`).
2. `docker compose build search-api && docker compose up -d` (use `build --no-cache` after `src` changes).
3. Run `/sync` after deploy or on a schedule; persist `meili_data` volume.
4. Lock down firewall: only your Laravel egress needs `8080` (or put nginx + TLS in front).

### Flutter app

1. **Base URL** must match the shop API host (same host that serves `mobikulhttp/...`).
2. No extra env is required for search beyond existing API base URL / `api-token` / store prefs; endpoints are fixed in `Api_constants.dart`.
3. **Optional:** reduce or lazy-load `allproductnames` later if you want less payload on open search — currently still used for cache + fallback.

---

## Troubleshooting

| Symptom | Things to check |
|---------|------------------|
| `fuzzysuggest` 404 | Route not deployed or route cache; wrong URL prefix. |
| `fuzzysuggest` always `[]` | `SEARCH_SUGGEST_ENABLED` / `base_url` / `token`; PHP cannot reach search-api (Docker networking); VPS returns no hits (re-sync, wrong `store_id` in index). |
| `searchsuggestion` empty but `/suggest` returns IDs | Channel/locale mismatch vs `product_flat`; IDs not visible in that channel. |
| Export fails | `SEARCH_EXPORT_TOKEN` mismatch; `Host` header if using `host.docker.internal`; HTTPS vs HTTP. |
| Stale ranking after code change | Rebuild `search-api` image; restart container. |

---

## Related paths in this monorepo

| Area | Path |
|------|------|
| Laravel suggest + export | `Code/html2/packages/Webkul/MobikulApiTransformer/` |
| Laravel config | `Code/html2/config/services.php` |
| Docker app notes | `Code/html2/DOCKER_SETUP.md` |
| VPS stack | `vps-search/` |
| Flutter search | `markattystoreapp/lib/screens/search/` |

---

## Flutter app pointer

The mobile app lives in a sibling project: **`markattystoreapp`**. Search entry points: `lib/screens/search/data/services/search_service.dart` and `lib/screens/search/presentiation/pages/my_search_delegate.dart`. API contracts are defined in `lib/core/api/ApiClient.dart` (regenerate with `build_runner` after annotation changes).
