(
user_id: str,
query: str,
node_type: Optional[str] = None,
limit: int = 20,
)
| 629 | |
| 630 | |
| 631 | def search_nodes( |
| 632 | user_id: str, |
| 633 | query: str, |
| 634 | node_type: Optional[str] = None, |
| 635 | limit: int = 20, |
| 636 | ) -> List[Dict[str, Any]]: |
| 637 | init_wiki_schema() |
| 638 | query_text = (query or "").strip() |
| 639 | if not query_text: |
| 640 | return list_nodes(user_id, node_type=node_type, limit=limit) |
| 641 | |
| 642 | keyword_hits = _keyword_search_nodes(user_id, query_text, node_type, max(limit, 30)) |
| 643 | try: |
| 644 | vector_hits = vector_search_nodes(user_id, query_text, node_type=node_type, limit=max(limit, 30)) |
| 645 | except Exception: |
| 646 | vector_hits = [] |
| 647 | merged: Dict[str, Dict[str, Any]] = {} |
| 648 | |
| 649 | for node in keyword_hits: |
| 650 | merged[node["node_id"]] = node |
| 651 | vector_scores = [float(node.get("vector_score", 0.0)) for node in vector_hits] |
| 652 | vector_min = min(vector_scores) if vector_scores else 0.0 |
| 653 | vector_max = max(vector_scores) if vector_scores else 1.0 |
| 654 | vector_range = max(vector_max - vector_min, 1e-6) |
| 655 | for node in vector_hits: |
| 656 | existing = merged.setdefault(node["node_id"], node) |
| 657 | existing["vector_score"] = float(node.get("vector_score", 0.0)) |
| 658 | existing["vector_score_norm"] = (existing["vector_score"] - vector_min) / vector_range |
| 659 | |
| 660 | for node in merged.values(): |
| 661 | keyword_score = float(node.get("keyword_score", 0.0)) |
| 662 | vector_score = float(node.get("vector_score_norm", 0.0)) |
| 663 | interaction_bonus = _interaction_bonus(user_id, node) |
| 664 | type_weight = {"section": 1.0, "paper": 0.9, "trajectory": 0.8, "topic": 0.75}.get( |
| 665 | str(node.get("node_type")), |
| 666 | 0.7, |
| 667 | ) |
| 668 | node["score"] = (0.45 * keyword_score + 0.55 * vector_score + interaction_bonus) * type_weight |
| 669 | |
| 670 | results = sorted(merged.values(), key=lambda item: float(item.get("score", 0.0)), reverse=True) |
| 671 | return results[: max(1, int(limit))] |
| 672 | |
| 673 | |
| 674 | def _interaction_bonus(user_id: str, node: Dict[str, Any]) -> float: |
nothing calls this directly
no test coverage detected