|
|
@@ -45,11 +45,22 @@ def run(
|
|
|
pattern_seed_pack: dict[str, Any],
|
|
|
runtime: RuntimeFileStore,
|
|
|
query_variant_client: QueryVariantClient,
|
|
|
+ *,
|
|
|
+ strategy_version: str | None = None,
|
|
|
) -> list[dict[str, Any]]:
|
|
|
created_at = datetime.now(timezone.utc).isoformat()
|
|
|
seed_terms = _terms(pattern_seed_pack.get("seed_terms"))
|
|
|
if not seed_terms:
|
|
|
raise _query_generation_error("seed_terms_empty")
|
|
|
+ if str(strategy_version or "").upper() == "V4":
|
|
|
+ return _run_v4(
|
|
|
+ run_id,
|
|
|
+ policy_run_id,
|
|
|
+ pattern_seed_pack,
|
|
|
+ runtime,
|
|
|
+ created_at,
|
|
|
+ seed_terms,
|
|
|
+ )
|
|
|
|
|
|
profile = getattr(query_variant_client, "profile", DEFAULT_PROFILE)
|
|
|
variants_per_seed = int(profile.get("variants_per_seed", 1))
|
|
|
@@ -129,6 +140,214 @@ def run(
|
|
|
return search_queries
|
|
|
|
|
|
|
|
|
+def _run_v4(
|
|
|
+ run_id: str,
|
|
|
+ policy_run_id: str,
|
|
|
+ pattern_seed_pack: dict[str, Any],
|
|
|
+ runtime: RuntimeFileStore,
|
|
|
+ created_at: str,
|
|
|
+ seed_terms: list[str],
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
+ candidates = [
|
|
|
+ *_v4_seed_candidates(pattern_seed_pack, seed_terms),
|
|
|
+ *_v4_piaoquan_point_candidates(pattern_seed_pack),
|
|
|
+ *_v4_category_element_candidates(pattern_seed_pack),
|
|
|
+ ]
|
|
|
+ if not candidates:
|
|
|
+ raise _query_generation_error("v4_query_candidates_empty")
|
|
|
+
|
|
|
+ search_queries: list[dict[str, Any]] = []
|
|
|
+ seen_queries: set[str] = set()
|
|
|
+ for candidate in candidates:
|
|
|
+ query = _normalize_query(candidate.get("text"))
|
|
|
+ if not query:
|
|
|
+ continue
|
|
|
+ normalized_key = _normalize_query_key(query)
|
|
|
+ if normalized_key in seen_queries:
|
|
|
+ continue
|
|
|
+ seen_queries.add(normalized_key)
|
|
|
+ query_id = f"q_{len(search_queries) + 1:03d}"
|
|
|
+ row = _base_query_record(
|
|
|
+ run_id=run_id,
|
|
|
+ policy_run_id=policy_run_id,
|
|
|
+ search_query_id=query_id,
|
|
|
+ search_query=query,
|
|
|
+ generation_method=candidate["generation_method"],
|
|
|
+ seed_term=query,
|
|
|
+ pattern_seed_ref=_v4_pattern_seed_ref(pattern_seed_pack, candidate, query),
|
|
|
+ created_at=created_at,
|
|
|
+ query_source_terms=[query],
|
|
|
+ query_source_fields=candidate["query_source_fields"],
|
|
|
+ )
|
|
|
+ row["query_source_refs"] = [_v4_query_source_ref(candidate, query)]
|
|
|
+ search_queries.append(row)
|
|
|
+
|
|
|
+ if not search_queries:
|
|
|
+ raise _query_generation_error("v4_search_queries_empty")
|
|
|
+
|
|
|
+ search_queries = [with_raw_payload(row) for row in search_queries]
|
|
|
+ runtime.append_jsonl(run_id, "search_queries.jsonl", search_queries)
|
|
|
+ return search_queries
|
|
|
+
|
|
|
+
|
|
|
+def _v4_seed_candidates(
|
|
|
+ pattern_seed_pack: dict[str, Any],
|
|
|
+ seed_terms: list[str],
|
|
|
+) -> list[dict[str, Any]]:
|
|
|
+ return [
|
|
|
+ {
|
|
|
+ "text": seed_term,
|
|
|
+ "query_source_type": "seed_term",
|
|
|
+ "generation_method": "seed_term",
|
|
|
+ "query_source_fields": ["seed_terms"],
|
|
|
+ "rank": index + 1,
|
|
|
+ "source_ref": {
|
|
|
+ "source_field": "seed_terms",
|
|
|
+ "source_index": index,
|
|
|
+ "seed_term": seed_term,
|
|
|
+ "query_source_ref_id": f"seed_terms:{index}",
|
|
|
+ },
|
|
|
+ }
|
|
|
+ for index, seed_term in enumerate(seed_terms)
|
|
|
+ ]
|
|
|
+
|
|
|
+
|
|
|
+def _v4_piaoquan_point_candidates(pattern_seed_pack: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
+ candidates: list[dict[str, Any]] = []
|
|
|
+ for binding_index, binding in enumerate(_bindings(pattern_seed_pack), start=1):
|
|
|
+ for point_index, sample in enumerate(_sample_elements(binding), start=1):
|
|
|
+ point_type = str(sample.get("point_type") or "").strip()
|
|
|
+ if point_type not in {"目的点", "灵感点"}:
|
|
|
+ continue
|
|
|
+ point_text = _normalize_query(sample.get("point_text"))
|
|
|
+ if not point_text:
|
|
|
+ continue
|
|
|
+ text = _cleanup_purpose_point(point_text) if point_type == "目的点" else point_text
|
|
|
+ if not text:
|
|
|
+ continue
|
|
|
+ rank = len(candidates) + 1
|
|
|
+ source_ref = {
|
|
|
+ "query_source_ref_id": _point_source_ref_id(sample, rank),
|
|
|
+ "binding_rank": binding_index,
|
|
|
+ "point_rank": point_index,
|
|
|
+ "point_type": point_type,
|
|
|
+ "point_text": point_text,
|
|
|
+ "cleaned_text": text,
|
|
|
+ "post_id": sample.get("post_id"),
|
|
|
+ "point_id": sample.get("id") or sample.get("topic_point_id"),
|
|
|
+ "element_id": sample.get("source_element_id") or sample.get("id"),
|
|
|
+ "category_id": sample.get("category_id") or binding.get("category_id"),
|
|
|
+ }
|
|
|
+ candidates.append(
|
|
|
+ {
|
|
|
+ "text": text,
|
|
|
+ "query_source_type": "piaoquan_point_text",
|
|
|
+ "generation_method": "piaoquan_topic_point",
|
|
|
+ "query_source_fields": ["element_bindings.sample_elements.point_text"],
|
|
|
+ "rank": rank,
|
|
|
+ "source_ref": source_ref,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return candidates
|
|
|
+
|
|
|
+
|
|
|
+def _v4_category_element_candidates(pattern_seed_pack: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
+ candidates: list[dict[str, Any]] = []
|
|
|
+ for binding_index, binding in enumerate(_bindings(pattern_seed_pack), start=1):
|
|
|
+ for element_index, sample in enumerate(_sample_elements(binding), start=1):
|
|
|
+ name = _normalize_query(sample.get("name"))
|
|
|
+ if not name:
|
|
|
+ continue
|
|
|
+ rank = len(candidates) + 1
|
|
|
+ category_id = sample.get("category_id") or binding.get("category_id")
|
|
|
+ element_id = sample.get("source_element_id") or sample.get("id")
|
|
|
+ source_ref = {
|
|
|
+ "query_source_ref_id": _element_source_ref_id(category_id, element_id, name, rank),
|
|
|
+ "binding_rank": binding_index,
|
|
|
+ "element_rank": element_index,
|
|
|
+ "element_name": name,
|
|
|
+ "element_id": element_id,
|
|
|
+ "category_id": category_id,
|
|
|
+ "itemset_item_id": binding.get("itemset_item_id"),
|
|
|
+ "post_id": sample.get("post_id"),
|
|
|
+ "point_type": sample.get("point_type"),
|
|
|
+ }
|
|
|
+ candidates.append(
|
|
|
+ {
|
|
|
+ "text": name,
|
|
|
+ "query_source_type": "category_terminal_element",
|
|
|
+ "generation_method": "category_leaf_element",
|
|
|
+ "query_source_fields": ["element_bindings.sample_elements.name"],
|
|
|
+ "rank": rank,
|
|
|
+ "source_ref": source_ref,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ return candidates
|
|
|
+
|
|
|
+
|
|
|
+def _bindings(pattern_seed_pack: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
+ values = pattern_seed_pack.get("element_bindings")
|
|
|
+ return [item for item in values if isinstance(item, dict)] if isinstance(values, list) else []
|
|
|
+
|
|
|
+
|
|
|
+def _sample_elements(binding: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
+ values = binding.get("sample_elements")
|
|
|
+ return [item for item in values if isinstance(item, dict)] if isinstance(values, list) else []
|
|
|
+
|
|
|
+
|
|
|
+def _cleanup_purpose_point(value: str) -> str:
|
|
|
+ text = _normalize_query(value)
|
|
|
+ for prefix in ("分享", "讲述", "表达", "传递", "呼吁", "介绍", "展示", "记录", "呈现"):
|
|
|
+ if text.startswith(prefix) and len(text) > len(prefix):
|
|
|
+ text = text[len(prefix):].strip(" ,,。::")
|
|
|
+ break
|
|
|
+ return _normalize_query(text)
|
|
|
+
|
|
|
+
|
|
|
+def _point_source_ref_id(sample: dict[str, Any], rank: int) -> str:
|
|
|
+ post_id = sample.get("post_id") or "unknown_post"
|
|
|
+ point_id = sample.get("id") or sample.get("topic_point_id") or rank
|
|
|
+ return f"post:{post_id}#point:{point_id}"
|
|
|
+
|
|
|
+
|
|
|
+def _element_source_ref_id(category_id: Any, element_id: Any, name: str, rank: int) -> str:
|
|
|
+ category = category_id or "unknown_category"
|
|
|
+ element = element_id or name or rank
|
|
|
+ return f"category:{category}#element:{element}"
|
|
|
+
|
|
|
+
|
|
|
+def _v4_pattern_seed_ref(
|
|
|
+ pattern_seed_pack: dict[str, Any],
|
|
|
+ candidate: dict[str, Any],
|
|
|
+ query: str,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ source_ref = dict(candidate.get("source_ref") or {})
|
|
|
+ return {
|
|
|
+ "query_source_type": candidate["query_source_type"],
|
|
|
+ "query_source_ref_id": source_ref.get("query_source_ref_id"),
|
|
|
+ "query_source_text": query,
|
|
|
+ "query_source_rank": candidate["rank"],
|
|
|
+ "pattern_execution_id": pattern_seed_pack.get("pattern_execution_id"),
|
|
|
+ "mining_config_id": pattern_seed_pack.get("mining_config_id"),
|
|
|
+ "source_post_id": pattern_seed_pack.get("source_post_id"),
|
|
|
+ "matched_post_ids": pattern_seed_pack.get("matched_post_ids") or [],
|
|
|
+ "itemset_ids": _itemset_ids(pattern_seed_pack),
|
|
|
+ "source_ref": source_ref,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _v4_query_source_ref(candidate: dict[str, Any], query: str) -> dict[str, Any]:
|
|
|
+ source_ref = dict(candidate.get("source_ref") or {})
|
|
|
+ return {
|
|
|
+ "query_source_type": candidate["query_source_type"],
|
|
|
+ "query_source_ref_id": source_ref.get("query_source_ref_id"),
|
|
|
+ "query_source_text": query,
|
|
|
+ "query_source_rank": candidate["rank"],
|
|
|
+ "generation_method": candidate["generation_method"],
|
|
|
+ "source_ref": source_ref,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
def _terms(values: Any) -> list[str]:
|
|
|
if not isinstance(values, list):
|
|
|
return []
|
|
|
@@ -155,6 +374,8 @@ def _base_query_record(
|
|
|
seed_term: str,
|
|
|
pattern_seed_ref: dict[str, Any],
|
|
|
created_at: str,
|
|
|
+ query_source_terms: list[str] | None = None,
|
|
|
+ query_source_fields: list[str] | None = None,
|
|
|
) -> dict[str, Any]:
|
|
|
return {
|
|
|
"record_schema_version": RUNTIME_RECORD_SCHEMA_VERSION,
|
|
|
@@ -166,8 +387,8 @@ def _base_query_record(
|
|
|
"discovery_start_source": "pattern_itemset",
|
|
|
"previous_discovery_step": "pattern_search_query",
|
|
|
"search_query_effect_status": "pending",
|
|
|
- "query_source_terms": [seed_term],
|
|
|
- "query_source_fields": ["seed_terms"],
|
|
|
+ "query_source_terms": query_source_terms or [seed_term],
|
|
|
+ "query_source_fields": query_source_fields or ["seed_terms"],
|
|
|
"pattern_seed_ref": pattern_seed_ref,
|
|
|
"created_at": created_at,
|
|
|
}
|
|
|
@@ -338,6 +559,10 @@ def _normalize_query(value: Any) -> str:
|
|
|
return " ".join(value.split()).strip()
|
|
|
|
|
|
|
|
|
+def _normalize_query_key(value: Any) -> str:
|
|
|
+ return _normalize_query(value).casefold()
|
|
|
+
|
|
|
+
|
|
|
def _is_generic_query(query: str, generic_filter: dict[str, Any] | None = None) -> bool:
|
|
|
generic_queries = set((generic_filter or {}).get("queries") or GENERIC_QUERIES)
|
|
|
generic_tokens = tuple((generic_filter or {}).get("tokens") or GENERIC_QUERY_TOKENS)
|