fix: 검색 UX 개선 — Enter 키 기반 + 한국어 검색 ILIKE fallback
- 프론트: debounce 자동검색 제거 → Enter 키로만 검색 (한글 조합 문제 해결) - 백엔드: trgm threshold 0.1로 낮춤 + ILIKE '%검색어%' fallback 추가 - hybrid 검색 score threshold 0.01 → 0.001로 낮춤 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,17 +93,21 @@ async def _search_fts(session: AsyncSession, query: str, limit: int) -> list[Sea
|
|||||||
|
|
||||||
|
|
||||||
async def _search_trgm(session: AsyncSession, query: str, limit: int) -> list[SearchResult]:
|
async def _search_trgm(session: AsyncSession, query: str, limit: int) -> list[SearchResult]:
|
||||||
"""트리그램 부분매칭 (한국어 지원)"""
|
"""트리그램 부분매칭 + ILIKE fallback (한국어 지원)"""
|
||||||
|
# threshold 낮춰서 한국어 매칭 향상
|
||||||
|
await session.execute(text("SET pg_trgm.similarity_threshold = 0.1"))
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT id, title, ai_domain, ai_summary, file_format,
|
SELECT id, title, ai_domain, ai_summary, file_format,
|
||||||
similarity(
|
GREATEST(
|
||||||
coalesce(title, '') || ' ' || coalesce(extracted_text, ''),
|
similarity(coalesce(title, '') || ' ' || coalesce(extracted_text, ''), :query),
|
||||||
:query
|
CASE WHEN (coalesce(title, '') || ' ' || coalesce(extracted_text, '')) ILIKE '%' || :query || '%'
|
||||||
|
THEN 0.5 ELSE 0 END
|
||||||
) AS score,
|
) AS score,
|
||||||
left(extracted_text, 200) AS snippet
|
left(extracted_text, 200) AS snippet
|
||||||
FROM documents
|
FROM documents
|
||||||
WHERE (coalesce(title, '') || ' ' || coalesce(extracted_text, '')) %% :query
|
WHERE (coalesce(title, '') || ' ' || coalesce(extracted_text, '')) %% :query
|
||||||
|
OR (coalesce(title, '') || ' ' || coalesce(extracted_text, '')) ILIKE '%' || :query || '%'
|
||||||
ORDER BY score DESC
|
ORDER BY score DESC
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
"""),
|
"""),
|
||||||
@@ -182,8 +186,9 @@ async def _search_hybrid(session: AsyncSession, query: str, limit: int) -> list[
|
|||||||
FROM documents d
|
FROM documents d
|
||||||
{vector_clause}
|
{vector_clause}
|
||||||
WHERE coalesce(d.extracted_text, '') != ''
|
WHERE coalesce(d.extracted_text, '') != ''
|
||||||
|
OR (coalesce(d.title, '') || ' ' || coalesce(d.extracted_text, '')) ILIKE '%' || :query || '%'
|
||||||
) sub
|
) sub
|
||||||
WHERE sub.score > 0.01
|
WHERE sub.score > 0.001
|
||||||
ORDER BY sub.score DESC
|
ORDER BY sub.score DESC
|
||||||
LIMIT :limit
|
LIMIT :limit
|
||||||
"""),
|
"""),
|
||||||
|
|||||||
@@ -63,27 +63,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchInput() {
|
function submitSearch() {
|
||||||
clearTimeout(debounceTimer);
|
const params = new URLSearchParams($page.url.searchParams);
|
||||||
debounceTimer = setTimeout(() => {
|
params.delete('page');
|
||||||
const params = new URLSearchParams($page.url.searchParams);
|
if (searchQuery.trim()) {
|
||||||
params.delete('page');
|
params.set('q', searchQuery.trim());
|
||||||
if (searchQuery.trim()) {
|
} else {
|
||||||
params.set('q', searchQuery.trim());
|
params.delete('q');
|
||||||
} else {
|
}
|
||||||
params.delete('q');
|
if (searchMode !== 'hybrid') {
|
||||||
}
|
params.set('mode', searchMode);
|
||||||
if (searchMode !== 'hybrid') {
|
} else {
|
||||||
params.set('mode', searchMode);
|
params.delete('mode');
|
||||||
} else {
|
}
|
||||||
params.delete('mode');
|
for (const [key, val] of [...params.entries()]) {
|
||||||
}
|
if (!val) params.delete(key);
|
||||||
for (const [key, val] of [...params.entries()]) {
|
}
|
||||||
if (!val) params.delete(key);
|
const qs = params.toString();
|
||||||
}
|
goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
|
||||||
const qs = params.toString();
|
}
|
||||||
goto(`/documents${qs ? '?' + qs : ''}`, { noScroll: true });
|
|
||||||
}, 300);
|
function handleSearchKeydown(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
submitSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doSearch(q, mode) {
|
async function doSearch(q, mode) {
|
||||||
@@ -145,13 +149,13 @@
|
|||||||
data-search-input
|
data-search-input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
oninput={handleSearchInput}
|
onkeydown={handleSearchKeydown}
|
||||||
placeholder="검색어 입력... (/ 키로 포커스)"
|
placeholder="검색어 입력 후 Enter (/ 키로 포커스)"
|
||||||
class="flex-1 px-3 py-1.5 bg-[var(--surface)] border border-[var(--border)] rounded-lg text-[var(--text)] text-sm focus:border-[var(--accent)] outline-none"
|
class="flex-1 px-3 py-1.5 bg-[var(--surface)] border border-[var(--border)] rounded-lg text-[var(--text)] text-sm focus:border-[var(--accent)] outline-none"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
bind:value={searchMode}
|
bind:value={searchMode}
|
||||||
onchange={() => { if (searchQuery) handleSearchInput(); }}
|
onchange={() => { if (searchQuery) submitSearch(); }}
|
||||||
class="px-2 py-1.5 bg-[var(--surface)] border border-[var(--border)] rounded-lg text-[var(--text)] text-xs"
|
class="px-2 py-1.5 bg-[var(--surface)] border border-[var(--border)] rounded-lg text-[var(--text)] text-xs"
|
||||||
>
|
>
|
||||||
<option value="hybrid">하이브리드</option>
|
<option value="hybrid">하이브리드</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user