- SearchController: q 빈값 가드 (HTTP 400) — '%%' LIKE 응답 폭발 차단
- SearchService:
- keywordSearch: LIKE 와일드카드 escape (%, _, \\)
- hybrid 모드: semantic 결과에도 attachChannels 호출 (이전: keyword만)
- ObjectMapper/TypeReference static 재사용 (캐시 hit 경로 GC 압박 완화)
- 알 수 없는 mode → warn 로그 + keyword fallback (이전: silent)
- maxDistance를 @Value("${app.search.max-distance:0.57}")로 외부화
- SearchMapper.xml: LIKE 절에 ESCAPE '\\' 추가
- VectorService.searchSimilar: embeddings/first list null/empty 가드 (NPE 방지)
- application.yml: app.search.max-distance (env SEARCH_MAX_DISTANCE) 추가
후속 분리: batch insert + 테스트 (별도 후속 이슈)
Refs: #293
55 lines
2.6 KiB
XML
55 lines
2.6 KiB
XML
<?xml version="1.0" encoding="UTF-8" ?>
|
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
<mapper namespace="com.tasteby.mapper.SearchMapper">
|
|
|
|
<resultMap id="restaurantMap" type="Restaurant">
|
|
<id property="id" column="id"/>
|
|
<result property="name" column="name"/>
|
|
<result property="address" column="address"/>
|
|
<result property="region" column="region"/>
|
|
<result property="latitude" column="latitude"/>
|
|
<result property="longitude" column="longitude"/>
|
|
<result property="cuisineType" column="cuisine_type"/>
|
|
<result property="priceRange" column="price_range"/>
|
|
<result property="phone" column="phone"/>
|
|
<result property="website" column="website"/>
|
|
<result property="googlePlaceId" column="google_place_id"/>
|
|
<result property="tablingUrl" column="tabling_url"/>
|
|
<result property="catchtableUrl" column="catchtable_url"/>
|
|
<result property="businessStatus" column="business_status"/>
|
|
<result property="rating" column="rating"/>
|
|
<result property="ratingCount" column="rating_count"/>
|
|
</resultMap>
|
|
|
|
<select id="keywordSearch" resultMap="restaurantMap">
|
|
SELECT DISTINCT r.id, r.name, r.address, r.region, r.latitude, r.longitude,
|
|
r.cuisine_type, r.price_range, r.phone, r.website, r.google_place_id,
|
|
r.tabling_url, r.catchtable_url,
|
|
r.business_status, r.rating, r.rating_count
|
|
FROM restaurants r
|
|
JOIN video_restaurants vr ON vr.restaurant_id = r.id
|
|
JOIN videos v ON v.id = vr.video_id
|
|
WHERE r.latitude IS NOT NULL
|
|
<!-- #293 — ESCAPE 절로 사용자 입력의 %, _ 와일드카드 의도 우회 차단 -->
|
|
AND (UPPER(r.name) LIKE UPPER(#{query}) ESCAPE '\'
|
|
OR UPPER(r.address) LIKE UPPER(#{query}) ESCAPE '\'
|
|
OR UPPER(r.region) LIKE UPPER(#{query}) ESCAPE '\'
|
|
OR UPPER(r.cuisine_type) LIKE UPPER(#{query}) ESCAPE '\'
|
|
OR UPPER(vr.foods_mentioned) LIKE UPPER(#{query}) ESCAPE '\'
|
|
OR UPPER(v.title) LIKE UPPER(#{query}) ESCAPE '\')
|
|
FETCH FIRST #{limit} ROWS ONLY
|
|
</select>
|
|
|
|
<select id="findChannelsByRestaurantIds" resultType="map">
|
|
SELECT DISTINCT vr.restaurant_id, c.channel_name
|
|
FROM video_restaurants vr
|
|
JOIN videos v ON v.id = vr.video_id
|
|
JOIN channels c ON c.id = v.channel_id
|
|
WHERE vr.restaurant_id IN
|
|
<foreach item="id" collection="ids" open="(" separator="," close=")">
|
|
#{id}
|
|
</foreach>
|
|
</select>
|
|
|
|
</mapper>
|