# ĐẶC TẢ KỸ THUẬT API: SEARCH & FILTER (v2.6)

> **Dành cho:** Frontend Engineering Team (Web, Mobile App)  
> **Dịch vụ:** AI Search Engine API — LOTTE Mart Vietnam  
> **Phiên bản:** `2.6.0` | **Cập nhật:** 11/10/2026 | **Giao thức:** JSON over HTTP/2  

---

## 1. THÔNG TIN ENDPOINT & GATEWAY

- **HTTP Method:** `POST`
- **Accept:** `application/json`

### 1.1. Cấu trúc URL Endpoint (Đồng bộ tiền tố `/api`)

Hệ thống cung cấp 2 dịch vụ tìm kiếm qua API Gateway với định dạng URL đồng bộ:

1. **Tìm kiếm Từ khóa & Bộ lọc (Text Search):**  
   - Canonical: `POST /api/v2/{lang}/{storeId}/products/search`  
   - Alias: `POST /api/v2/p/mart/es/{storeId}/{lang}/products/search`
2. **Tìm kiếm bằng Hình ảnh (Image Search):**  
   - Canonical: `POST /api/v2/{lang}/{storeId}/products/search-by-image`  
   - Alias: `POST /api/v2/p/mart/es/{storeId}/{lang}/products/search-by-image`

*Ghi chú tham số URL: `{lang}` là mã ngôn ngữ (`vi` \| `en` \| `ko`), `{storeId}` là mã siêu thị / kho (vd: `nsg` — LOTTE Mart Nam Sài Gòn).*

### 1.2. Danh sách Gateway Base URL theo môi trường

| Môi trường | Base Gateway URL | URL Text Search | URL Image Search |
| :--- | :--- | :--- | :--- |
| **DEV** | `https://dev-gateway.martonline.lotte.vn` | `/api/v2/vi/nsg/products/search` | `/api/v2/vi/nsg/products/search-by-image` |
| **STAGING** | `https://staging-gateway.martonline.lotte.vn` | `/api/v2/vi/nsg/products/search` | `/api/v2/vi/nsg/products/search-by-image` |
| **PRODUCTION** | `https://gateway.martonline.lotte.vn` | `/api/v2/vi/nsg/products/search` | `/api/v2/vi/nsg/products/search-by-image` |

*(Trong mạng nội bộ backend k8s / direct service: `POST /api/v1/search`)*

---

## 2. REQUEST SPECIFICATION (THAM SỐ GỬI LÊN)

### 2.1. Cấu trúc JSON Request

| Trường root | Kiểu dữ liệu | Bắt buộc | Mặc định | Mô tả & Ràng buộc |
| :--- | :---: | :---: | :---: | :--- |
| `query` | `string` | **Có** | — | Từ khóa tìm kiếm. Tối thiểu 1 ký tự sau trim. Chuỗi rỗng trả về HTTP 400. |
| `paginationMode`| `string` | Không | `"cursor"` | Chế độ phân trang: `"cursor"` (cho infinite scroll) hoặc `"page"` (cho click số trang). |
| `cursor` | `string` | Không | `null` | Token phân trang Base64 lấy từ `nextCursor` của response trước đó (dùng cho mode `"cursor"`). |
| `page` | `number` | Không | `1` | Số thứ tự trang (khi dùng `paginationMode: "page"`, bắt đầu từ 1). |
| `pageSize` | `number` | Không | `20` | Số lượng sản phẩm trả về mỗi trang. Tối đa 100. |
| `sort` | `string` | Không | `"relevance"` | Tiêu chí sắp xếp: `"relevance"` (liên quan nhất), `"price_asc"` (giá tăng dần), `"price_desc"` (giá giảm dần), `"newest"` (mới nhất). |
| `filters` | `object` | Không | `{}` | Đối tượng chứa toàn bộ bộ lọc sản phẩm (Xem mục 2.2). **Lưu ý: Không dùng tên cũ `facetFilters`.** |
| `fields` | `string[]` | Không | All | Danh sách các trường cần lấy để tối ưu băng thông (Xem mục 2.3). |

### 2.2. Chi tiết các cách Filter (`filters`)

Tất cả các bộ lọc **bắt buộc nằm trong đối tượng `filters`**. Giữa các bộ lọc khác nhau áp dụng toán tử **AND**.

| Trường trong `filters` | Kiểu dữ liệu | Toán tử | Cú pháp gửi lên | Mô tả hành vi & Quy tắc |
| :--- | :---: | :---: | :--- | :--- |
| `viTags` | `string[]` | **OR** | `["Sữa Tươi", "Ít Đường"]` | Lọc theo tag tiếng Việt. Tối đa **10 items**. Sản phẩm khớp nếu có ít nhất 1 tag trong mảng. |
| `enTags` | `string[]` | **OR** | `["Fresh Milk"]` | Lọc theo tag tiếng Anh. Tối đa 10 items. |
| `krTags` | `string[]` | **OR** | `["우유"]` | Lọc theo tag tiếng Hàn. Tối đa 10 items. |
| `tags` | `string[]` | **OR** | `["Organic"]` | Tag alias chung đa ngữ. Tối đa 10 items. |
| `brandId` | `string` | **EQUAL** | `"Vinamilk"` | Lọc theo mã thương hiệu (Khớp chính xác). |
| `categoryId` | `string` | **PREFIX**| `"110"` | Lọc theo mã ngành hàng. Khớp tiền tố phân cấp (vd: `"110"` lấy cả con `"11001"`, `"11002"`). |
| `priceMin` | `number` | `>=` | `50000` | Giá tối thiểu (VND). Bỏ qua nếu `<= 0`. |
| `priceMax` | `number` | `<=` | `200000` | Giá tối đa (VND). Phải `>= priceMin`. |
| `inStockOnly` | `boolean` | **EQUAL** | `true` | Chỉ trả về sản phẩm còn tồn kho (`inStock: true`). |
| `delivery` | `string[]` | **OR** | `["express"]` | Phương thức giao: `"express"` (siêu tốc), `"standard"` (tiêu chuẩn). |
| `benefitFlags` | `string[]` | **OR** | `["special_price"]` | Ưu đãi: `"special_price"` (giá sốc), `"gift"` (quà tặng kèm). |
| `ratingMin` | `number` | `>=` | `4.0` | Đánh giá tối thiểu từ 1.0 đến 5.0 (vd: `4.0` tương ứng từ 4 sao trở lên). |
| `attrs` | `object` | **OR** *(trong key)* | `{"brand_origin": ["Vietnam"]}` | Thuộc tính động chuẩn hóa (EAV). Hỗ trợ 8 keys: `brand_origin`, `pack_type`, `volume`, `weight`, `flavor`, `target_user`, `usage_type`, `storage_condition`. |

### 2.3. Chọn trường dữ liệu tối ưu (`fields`)

Frontend kiểm soát chính xác payload trả về bằng mảng `fields`:
- **Khối cấp 1:** `"products"`, `"facets"`, `"suggestedFilters"`.
- **Trường trong thẻ sản phẩm:** `products.<field>` (vd: `products.id`, `products.name`, `products.price`, `products.imageUrl`, `products.inStock`...).
- **Quy tắc tối ưu băng thông:**
  - **Trang 1:** Gửi `fields: ["products", "facets"]` để lấy cả sản phẩm và cây bộ lọc.
  - **Trang 2 trở đi (Infinite scroll):** Gửi `fields: ["products"]` (bỏ qua tính toán facets, giảm 70% kích thước JSON).

---

## 3. RESPONSE SPECIFICATION (GIẢI THÍCH CHI TIẾT CÁC FIELD)

### 3.1. Các trường Siêu dữ liệu (Metadata)

Các trường này **luôn luôn xuất hiện trong mọi response**, không bị ảnh hưởng bởi tham số `fields`:

| Trường Response | Kiểu dữ liệu | Ý nghĩa & Mục đích sử dụng |
| :--- | :---: | :--- |
| `queryId` | `string` | UUID phiên tìm kiếm. Dùng để gửi kèm event theo dõi log / click-through tracking. |
| `totalHits` | `number` | Tổng số lượng sản phẩm thỏa mãn toàn bộ từ khóa và điều kiện lọc. Dùng hiển thị "Tìm thấy X sản phẩm". |
| `tookMs` | `number` | Thời gian OpenSearch thực thi truy vấn (tính bằng milliseconds). |
| `paginationMode`| `string` | Chế độ phân trang đang chạy (`"cursor"` hoặc `"page"`). |
| `page` | `number` | Số trang hiện tại (chỉ có khi `paginationMode: "page"`). |
| `pageSize` | `number` | Số sản phẩm được cấu hình trả về trên mỗi trang. |
| `hasMore` | `boolean` | `true` nếu vẫn còn sản phẩm ở các trang tiếp theo; `false` nếu đã đến trang cuối cùng. Dùng để ngắt trigger infinite scroll. |
| `nextCursor` | `string` | Chuỗi token Opaque Base64 đại diện cho vị trí tiếp theo. Frontend giữ nguyên giá trị này và truyền vào `cursor` ở request tiếp theo. |
| `resolvedMode` | `string` | Mode thuật toán thực thi nội bộ (`"text"`, `"hybrid"`...). |
| `corrected` | `boolean` | `true` nếu từ khóa người dùng bị sai chính tả và hệ thống đã tự động sửa; `false` nếu từ khóa gốc được giữ nguyên. |
| `normalizedQuery`| `string` | Chuỗi từ khóa sau khi đã chuẩn hóa và tự động sửa lỗi chính tả. Dùng hiển thị: *"Kết quả cho từ khóa: ... "* |
| `didYouMean` | `string` | Từ khóa gợi ý nếu hệ thống phát hiện từ khóa khả dĩ khác. Nếu có, dùng hiển thị: *"Có phải bạn muốn tìm: ... "* |

### 3.2. Danh sách sản phẩm (`products[]`)

Mỗi phần tử trong mảng `products` đại diện cho một thẻ sản phẩm hoàn chỉnh:

| Trường | Kiểu dữ liệu | Ý nghĩa |
| :--- | :---: | :--- |
| `id` | `string` | Mã SKU định danh duy nhất của sản phẩm. |
| `name` | `string` | Tên sản phẩm đầy đủ. |
| `price` | `number` | Giá bán thực tế hiện tại (VND). |
| `originalPrice` | `number` | Giá niêm yết ban đầu trước khi giảm giá (VND). |
| `discountRate` | `number` | Phần trăm giảm giá (vd: `15` tức là giảm 15%). |
| `imageUrl` | `string` | Đường dẫn ảnh đại diện sản phẩm (CDN). |
| `brandId` | `string` | Mã định danh thương hiệu. |
| `brandName` | `string` | Tên hiển thị của thương hiệu. |
| `categoryId` | `string` | Mã ngành hàng của sản phẩm. |
| `categoryName` | `string` | Tên ngành hàng phân cấp. |
| `inStock` | `boolean` | Trạng thái tồn kho: `true` = còn hàng, `false` = hết hàng. |
| `stockQty` | `number` | Số lượng tồn kho thực tế khả dụng. |
| `rating` | `number` | Điểm đánh giá trung bình của sản phẩm (thang điểm 1.0 đến 5.0). |
| `reviewCount` | `number` | Tổng số lượt khách hàng đánh giá sản phẩm. |
| `deliveryMethod`| `string[]`| Các hình thức giao hàng hỗ trợ (vd: `["express", "standard"]`). |
| `isNew` | `boolean` | `true` nếu là sản phẩm mới về. Dùng gắn badge "MỚI". |
| `isBest` | `boolean` | `true` nếu là sản phẩm bán chạy. Dùng gắn badge "BEST SELLER". |
| `isPromotion` | `boolean` | `true` nếu sản phẩm đang áp dụng chương trình khuyến mãi. |
| `tags` | `string[]` | Danh sách các tag nổi bật của sản phẩm. |

### 3.3. Dữ liệu bộ lọc (`facets`)

Đối tượng `facets` chứa số lượng đếm (aggregation count) của các bộ lọc để render sidebar và chip lọc:

| Nhóm Facet | Cấu trúc dữ liệu | Chi tiết & Cách xử lý ở Frontend |
| :--- | :---: | :--- |
| `viTags`, `enTags`, `krTags`, `tags` | `FacetBucket[]` | Danh sách các tag và số lượng SP khớp. Cơ chế Self-exclusion: Tag đang được chọn vẫn hiển thị các tag anh em kèm số lượng nếu chọn thêm. |
| `brands` | `FacetBucket[]` | Danh sách thương hiệu khớp. Gồm `key` (mã gửi lên `filters.brandId`), `count`, và `label`. |
| `categories` | `FacetBucket[]` | Danh sách ngành hàng khớp. Gồm `key` (mã gửi lên `filters.categoryId`), `count`, và `label`. |
| `priceRanges` | `FacetBucket[]` | Các phân khúc giá được tính toán sẵn. |
| `inStock` | `FacetBucket[]` | Số lượng sản phẩm còn hàng (`key: "true"`). |
| `delivery` | `FacetBucket[]` | Số lượng sản phẩm theo từng hình thức giao (`key: "express"`, `"standard"`). |
| `benefitFlags` | `FacetBucket[]` | Số lượng sản phẩm có ưu đãi (`key: "special_price"`, `"gift"`). |
| `rating` | `FacetBucket[]` | Số lượng sản phẩm theo các mốc sao. |
| `attrs` | `Record<string, FacetBucket[]>` | **LƯU Ý:** Đây là **Object Map**, KHÔNG PHẢI MẢNG. Duyệt bằng `Object.entries(facets.attrs)`. Mỗi key là tên thuộc tính (vd: `"brand_origin"`), value là mảng `FacetBucket[]`. |

Mỗi phần tử `FacetBucket` có cấu trúc:
```json
{
  "key": "Vinamilk",
  "count": 45,
  "label": "Vinamilk"
}
```

---

## 4. VÍ DỤ cURL VÀ RESPONSE THỰC TẾ

### 4.1. cURL: Tìm kiếm cơ bản (Trang đầu tiên)
```bash
curl -X POST "https://dev-gateway.martonline.lotte.vn/api/v2/vi/nsg/products/search" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "sữa tươi",
    "paginationMode": "cursor",
    "pageSize": 20,
    "fields": ["products", "facets"]
  }'
```

### 4.2. cURL: Tìm kiếm kết hợp đầy đủ Bộ lọc (Filters + Sort + Fields)
```bash
curl -X POST "https://dev-gateway.martonline.lotte.vn/api/v2/vi/nsg/products/search" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "sữa tươi",
    "paginationMode": "cursor",
    "pageSize": 20,
    "sort": "relevance",
    "filters": {
      "viTags": ["Sữa Tươi", "Ít Đường"],
      "brandId": "Vinamilk",
      "priceMin": 20000,
      "priceMax": 100000,
      "inStockOnly": true,
      "delivery": ["express"]
    },
    "fields": ["products", "facets"]
  }'
```

### 4.3. cURL: Cuộn trang tiếp theo (Infinite Scroll bằng Cursor)
```bash
curl -X POST "https://dev-gateway.martonline.lotte.vn/api/v2/vi/nsg/products/search" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "sữa tươi",
    "paginationMode": "cursor",
    "cursor": "ZXlKaGJHY2lPaUpTVXpVeE1pSXNJbXR...",
    "pageSize": 20,
    "fields": ["products"]
  }'
```

### 4.4. Response JSON mẫu thực tế (Trang 1):
```json
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8

{
  "queryId": "e3b0c442-98fc-1c14-9afe-432100000000",
  "totalHits": 45,
  "tookMs": 18,
  "paginationMode": "cursor",
  "pageSize": 20,
  "hasMore": true,
  "nextCursor": "ZXlKaGJHY2lPaUpTVXpVeE1pSXNJbXR...",
  "resolvedMode": "hybrid",
  "corrected": false,
  "normalizedQuery": "sữa tươi",
  "products": [
    {
      "id": "8934673123456",
      "name": "Sữa Tươi Tiệt Trùng Vinamilk 100% Ít Đường 1L",
      "price": 36500,
      "originalPrice": 41000,
      "discountRate": 11,
      "imageUrl": "https://img.martonline.lotte.vn/p/8934673123456.jpg",
      "brandId": "Vinamilk",
      "brandName": "Vinamilk",
      "categoryId": "11001",
      "categoryName": "Sữa Tươi Tiệt Trùng",
      "inStock": true,
      "rating": 4.8,
      "reviewCount": 124,
      "deliveryMethod": ["express", "standard"],
      "isPromotion": true,
      "tags": ["Sữa Tươi", "Ít Đường"]
    }
  ],
  "facets": {
    "viTags": [
      { "key": "Sữa Tươi", "count": 45, "label": "Sữa Tươi" },
      { "key": "Ít Đường", "count": 18, "label": "Ít Đường" }
    ],
    "brands": [
      { "key": "Vinamilk", "count": 45, "label": "Vinamilk" }
    ],
    "attrs": {
      "brand_origin": [
        { "key": "Vietnam", "count": 45, "label": "Việt Nam" }
      ]
    }
  }
}
```

---

## 5. MÃ LỖI HTTP & NGUYÊN NHÂN TỪ CHỐI (HTTP STATUS CODES)

| Mã HTTP | Tình huống | Thông điệp lỗi mẫu | Nguyên nhân & Cách khắc phục |
| :---: | :--- | :--- | :--- |
| **`200 OK`** | Thành công | Dữ liệu theo schema `SearchResponse`. | Xử lý bình thường. |
| **`400 Bad Request`** | Từ khóa rỗng | `{"error": "query must not be empty"}` | Giá trị `query` bị rỗng sau khi trim. Client phải kiểm tra `query.trim()` trước khi gửi. |
| **`400 Bad Request`** | Sai cấu trúc | `{"error": "unknown field \"facetFilters\""}` | Gửi tham số ngoài schema. Bắt buộc đổi tên thành `filters`. Không gửi trường dư thừa. |
| **`400 Bad Request`** | Quá giới hạn tag | `{"error": "filters.viTags accepts at most 10 items, got 11"}` | Mảng tag vượt quá giới hạn 10 phần tử. Khống chế tối đa 10 tag được kích hoạt trên UI. |
| **`400 Bad Request`** | Sai cursor | `{"error": "invalid cursor token"}` | Token `cursor` bị lỗi hoặc sửa đổi. Gửi đúng chuỗi nguyên bản từ `nextCursor`. |
| **`500 Internal Error`** | Lỗi máy chủ | `{"error": "Internal search error"}` | Lỗi hạ tầng backend. Hiển thị thông báo thân thiện và nút thử lại. |

---

## 6. ĐẶC TẢ TÌM KIẾM BẰNG HÌNH ẢNH (IMAGE SEARCH API)

### 6.1. Quy trình 2 pha (Two-phase Workflow)

Quy trình tìm kiếm ảnh được thiết kế tối ưu băng thông mạng và độ trễ theo 2 pha:

- **Pha 1 (Trang đầu tiên):** Client gửi request dạng `multipart/form-data` chứa file ảnh `image` và tham số lọc `params` (JSON). Response trả về danh sách sản phẩm cùng mã `imageRef` (vector cache key).
- **Pha 2 (Trang 2+ Infinite Scroll):** Client gửi request dạng `application/json` thông thường mang theo `imageRef` và `cursor`. **Tuyệt đối không cần upload lại file ảnh**, máy chủ tự động tra cứu vector đã tính toán trong bộ nhớ cache.

### 6.2. Cấu trúc Tham số Image Search

#### Pha 1: Upload ảnh (`multipart/form-data`)
- **Part `image` (Bắt buộc):** Binary dữ liệu ảnh (hỗ trợ `image/jpeg`, `image/png`, `image/webp`). Dung lượng tối đa 5MB *(khuyến nghị Frontend / App resize cạnh dài về khoảng 800px trước khi upload)*.
- **Part `params` (Tùy chọn):** Chuỗi JSON định nghĩa các tiêu chí bổ trợ:
  ```json
  {
    "query": "loại không đường",          // (Tùy chọn) Text đi kèm để lọc hẹp trong kết quả ảnh
    "paginationMode": "cursor",           // Mặc định: "cursor"
    "pageSize": 20,                       // Số sản phẩm mỗi trang (mặc định 20, max 100)
    "filters": {                          // Bộ lọc (y hệt cấu trúc filters của Text Search)
      "inStockOnly": true,
      "brandId": "Vinamilk"
    },
    "fields": ["products", "facets"]      // Chọn trường cần nhận
  }
  ```

#### Pha 2: Phân trang tiếp theo (`application/json`)
Khi người dùng cuộn trang tiếp theo, gửi body JSON:
```json
{
  "imageRef": "sha256:9f2a4c8e71b2d3...", // Token nhận từ response Pha 1 (BẮT BUỘC)
  "paginationMode": "cursor",
  "cursor": "ZXlKaGJHY2lPaUpTVXpVeE1pSXNJbXR...", // nextCursor từ trang trước
  "pageSize": 20,
  "fields": ["products"]                  // Tiết kiệm băng thông: chỉ lấy products
}
```

### 6.3. Các trường Response bổ sung cho Image Search

Response của Image Search kế thừa toàn bộ cấu trúc của Text Search (`products[]`, `facets`, `totalHits`, `tookMs`, `nextCursor`, `hasMore`), kèm theo các trường đặc thù:

| Trường Response | Kiểu dữ liệu | Ý nghĩa & Cách xử lý |
| :--- | :---: | :--- |
| `imageRef` | `string` | Mã hash SHA256 định danh vector của bức ảnh trong cache. **Client lưu chuỗi này** để gửi lên ở request Pha 2+. |
| `resolvedImageMode`| `string` | Thuật toán vector ảnh thực thi nội bộ (vd: `"vector_titan_multi"`). |
| `caption` | `object \| null` | Thông tin chú thích nếu hệ thống chạy ở chế độ AI Caption (gồm `query`, `outcome`, `modelId`). Thường là `null` ở chế độ vector thông thường. |

### 6.4. Ví dụ cURL Image Search Thực Tế

#### cURL Pha 1: Upload ảnh tìm kiếm lần đầu (Multipart)
```bash
curl -X POST "https://dev-gateway.martonline.lotte.vn/api/v2/vi/nsg/products/search-by-image" \
  -F "image=@san-pham.jpg;type=image/jpeg" \
  -F 'params={
    "paginationMode": "cursor",
    "pageSize": 20,
    "filters": {
      "inStockOnly": true
    },
    "fields": ["products", "facets"]
  }'
```

#### cURL Pha 2: Cuộn xem trang tiếp theo (JSON dùng `imageRef`)
```bash
curl -X POST "https://dev-gateway.martonline.lotte.vn/api/v2/vi/nsg/products/search-by-image" \
  -H "Content-Type: application/json" \
  -d '{
    "imageRef": "sha256:9f2a4c8e71b2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8",
    "paginationMode": "cursor",
    "cursor": "ZXlKaGJHY2lPaUpTVXpVeE1pSXNJbXR...",
    "pageSize": 20,
    "fields": ["products"]
  }'
```

