recall_regression_test.sh 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #!/usr/bin/env bash
  2. # recall_regression_test.sh
  3. #
  4. # 召回测试接口 — 部署前后回归对比脚本
  5. #
  6. # 用法:
  7. # ./recall_regression_test.sh run baseline # 部署前: 跑全部用例,存到 results/baseline/
  8. # ./recall_regression_test.sh run verify # 部署后: 跑同样用例,存到 results/verify/
  9. # ./recall_regression_test.sh diff baseline verify # 比对两次输出
  10. #
  11. # 可选环境变量:
  12. # BASE_URL 默认 https://api-internal.piaoquantv.com/videoVector
  13. # RESULTS_DIR 默认 ./recall_test_results
  14. #
  15. # 依赖: curl, python3 (用于 json 格式化)
  16. set -euo pipefail
  17. BASE_URL="${BASE_URL:-https://api-internal.piaoquantv.com/videoVector}"
  18. RESULTS_DIR="${RESULTS_DIR:-$(cd "$(dirname "$0")" && pwd)/recall_test_results}"
  19. # ===== 测试输入 =====
  20. # 真实视频 ID (用户确认)
  21. VIDEO_ID_PRIMARY=64632804
  22. # 候补 ID (从 prod matchByText 实际返回中取得)
  23. VIDEO_ID_SECONDARY=67688956
  24. # 文本召回查询词
  25. QUERY_TEXTS=(
  26. "夏季减肥小妙招"
  27. "宝宝辅食做法"
  28. "婆婆和儿媳"
  29. )
  30. # 配置编码
  31. CONFIG_CODES=(VIDEO_TOPIC VIDEO_INSPIRATION)
  32. # Top-N (固定,避免参数不同导致的 size 差异)
  33. TOP_N=10
  34. # ===== 工具函数 =====
  35. log() { echo "[$(date +%H:%M:%S)] $*" >&2; }
  36. # 美化 + 标准化 JSON (排序 key、UTF-8 不转义、固定缩进) → 稳定 diff
  37. prettify() {
  38. python3 -c '
  39. import sys, json
  40. try:
  41. d = json.load(sys.stdin)
  42. print(json.dumps(d, indent=2, ensure_ascii=False, sort_keys=True))
  43. except Exception as e:
  44. sys.stderr.write("JSON parse failed: %s\n" % e)
  45. sys.exit(1)
  46. '
  47. }
  48. # 调 GET
  49. do_get() {
  50. local name="$1"; shift
  51. local path="$1"; shift
  52. local out_dir="$1"; shift
  53. local raw="$out_dir/${name}.raw.txt"
  54. local pretty="$out_dir/${name}.json"
  55. log "GET $name -> $path"
  56. if curl -sS --max-time 30 "${BASE_URL}${path}" > "$raw" 2>"$raw.err"; then
  57. if prettify < "$raw" > "$pretty" 2>/dev/null; then
  58. rm -f "$raw" "$raw.err"
  59. else
  60. log " WARN: $name 返回非 JSON,保留 .raw.txt"
  61. mv "$raw" "$pretty"
  62. rm -f "$raw.err"
  63. fi
  64. else
  65. log " ERROR: $name curl 失败"
  66. mv "$raw.err" "$pretty"
  67. rm -f "$raw"
  68. fi
  69. }
  70. # 调 POST
  71. do_post() {
  72. local name="$1"; shift
  73. local path="$1"; shift
  74. local body="$1"; shift
  75. local out_dir="$1"; shift
  76. local raw="$out_dir/${name}.raw.txt"
  77. local pretty="$out_dir/${name}.json"
  78. log "POST $name -> $path body=$body"
  79. if curl -sS --max-time 60 -X POST \
  80. -H "Content-Type: application/json" \
  81. -d "$body" \
  82. "${BASE_URL}${path}" > "$raw" 2>"$raw.err"; then
  83. if prettify < "$raw" > "$pretty" 2>/dev/null; then
  84. rm -f "$raw" "$raw.err"
  85. else
  86. log " WARN: $name 返回非 JSON,保留 .raw.txt"
  87. mv "$raw" "$pretty"
  88. rm -f "$raw.err"
  89. fi
  90. else
  91. log " ERROR: $name curl 失败"
  92. mv "$raw.err" "$pretty"
  93. rm -f "$raw"
  94. fi
  95. }
  96. # ===== run 模式 =====
  97. run_tests() {
  98. local label="$1"
  99. local out_dir="$RESULTS_DIR/$label"
  100. mkdir -p "$out_dir"
  101. log "==> 输出目录: $out_dir"
  102. log "==> BASE_URL: $BASE_URL"
  103. # --- videoDetail ---
  104. do_get "videoDetail__primary" "/recallTest/videoDetail?videoId=$VIDEO_ID_PRIMARY" "$out_dir"
  105. do_get "videoDetail__secondary" "/recallTest/videoDetail?videoId=$VIDEO_ID_SECONDARY" "$out_dir"
  106. do_get "videoDetail__missing" "/recallTest/videoDetail?videoId=1" "$out_dir"
  107. # --- aiUnderstanding ---
  108. do_get "aiUnderstanding__primary" "/recallTest/aiUnderstanding?videoId=$VIDEO_ID_PRIMARY" "$out_dir"
  109. # --- deconstructPoints ---
  110. do_get "deconstructPoints__primary" "/recallTest/deconstructPoints?videoId=$VIDEO_ID_PRIMARY" "$out_dir"
  111. do_get "deconstructPoints__secondary" "/recallTest/deconstructPoints?videoId=$VIDEO_ID_SECONDARY" "$out_dir"
  112. # --- matchByText × (queryText × configCode) ---
  113. local idx=0
  114. for qt in "${QUERY_TEXTS[@]}"; do
  115. idx=$((idx+1))
  116. for cc in "${CONFIG_CODES[@]}"; do
  117. do_post \
  118. "matchByText__q${idx}__${cc}" \
  119. "/recallTest/matchByText" \
  120. "{\"queryText\":\"$qt\",\"configCode\":\"$cc\",\"topN\":$TOP_N}" \
  121. "$out_dir"
  122. done
  123. # 默认 configCode (不传)
  124. do_post \
  125. "matchByText__q${idx}__DEFAULT" \
  126. "/recallTest/matchByText" \
  127. "{\"queryText\":\"$qt\",\"topN\":$TOP_N}" \
  128. "$out_dir"
  129. done
  130. # --- matchByText 边界: 空文本 ---
  131. do_post "matchByText__empty" "/recallTest/matchByText" \
  132. "{\"queryText\":\"\",\"configCode\":\"VIDEO_TOPIC\",\"topN\":$TOP_N}" "$out_dir"
  133. # --- matchByVideoId × configCode ---
  134. for cc in "${CONFIG_CODES[@]}"; do
  135. do_post \
  136. "matchByVideoId__primary__${cc}" \
  137. "/recallTest/matchByVideoId" \
  138. "{\"videoId\":$VIDEO_ID_PRIMARY,\"configCode\":\"$cc\",\"topN\":$TOP_N}" \
  139. "$out_dir"
  140. do_post \
  141. "matchByVideoId__secondary__${cc}" \
  142. "/recallTest/matchByVideoId" \
  143. "{\"videoId\":$VIDEO_ID_SECONDARY,\"configCode\":\"$cc\",\"topN\":$TOP_N}" \
  144. "$out_dir"
  145. done
  146. # 默认 configCode
  147. do_post \
  148. "matchByVideoId__primary__DEFAULT" \
  149. "/recallTest/matchByVideoId" \
  150. "{\"videoId\":$VIDEO_ID_PRIMARY,\"topN\":$TOP_N}" \
  151. "$out_dir"
  152. # --- matchByVideoId 边界: 不存在的 ID ---
  153. do_post "matchByVideoId__missing" "/recallTest/matchByVideoId" \
  154. "{\"videoId\":1,\"configCode\":\"VIDEO_TOPIC\",\"topN\":$TOP_N}" "$out_dir"
  155. log "==> 完成,共生成 $(ls "$out_dir" | wc -l | tr -d ' ') 个文件"
  156. log "==> 路径: $out_dir"
  157. }
  158. # ===== diff 模式 =====
  159. diff_results() {
  160. local a="$1"
  161. local b="$2"
  162. local dir_a="$RESULTS_DIR/$a"
  163. local dir_b="$RESULTS_DIR/$b"
  164. if [[ ! -d "$dir_a" ]]; then echo "目录不存在: $dir_a" >&2; exit 2; fi
  165. if [[ ! -d "$dir_b" ]]; then echo "目录不存在: $dir_b" >&2; exit 2; fi
  166. echo "==> 比对 $dir_a vs $dir_b"
  167. echo
  168. local files_a files_b
  169. files_a=$(cd "$dir_a" && ls *.json 2>/dev/null | sort)
  170. files_b=$(cd "$dir_b" && ls *.json 2>/dev/null | sort)
  171. if [[ "$files_a" != "$files_b" ]]; then
  172. echo "!! 文件清单不一致:"
  173. diff <(echo "$files_a") <(echo "$files_b") || true
  174. echo
  175. fi
  176. local total=0 changed=0 same=0
  177. for f in $files_a; do
  178. if [[ ! -f "$dir_b/$f" ]]; then continue; fi
  179. total=$((total+1))
  180. if diff -q "$dir_a/$f" "$dir_b/$f" >/dev/null 2>&1; then
  181. same=$((same+1))
  182. else
  183. changed=$((changed+1))
  184. echo "----- DIFF: $f -----"
  185. diff -u "$dir_a/$f" "$dir_b/$f" || true
  186. echo
  187. fi
  188. done
  189. echo "==> 统计: 共 $total 个文件, 一致 $same, 有差异 $changed"
  190. }
  191. # ===== 入口 =====
  192. cmd="${1:-}"
  193. case "$cmd" in
  194. run)
  195. label="${2:-baseline}"
  196. run_tests "$label"
  197. ;;
  198. diff)
  199. a="${2:-baseline}"
  200. b="${3:-verify}"
  201. diff_results "$a" "$b"
  202. ;;
  203. *)
  204. cat <<EOF
  205. 用法:
  206. $0 run [LABEL] # 跑全部测试,默认 LABEL=baseline
  207. $0 diff LABEL_A LABEL_B # 比对两次输出
  208. 示例:
  209. # 部署前
  210. $0 run baseline
  211. # (你部署新版本)
  212. $0 run verify
  213. # 比对差异
  214. $0 diff baseline verify
  215. 环境变量:
  216. BASE_URL=$BASE_URL
  217. RESULTS_DIR=$RESULTS_DIR
  218. EOF
  219. exit 1
  220. ;;
  221. esac