refactor(search): 只展示 keyword 续接词,去掉 titles 段
产品决定:搜索建议只展示 ts_stat 高频词续接(如'美'→美国/美军/美国政府), 不要真实文章 id 提示(用户认为这种'文章#566871'是噪音,没连续性)。 改动: - SearchSuggestionsResponse 去 title,只剩 query + keywords - SearchService 只查 search_keywords,fallback 路径也只针对 keywords - Feed.vue: 删掉 suggestTitles 状态 + SuggestTitleOption 类型联合, renderSuggestion 简化成 '词' 标签 + 词文本 + 右侧 weight 数字 - 0011 迁移: 删 search_title_suggestions 表 + 3 索引 + trigger + 函数 (trigger 在每篇文章 INSERT/UPDATE 都会跑,删了能省掉无用性能损耗) - 删除: app/models/search_title_suggestion.py + backfill_search_suggestions.py 替换成: app/scripts/refresh_search_keywords.py(只跑一次词频刷新)
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
NPagination, NAutoComplete, useMessage,
|
||||
} from 'naive-ui'
|
||||
import { articlesApi, readsApi, sourcesApi, type ArticleListItem, type Source } from '@/api/articles'
|
||||
import { searchApi, type SearchKeyword, type SearchTitleSuggestion } from '@/api/search'
|
||||
import { searchApi, type SearchKeyword } from '@/api/search'
|
||||
import { useDebounce } from '@/composables/useDebounce'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -37,18 +37,16 @@ const hideRead = ref(true)
|
||||
|
||||
const sourceOptions = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
// === 搜索建议(autocomplete) ===
|
||||
// === 搜索建议(autocomplete) — 纯 keyword 续接词 ===
|
||||
// 触发:q 变化(用户输入)→ 250ms debounce → 调 /api/v1/search/suggestions
|
||||
// 取消:每次新输入前 abort 上一次未完成的请求,避免旧响应覆盖新结果
|
||||
// 选词:@select → 填入 q + 触发搜索(不再等回车)
|
||||
const suggestTitles = ref<SearchTitleSuggestion[]>([])
|
||||
const suggestKeywords = ref<SearchKeyword[]>([])
|
||||
let suggestAbort: AbortController | null = null
|
||||
|
||||
async function fetchSuggestions(prefix: string) {
|
||||
const p = prefix.trim()
|
||||
if (!p) {
|
||||
suggestTitles.value = []
|
||||
suggestKeywords.value = []
|
||||
return
|
||||
}
|
||||
@@ -58,9 +56,8 @@ async function fetchSuggestions(prefix: string) {
|
||||
suggestAbort = ctrl
|
||||
try {
|
||||
const resp = await searchApi.suggestions(p, 10, { signal: ctrl.signal })
|
||||
// 注意:race condition 防护 — 只采纳最新请求的响应
|
||||
// race condition 防护 — 只采纳最新请求的响应
|
||||
if (suggestAbort === ctrl) {
|
||||
suggestTitles.value = resp.titles
|
||||
suggestKeywords.value = resp.keywords
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -70,7 +67,6 @@ async function fetchSuggestions(prefix: string) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.debug('search suggestions failed:', e?.message)
|
||||
if (suggestAbort === ctrl) {
|
||||
suggestTitles.value = []
|
||||
suggestKeywords.value = []
|
||||
}
|
||||
}
|
||||
@@ -84,78 +80,42 @@ watch(q, (v) => {
|
||||
})
|
||||
|
||||
// === NAutoComplete options ===
|
||||
// 把 titles + keywords 拼成扁平 options 数组。
|
||||
// 用 discriminated union 让 TypeScript 在 onSelect 里能自动 narrow 出 meta 的具体类型。
|
||||
type SuggestTitleOption = {
|
||||
// 只用 keyword 续接词,扁平结构。
|
||||
type SuggestOption = {
|
||||
label: string
|
||||
value: string
|
||||
type: 'title'
|
||||
meta: SearchTitleSuggestion
|
||||
}
|
||||
type SuggestKeywordOption = {
|
||||
label: string
|
||||
value: string
|
||||
type: 'keyword'
|
||||
meta: SearchKeyword
|
||||
}
|
||||
type SuggestOption = SuggestTitleOption | SuggestKeywordOption
|
||||
|
||||
const suggestOptions = computed<SuggestOption[]>(() => {
|
||||
const out: SuggestOption[] = []
|
||||
for (const t of suggestTitles.value) {
|
||||
// 标题项:label 用 #id 标识(可后续扩展拉标题),value 是 id 字符串
|
||||
out.push({
|
||||
label: `#${t.id}`,
|
||||
value: t.id.toString(),
|
||||
type: 'title',
|
||||
meta: t,
|
||||
})
|
||||
}
|
||||
for (const k of suggestKeywords.value) {
|
||||
out.push({
|
||||
label: k.word,
|
||||
value: k.word,
|
||||
type: 'keyword',
|
||||
meta: k,
|
||||
})
|
||||
}
|
||||
return out
|
||||
return suggestKeywords.value.map((k) => ({
|
||||
label: k.word,
|
||||
value: k.word,
|
||||
meta: k,
|
||||
}))
|
||||
})
|
||||
|
||||
// 自定义 render:显示分类图标 + 类型
|
||||
// 自定义 render:显示"词"标签 + 词文本 + 权重
|
||||
function renderSuggestion(opt: SuggestOption) {
|
||||
return h(
|
||||
'div',
|
||||
{ class: 'feed-suggest-row' },
|
||||
[
|
||||
h('span', { class: `feed-suggest-tag feed-suggest-tag-${opt.type}` },
|
||||
opt.type === 'title' ? '文章' : '词'),
|
||||
h('span', { class: 'feed-suggest-tag feed-suggest-tag-keyword' }, '词'),
|
||||
h('span', { class: 'feed-suggest-text' }, opt.label),
|
||||
h('span', { class: 'feed-suggest-weight' }, String(opt.meta.weight)),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// 选完候选词:从当前 suggestOptions 反查 meta。
|
||||
// naive-ui 的 NAutoComplete 选完时只把 value 写回 v-model,我们额外用
|
||||
// ref 维护一个 lastSelectedType,模板里在 click 时先 setType,on-select 时
|
||||
// 再读 — 但更简单的做法:看 value 格式。title 类型的 value 是纯数字 id 字符串,
|
||||
// keyword 类型是中文/字母词。
|
||||
// 选完候选词:naive-ui 把 value 写回 v-model,我们从 suggestOptions 找 meta
|
||||
function onSelectSuggestion(value: string) {
|
||||
// 反查 suggestOptions 找 meta
|
||||
const matched = suggestOptions.value.find((o) => o.value === value)
|
||||
if (matched?.type === 'title') {
|
||||
router.push(`/article/${matched.meta.id}`)
|
||||
return
|
||||
}
|
||||
if (matched?.type === 'keyword') {
|
||||
if (matched) {
|
||||
q.value = matched.meta.word
|
||||
resetToFirstPage()
|
||||
return
|
||||
}
|
||||
// 兜底
|
||||
if (/^\d+$/.test(value)) {
|
||||
router.push(`/article/${value}`)
|
||||
} else {
|
||||
// 兜底:value 就是用户要的关键词
|
||||
q.value = value
|
||||
resetToFirstPage()
|
||||
}
|
||||
@@ -762,10 +722,6 @@ onMounted(async () => {
|
||||
line-height: 1.4;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.feed-suggest-tag-title {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
.feed-suggest-tag-keyword {
|
||||
background: #f3e8ff;
|
||||
color: #6b21a8;
|
||||
@@ -777,6 +733,12 @@ onMounted(async () => {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.feed-suggest-weight {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ===== 移动端(<= 768px):过滤条全宽,允许换行 ===== */
|
||||
@media (max-width: 768px) {
|
||||
|
||||
Reference in New Issue
Block a user