Skip to content

Commit

Permalink
Fes 63 improve the search api with filter (#103)
Browse files Browse the repository at this point in the history
* add search logic

* add command to allow create function in mysql

---------

Co-authored-by: NHT <[email protected]>
  • Loading branch information
nfesta2023 and hoangtuan910 authored Apr 5, 2024
1 parent 4a52c78 commit 3d79bbf
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 2 deletions.
3 changes: 2 additions & 1 deletion local-db-init/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ services:
ports:
- "3306:3306"
volumes:
- ./sql-scripts:/docker-entrypoint-initdb.d
- ./sql-scripts:/docker-entrypoint-initdb.d
command: ["--log_bin_trust_function_creators=1"]
33 changes: 33 additions & 0 deletions src/feature/search/dto/search-food-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export enum ResultType {
FOOD = 'FOOD',
RESTAURANT = 'RESTAURANT',
}
export enum SortType {
RELEVANCE = 'RELEVANCE',
PRICE_ASC = 'PRICE_ASC',
PRICE_DESC = 'PRICE_DESC',
}
export enum Filter {
FROM_4STAR = 'FROM_4STAR',
VEG = 'VEG',
UPTO_500KCAL = 'UPTO_500KCAL',
}

enum IsoLangCode {
VIE = 'vie',
ENG = 'eng',
}

export class SearchFoodRequest {
keyword: string; //REQUIRED
ISO_language_code: IsoLangCode; //REQUIRED
lat: string; //REQUIRED
long: string; //REQUIRED
result_type: ResultType; //REQUIRED - FOOD | RESTAURANT
sort_type: SortType; //REQUIRED - RELEVANCE | PRICE_ASC | PRICE_DESC
filter: Filter[]; //OPTIONAL - FROM_4STAR | VEG | UPTO_500KCAL
offset: number; //REQUIRED
page_size: number; //REQUIRED
distance_limit_m: number; //OPTIONAL - DEFAULT: 10;000
base_distance_for_grouping_m: number; //OPTIONAL - DEFAUL: 200
}
63 changes: 63 additions & 0 deletions src/feature/search/dto/search-food-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export class SearchFoodResponse {
results: FoodDTO[] | SrestaurantDTO[];
keyword: string;
ISO_language_code: string;
lat: string;
long: string;
result_type: string;
sort_type: string;
filter: string[];
offset: number;
total: number;
distance_limit_m: number;
base_distance_for_grouping_m: number;
}

export interface FoodDTO {
id: number;
image: string;
top_label: string;
bottom_label: string;
name: TextByLang[];
restaurant_name: TextByLang[];
restaurant_id: number;
calorie_kcal: number;
rating: number;
distance_km: number;
delivery_time_s: number;
main_cooking_method: TextByLang[];
ingredient_brief_vie: string;
ingredient_brief_eng: string;
price: number;
price_after_discount: number;
promotion: string;
preparing_time_s: number;
cooking_time_s: number;
quantity_available: number;
is_vegetarian: boolean;
cooking_schedule: string;
units_sold: number;
is_advanced_customizable: boolean;
}
export interface SrestaurantDTO {
id: number;
intro_video: string;
logo_img: string;
name: TextByLang[];
rating: number;
distance_km: number;
delivery_time_s: number;
specialty: TextByLang[];
top_food: string;
promotion: string;
having_vegeterian_food: boolean;
max_price: number;
min_price: number;
unit: string;
is_advanced_customizable: boolean;
food_result: string[];
}
interface TextByLang {
ISO_language_code: string;
text: string;
}
81 changes: 80 additions & 1 deletion src/feature/search/search.controller.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Controller, Inject } from '@nestjs/common';
import { Controller, Inject, UseFilters } from '@nestjs/common';
import { SearchService } from './search.service';
import { MessagePattern } from '@nestjs/microservices';
import { FlagsmithService } from 'src/dependency/flagsmith/flagsmith.service';
import { SearchFoodByNameRequest } from './dto/search-food-by-name-request.dto';
import { SearchResult } from 'src/dto/search-result.dto';
import {
ResultType,
SearchFoodRequest,
SortType,
} from './dto/search-food-request.dto';
import {
SearchFoodResponse,
FoodDTO as SearchFoodDTO,
SrestaurantDTO as SearchRestaurantDTO,
} from './dto/search-food-response.dto';
import { CustomRpcExceptionFilter } from 'src/filters/custom-rpc-exception.filter';

@Controller()
export class SearchController {
Expand All @@ -28,4 +39,72 @@ export class SearchController {

//CURRENT LOGIC
}

@MessagePattern({ cmd: 'search_food' })
@UseFilters(new CustomRpcExceptionFilter())
async searchFood(data: SearchFoodRequest): Promise<SearchFoodResponse> {
const {
keyword,
ISO_language_code,
lat,
long,
result_type,
sort_type,
filter = [],
offset,
page_size,
distance_limit_m = 10000,
base_distance_for_grouping_m = 200,
} = data;

const fullResult: SearchFoodDTO[] | SearchRestaurantDTO[] =
await this.searchService.searchFoodInGeneral(
keyword.trim(),
ISO_language_code,
Number(lat),
Number(long),
distance_limit_m,
base_distance_for_grouping_m,
result_type,
filter,
);

//Sort the result based on the sort type
if (result_type == ResultType.FOOD) {
switch (sort_type) {
case SortType.RELEVANCE:
//Do nothing
break;
case SortType.PRICE_ASC:
fullResult.sort((a, b) => a.price - b.price);
break;
case SortType.PRICE_DESC:
fullResult.sort((a, b) => b.price - a.price);
break;

default:
break;
}
}

//Filter with the offset & page_size
const finalResult = fullResult.slice(offset, page_size);

const result: SearchFoodResponse = {
results: finalResult,
keyword: keyword,
ISO_language_code: ISO_language_code,
lat: lat,
long: long,
result_type: result_type,
sort_type: sort_type,
filter: filter,
offset: offset + finalResult.length,
total: fullResult.length,
distance_limit_m: distance_limit_m,
base_distance_for_grouping_m: base_distance_for_grouping_m,
};

return result;
}
}
143 changes: 143 additions & 0 deletions src/feature/search/search.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import { FoodDTO } from 'src/dto/food.dto';
import { PriceRange } from 'src/type';
import { GeneralResponse } from 'src/dto/general-response.dto';
import { CommonService } from '../common/common.service';
import {
FoodDTO as SearchFoodDTO,
SrestaurantDTO as SearchRestaurantDTO,
} from './dto/search-food-response.dto';
import { ResultType, Filter } from './dto/search-food-request.dto';
import { MenuItem } from 'src/entity/menu-item.entity';

@Injectable()
export class SearchService {
Expand Down Expand Up @@ -132,4 +138,141 @@ export class SearchService {
response.data = searchResult;
return response;
}

async searchFoodInGeneral(
keyword: string,
ISO_language_code: string,
lat: number,
long: number,
distance_limit_m: number,
base_distance_for_grouping_m: number,
result_type: ResultType,
filter: Filter[],
): Promise<SearchFoodDTO[] | SearchRestaurantDTO[]> {
let searchResult: SearchFoodDTO[] | SearchRestaurantDTO[];
let rawData: any;
// Search with raw SQL query in the full-search table Food_Search
if (keyword) {
rawData = await this.entityManager.query(
`SELECT food_search.menu_item_id, food_search.restaurant_id, calculate_distance(${lat}, ${long}, food_search.latitude, food_search.longitude, ${base_distance_for_grouping_m}) AS distance, MATCH (food_search.name , food_search.short_name) AGAINST ('${keyword}' IN NATURAL LANGUAGE MODE) AS score FROM Food_Search AS food_search Where food_search.ISO_language_code = '${ISO_language_code}' GROUP BY menu_item_id HAVING distance <= ${distance_limit_m} AND score > 0 ORDER BY distance ASC , score DESC`,
);
} else if (!keyword) {
rawData = await this.entityManager.query(
`SELECT food_search.menu_item_id, food_search.restaurant_id, calculate_distance(${lat}, ${long}, food_search.latitude, food_search.longitude, ${base_distance_for_grouping_m}) AS distance FROM Food_Search AS food_search Where food_search.ISO_language_code = '${ISO_language_code}' GROUP BY menu_item_id HAVING distance <= ${distance_limit_m} ORDER BY distance ASC`,
);
}

//Extract list of menu_item_id and restaurant_id
const menuItemIds = [];
const restaurantIds = [];
rawData.forEach((item) => {
menuItemIds.push(item.menu_item_id);

//Only add the unique restaurant Id
if (
restaurantIds.find((val) => val === item.restaurant_id) == undefined
) {
restaurantIds.push(item.restaurant_id);
}
});

//Get list of menu item
const menuItems = await this.foodService.getFoodsWithListOfMenuItem(
menuItemIds,
[ISO_language_code],
);

// Get list of delivery restaurant data
const deliveryRestaurants =
await this.restaurantService.getDeliveryRestaurantByListOfId(
restaurantIds,
lat,
long,
);

if (result_type === ResultType.FOOD) {
//Filtering
const filteredMenuItems: MenuItem[] = [];
for (const menuItem of menuItems) {
if (filter.includes(Filter.FROM_4STAR)) {
if (menuItem.rating < 4) {
continue;
}
}

if (filter.includes(Filter.UPTO_500KCAL)) {
if (Number(menuItem.skus[0].calorie_kcal) > 500) {
continue;
}
}

if (filter.includes(Filter.VEG)) {
if (menuItem.is_vegetarian == 0) {
continue;
}
}

filteredMenuItems.push(menuItem);
}
//Build FoodDTOs
const foodDTOs: SearchFoodDTO[] = [];
for (const menuItem of filteredMenuItems) {
const deliveryRestaurant = deliveryRestaurants.find(
(res) => menuItem.restaurant_id == res.restaurant_id,
);
const foodDTO = await this.commonService.convertIntoFoodDTO(
menuItem,
deliveryRestaurant,
);
foodDTOs.push(foodDTO);
}
searchResult = foodDTOs;
} else if (result_type === ResultType.RESTAURANT) {
//Filtering
const filteredRestaurants = [];
for (const restaurant of deliveryRestaurants) {
if (filter.includes(Filter.FROM_4STAR)) {
if (restaurant.rating < 4) {
continue;
}
}
filteredRestaurants.push(restaurant);
}
//Build RestaurantDTOs
const srestaurantDTOs: SearchRestaurantDTO[] = [];
for (const restaurant of filteredRestaurants) {
const menuItemIds: number[] = rawData.map((item) => {
if (item.restaurant_id == restaurant.restaurant_id) {
return item.menu_item_id;
}
});
const priceBeforeDiscountList = menuItems.map((food) => {
if (menuItemIds.includes(food.menu_item_id)) {
return food.skus[0].price;
}
});

const priceRange: PriceRange = {
min: Math.min(...priceBeforeDiscountList),
max: Math.max(...priceBeforeDiscountList),
};
const restaurantDTO =
await this.restaurantService.convertIntoRestaurantDTO(
restaurant,
priceRange,
);
const srestaurantDTO: SearchRestaurantDTO = {
...restaurantDTO,
food_result: menuItems.map((item) => {
if (menuItemIds.includes(item.menu_item_id)) {
return item.menuItemExt[0].short_name;
}
}),
};
srestaurantDTOs.push(srestaurantDTO);
}
searchResult = srestaurantDTOs;
}
return searchResult;
}
}

0 comments on commit 3d79bbf

Please sign in to comment.