✨ feat(tsl-syntax): improve natural language topic routing
This commit is contained in:
@@ -50,6 +50,63 @@ PITFALL_PAGE_BOOST = 18
|
||||
COUNTEREXAMPLE_BOOST = 8
|
||||
EXACT_ERROR_BOOST = 14
|
||||
MIXED_QUERY_MIN_SCORE = 10
|
||||
PAGE_INTENT_SCORE = 80
|
||||
|
||||
CHINESE_STOP_TOKENS = {
|
||||
"一个",
|
||||
"为什",
|
||||
"什么",
|
||||
"怎么",
|
||||
"怎样",
|
||||
"是否",
|
||||
"能不",
|
||||
"不能",
|
||||
"帮我",
|
||||
"想要",
|
||||
"然后",
|
||||
"里面",
|
||||
}
|
||||
|
||||
QUERY_SYNONYMS = {
|
||||
"打印": ("输出", "writeLn"),
|
||||
"打出来": ("输出", "writeLn"),
|
||||
"左连接": ("左联接", "left join", "TS-SQL"),
|
||||
"列表": ("数组",),
|
||||
"复用文件": ("tsf", "unit"),
|
||||
"多个文件": ("unit", "uses", "作用域"),
|
||||
"跳出去": ("break", "控制流"),
|
||||
"程序慢": ("性能分析", "计时", "profiler"),
|
||||
"瓶颈": ("性能分析", "profiler"),
|
||||
"字符串转整数": ("类型转换", "strToInt"),
|
||||
"高性能矩阵": ("FMArray",),
|
||||
}
|
||||
|
||||
PAGE_INTENT_ALIASES = {
|
||||
"01_quickstart.md": ("最简单能跑", "天软脚本"),
|
||||
"02_core_model.md": ("脚本和可复用", "可复用函数文件"),
|
||||
"03_values_and_literals.md": ("字符串和数组下标",),
|
||||
"04_variables_and_constants.md": ("常量怎么声明", "变量能不能直接赋值"),
|
||||
"05_functions_and_calls.md": ("默认参数", "函数怎么带"),
|
||||
"06_expressions_and_operators.md": ("赋值和相等比较",),
|
||||
"07_control_flow.md": ("跳出去", "循环里满足条件"),
|
||||
"08_objects_and_classes.md": ("定义类", "创建对象"),
|
||||
"09_units_and_scope.md": ("多个文件", "复用一组函数"),
|
||||
"10_runtime_context_and_with.md": ("临时切换系统参数",),
|
||||
"11_pitfalls.md": ("声明函数后面写代码", "语法报错"),
|
||||
"12_matrix_and_collections.md": ("某行存在", "二维数组怎么判断"),
|
||||
"13_resultset_and_filters.md": ("保留匹配行", "按某一列"),
|
||||
"14_ts_sql.md": ("左连接", "左联接", "数据库", "分组排序"),
|
||||
"15_debug_and_profiler.md": ("程序慢", "计时找瓶颈", "性能瓶颈"),
|
||||
"16_lexical_structure_and_compile_options.md": ("变量名区分大小写", "注释怎么写"),
|
||||
"17_types_and_conversions.md": ("字符串转整数", "类型转换"),
|
||||
"18_external_calls_and_threads.md": ("调用 dll", "开线程"),
|
||||
"19_namespace_libpath_and_unit_runtime.md": ("找不到 tsf", "搜索路径"),
|
||||
"20_object_runtime_and_introspection.md": ("查看对象属于哪个类", "运行时对象"),
|
||||
"21_builtin_runtime_objects.md": ("内存流",),
|
||||
"22_matrix_deep_dive.md": ("矩阵求逆", "矩阵转置", "求逆和转置"),
|
||||
"23_fmarray.md": ("高性能矩阵", "fmarray"),
|
||||
"24_object_overloads_and_iteration.md": ("自定义对象支持下标", "for in"),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -74,11 +131,12 @@ class ValidationProblem:
|
||||
class QueryMatch:
|
||||
section: Section
|
||||
score: int
|
||||
priority: tuple[int, int, int, int, int] = (0, 0, 0, 0, 0)
|
||||
priority: tuple[int, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ScoreBreakdown:
|
||||
intent: int
|
||||
heading_path: int
|
||||
exact_term: int
|
||||
page_title: int
|
||||
@@ -88,16 +146,23 @@ class ScoreBreakdown:
|
||||
|
||||
@property
|
||||
def lexical_total(self) -> int:
|
||||
return self.heading_path + self.exact_term + self.page_title + self.body
|
||||
return (
|
||||
self.intent
|
||||
+ self.heading_path
|
||||
+ self.exact_term
|
||||
+ self.page_title
|
||||
+ self.body
|
||||
)
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
return self.lexical_total + self.mode_boost
|
||||
|
||||
@property
|
||||
def priority(self) -> tuple[int, int, int, int, int]:
|
||||
def priority(self) -> tuple[int, ...]:
|
||||
if self.diagnose_priority:
|
||||
return (
|
||||
self.intent,
|
||||
self.mode_boost,
|
||||
self.heading_path,
|
||||
self.exact_term,
|
||||
@@ -105,6 +170,7 @@ class ScoreBreakdown:
|
||||
self.body,
|
||||
)
|
||||
return (
|
||||
self.intent,
|
||||
self.heading_path,
|
||||
self.exact_term,
|
||||
self.page_title,
|
||||
@@ -434,13 +500,44 @@ def validate_references(
|
||||
return problems
|
||||
|
||||
|
||||
def query_tokens(text: str) -> set[str]:
|
||||
def _base_query_tokens(text: str) -> set[str]:
|
||||
normalized = normalize(text)
|
||||
tokens = set(ASCII_TOKEN_RE.findall(normalized))
|
||||
for run in CHINESE_RUN_RE.findall(normalized):
|
||||
tokens.add(run)
|
||||
tokens.update(run[index : index + 2] for index in range(len(run) - 1))
|
||||
return {token for token in tokens if token.strip()}
|
||||
return {
|
||||
token
|
||||
for token in tokens
|
||||
if token.strip() and token not in CHINESE_STOP_TOKENS
|
||||
}
|
||||
|
||||
|
||||
def query_tokens(text: str) -> set[str]:
|
||||
tokens = _base_query_tokens(text)
|
||||
normalized = normalize(text)
|
||||
for phrase, synonyms in QUERY_SYNONYMS.items():
|
||||
if normalize(phrase) not in normalized:
|
||||
continue
|
||||
for synonym in synonyms:
|
||||
tokens.update(_base_query_tokens(synonym))
|
||||
return tokens
|
||||
|
||||
|
||||
def _text_contains_token(text: str, token: str) -> bool:
|
||||
normalized_text = normalize(text)
|
||||
if ASCII_TOKEN_RE.fullmatch(token):
|
||||
text_tokens = _bare_tokens(set(ASCII_TOKEN_RE.findall(normalized_text)))
|
||||
return token.rstrip(".$:+-") in text_tokens
|
||||
return token in normalized_text
|
||||
|
||||
|
||||
def _intent_score(section: Section, query: str) -> int:
|
||||
normalized_query = normalize(query)
|
||||
aliases = PAGE_INTENT_ALIASES.get(section.page.name, ())
|
||||
return PAGE_INTENT_SCORE * sum(
|
||||
normalize(alias) in normalized_query for alias in aliases
|
||||
)
|
||||
|
||||
|
||||
def _has_chinese_context(section: Section, query: str) -> bool:
|
||||
@@ -475,13 +572,13 @@ def _score_section(section: Section, query: str, mode: str) -> ScoreBreakdown:
|
||||
page_title_score = 0
|
||||
body_score = 0
|
||||
for token in tokens:
|
||||
if token in heading_text:
|
||||
if _text_contains_token(heading_text, token):
|
||||
heading_score += HEADING_TOKEN_SCORE
|
||||
if token in term_text:
|
||||
if _text_contains_token(term_text, token):
|
||||
term_score += TERM_TOKEN_SCORE
|
||||
if token in page_title_text:
|
||||
if _text_contains_token(page_title_text, token):
|
||||
page_title_score += PAGE_TITLE_TOKEN_SCORE
|
||||
if token in body_text:
|
||||
if _text_contains_token(body_text, token):
|
||||
body_score += BODY_TOKEN_SCORE
|
||||
if normalized_query and normalized_query in heading_text:
|
||||
heading_score += HEADING_EXACT_SCORE
|
||||
@@ -502,6 +599,7 @@ def _score_section(section: Section, query: str, mode: str) -> ScoreBreakdown:
|
||||
if normalized_query and normalized_query in section.searchable_text:
|
||||
mode_boost += EXACT_ERROR_BOOST
|
||||
return ScoreBreakdown(
|
||||
intent=_intent_score(section, query),
|
||||
heading_path=heading_score,
|
||||
exact_term=term_score,
|
||||
page_title=page_title_score,
|
||||
@@ -577,13 +675,16 @@ def query_sections(
|
||||
all_sections = load_sections(references_dir)
|
||||
sections = all_sections
|
||||
ascii_tokens = {
|
||||
token for token in query_tokens(query) if ASCII_TOKEN_RE.fullmatch(token)
|
||||
token for token in _base_query_tokens(query) if ASCII_TOKEN_RE.fullmatch(token)
|
||||
}
|
||||
if ascii_tokens:
|
||||
sections = [
|
||||
section
|
||||
for section in sections
|
||||
if any(token in section.searchable_text for token in ascii_tokens)
|
||||
if any(
|
||||
_text_contains_token(section.searchable_text, token)
|
||||
for token in ascii_tokens
|
||||
)
|
||||
]
|
||||
has_chinese = bool(CHINESE_RUN_RE.search(normalize(query)))
|
||||
minimum_score = MIXED_QUERY_MIN_SCORE if ascii_tokens and has_chinese else 1
|
||||
@@ -612,12 +713,16 @@ def query_sections(
|
||||
# H2 聚合 section 的正文逐字包含其 H3 子节;父子同时入选时只保留
|
||||
# 排名更高的一个,避免同一内容重复返回。
|
||||
matches: list[QueryMatch] = []
|
||||
page_counts: dict[Path, int] = {}
|
||||
for match in ranked:
|
||||
if any(
|
||||
_related_sections(match.section, kept.section) for kept in matches
|
||||
):
|
||||
continue
|
||||
if page_counts.get(match.section.page, 0) >= 2:
|
||||
continue
|
||||
matches.append(match)
|
||||
page_counts[match.section.page] = page_counts.get(match.section.page, 0) + 1
|
||||
if len(matches) == limit:
|
||||
break
|
||||
prelude = _write_prelude(all_sections) if mode == "write" else []
|
||||
|
||||
Reference in New Issue
Block a user