2026-10-20 TUE UTC 12:42 PM TPE 8:42 PM 每小時自動更新 中文 EN
LIVE 快訊
← 返回網站更新紀錄 Site Changelog

提交原始紀錄

Fix suggested search queries returning no results (#182)

SHA
e8e1f7b7800055d0fa3f466c022dc8a8db5fbdab
提交時間
作者
nnneeeooo-nnneeeooo
提交者
web-flow

變更檔案 (3)

  • static/app.js modified · +10 / −1
  • static/search.js modified · +25 / −8
  • tests/test_search.py modified · +37 / −1

完整差異

@@ -65,14 +65,23 @@\n     } catch (e) { /* keep the server-rendered fallback */ }\n   }\n   if (headerSearchForm && headerSearchInput) {\n-    headerSearchForm.addEventListener("submit", function () {\n+    headerSearchForm.addEventListener("submit", function (event) {\n       if (headerSearchInput.value.trim()) return;\n       var suggestedQuery = queryFromSearchPlaceholder(\n         headerSearchInput.placeholder\n       );\n       if (!suggestedQuery) return;\n       headerSearchInput.value = suggestedQuery;\n       headerSearchInput.dispatchEvent(new Event("input", { bubbles: true }));\n+      if (document.getElementById("news-search-app")) return;\n+\n+      /* Build the navigation URL explicitly.  Some browsers snapshot the\n+         form controls before this submit handler fills the suggestion, which\n+         otherwise opens the search page without q. */\n+      event.preventDefault();\n+      var searchUrl = new URL(headerSearchForm.action, window.location.href);\n+      searchUrl.searchParams.set(headerSearchInput.name || "q", suggestedQuery);\n+      window.location.assign(searchUrl.toString());\n     });\n   }\n   if (headerSearchInput && headerSearchClear) {\n@@ -113,23 +113,37 @@\n     return card;\n   }\n \n-  function scoreRecord(record, normalizedQuery, terms) {\n+  function scoreRecord(record, normalizedQuery, terms, minimumMatches) {\n     var haystack = recordSearchText(record);\n-    if (!terms.every(function (term) { return haystack.indexOf(term) !== -1; })) {\n+    var matchedTerms = terms.filter(function (term) {\n+      return haystack.indexOf(term) !== -1;\n+    });\n+    if (matchedTerms.length < minimumMatches) {\n       return -1;\n     }\n     var title = normalize(localized(record, "title"));\n     var summary = normalize(localized(record, "summary"));\n     var score = 1;\n     if (title === normalizedQuery) score += 200;\n     else if (title.indexOf(normalizedQuery) !== -1) score += 100;\n-    terms.forEach(function (term) {\n+    matchedTerms.forEach(function (term) {\n       if (title.indexOf(term) !== -1) score += 20;\n       if (summary.indexOf(term) !== -1) score += 6;\n     });\n     return score;\n   }\n \n+  function matchingRecords(normalizedQuery, terms, minimumMatches) {\n+    return records.map(function (record) {\n+      return {\n+        record: record,\n+        score: scoreRecord(record, normalizedQuery, terms, minimumMatches)\n+      };\n+    }).filter(function (row) {\n+      return row.score >= 0;\n+    });\n+  }\n+\n   function runSearch() {\n     var query = input.value.trim();\n     updateQueryUrl(query);\n@@ -148,11 +162,14 @@\n \n     var normalizedQuery = normalize(query);\n     var terms = normalizedQuery.split(/\s+/).filter(Boolean);\n-    var matches = records.map(function (record) {\n-      return { record: record, score: scoreRecord(record, normalizedQuery, terms) };\n-    }).filter(function (row) {\n-      return row.score >= 0;\n-    }).sort(function (left, right) {\n+    var matches = matchingRecords(normalizedQuery, terms, terms.length);\n+    if (!matches.length && terms.length >= 3) {\n+      /* Daily suggestions may paraphrase one phrase from the source title.\n+         Keep normal searches strict, then allow one unmatched term only when\n+         the strict pass produced nothing. */\n+      matches = matchingRecords(normalizedQuery, terms, terms.length - 1);\n+    }\n+    matches.sort(function (left, right) {\n       if (right.score !== left.score) return right.score - left.score;\n       return String(right.record.published || "").localeCompare(String(left.record.published || ""));\n     });\n@@ -4,6 +4,7 @@\n import json\n import re\n import sys\n+import unicodedata\n from pathlib import Path\n \n ROOT = Path(__file__).resolve().parent.parent\n@@ -32,6 +33,32 @@ def sample_article(title: str):\n     })\n \n \n+def normalize_search_text(value: str) -> str:\n+    value = unicodedata.normalize("NFKC", str(value or "")).lower()\n+    return re.sub(r"[^\w]+", " ", value).strip()\n+\n+\n+def suggestion_has_result(prompt: str, items: list[dict]) -> bool:\n+    terms = normalize_search_text(prompt).split()\n+    minimum_matches = len(terms) if len(terms) < 3 else len(terms) - 1\n+    for item in items:\n+        values = [\n+            item.get("id", ""), item.get("source", ""),\n+            item.get("date", ""), item.get("published", ""),\n+            item.get("search", ""),\n+        ]\n+        for field in ("title", "summary", "category"):\n+            localized = item.get(field, {})\n+            if isinstance(localized, dict):\n+                values.extend((localized.get("zh", ""), localized.get("en", "")))\n+            else:\n+                values.append(localized)\n+        haystack = normalize_search_text(" ".join(values))\n+        if sum(term in haystack for term in terms) >= minimum_matches:\n+            return True\n+    return False\n+\n+\n def main() -> None:\n     aliases = build.load_search_alias_groups()\n     assert len(aliases) >= 25\n@@ -92,6 +119,10 @@ def main() -> None:\n     assert zh_placeholders == list(build.header_search_placeholders("zh"))\n     assert en_placeholders == list(build.header_search_placeholders("en"))\n     assert len(zh_placeholders) == len(en_placeholders) == 6\n+    assert all(suggestion_has_result(prompt, payload["items"])\n+               for prompt in zh_placeholders + en_placeholders)\n+    assert suggestion_has_result(\n+        "亞馬遜空運 貨機 邁阿密 衝出跑道", payload["items"])\n     assert zh.count('id="news-search-form"') == 1\n     assert 'class="header-search-form"' in home\n     assert 'class="header-search-form"' in article\n@@ -117,7 +148,8 @@ def main() -> None:\n     assert ".header-search-submit" in css\n     assert "航空公司正式名稱與常用簡稱可互相查找" in zh\n     assert "Official airline names and common short names are interchangeable" in en\n-    assert "terms.every" in script\n+    assert "function matchingRecords" in script\n+    assert "terms.length - 1" in script\n     assert "function loadIndex()" in script\n     assert "function recordSearchText" in script\n     assert "if (input.value.trim()) runSearch();" in script\n@@ -145,6 +177,10 @@ def main() -> None:\n     assert "Search|Explore|Find|Look\\s+up|Show\\s+me" in app_script\n     assert "if (headerSearchInput.value.trim()) return" in app_script\n     assert "headerSearchInput.value = suggestedQuery" in app_script\n+    assert 'document.getElementById("news-search-app")' in app_script\n+    assert "event.preventDefault()" in app_script\n+    assert 'searchUrl.searchParams.set(headerSearchInput.name || "q", suggestedQuery)' in app_script\n+    assert "window.location.assign(searchUrl.toString())" in app_script\n     assert 'headerSearchInput.value = ""' in app_script\n     assert 'new Event("input", { bubbles: true })' in app_script\n 

你發現了隱藏主題!

8-bit 主題已解鎖。重新整理後,就能在主題選單選擇 8-bit。