From f6000de2301344e7eff39b0d0a751d97c0fde47e Mon Sep 17 00:00:00 2001 From: David Blanc Brioir Date: Thu, 1 Jan 2026 14:24:44 +0100 Subject: [PATCH] feat: Add force_hierarchical mode to prevent fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes Allow users to force hierarchical search mode without fallback to simple search, enabling testing of hierarchical UI even when 0 summaries are found. **Backend (flask_app.py):** - Added `force_hierarchical` parameter to `hierarchical_search()` - When True, never fallback to simple search (return empty hierarchical result) - Added `fallback_reason` field to explain why no results - Pass `force_hierarchical=True` when `force_mode == "hierarchical"` - Applied to all fallback points: - No Weaviate client - No summaries found in Stage 1 - No sections after author/work filtering - Exception during search **Frontend (templates/search.html):** - Display warning message when `fallback_reason` exists - Yellow alert box with explanation and suggestions - Works even when `results_data.results` is empty ## Usage 1. Select "🌳 HiΓ©rarchique (2-Γ©tapes)" in Mode dropdown 2. Enter any query (even if no matching summaries) 3. See hierarchical UI with warning instead of fallback ## Example Query: "Qu'est-ce que la justice ?" (not in Peirce corpus) - Mode forced: Hierarchical - Result: 0 sections, warning displayed - No silent fallback to simple search πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- generations/library_rag/flask_app.py | 105 +++++++++++++----- generations/library_rag/templates/search.html | 13 ++- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/generations/library_rag/flask_app.py b/generations/library_rag/flask_app.py index 9755a23..345ac38 100644 --- a/generations/library_rag/flask_app.py +++ b/generations/library_rag/flask_app.py @@ -291,6 +291,7 @@ def hierarchical_search( author_filter: Optional[str] = None, work_filter: Optional[str] = None, sections_limit: int = 5, + force_hierarchical: bool = False, ) -> Dict[str, Any]: """Two-stage hierarchical semantic search: Summary β†’ Chunks. @@ -303,6 +304,7 @@ def hierarchical_search( author_filter: Filter by author name. work_filter: Filter by work title. sections_limit: Number of top sections to retrieve (default: 5). + force_hierarchical: If True, never fallback to simple search (for testing). Returns: Dictionary with hierarchical search results: @@ -310,17 +312,28 @@ def hierarchical_search( - sections: List of section dictionaries with nested chunks - results: Flat list of all chunks (for compatibility) - total_chunks: Total number of chunks found + - fallback_reason: Explanation if forced but 0 results (optional) """ try: with get_weaviate_client() as client: if client is None: - # Fallback to simple search - results = simple_search(query, limit, author_filter, work_filter) - return { - "mode": "simple", - "results": results, - "total_chunks": len(results), - } + # Fallback to simple search only if not forced + if not force_hierarchical: + results = simple_search(query, limit, author_filter, work_filter) + return { + "mode": "simple", + "results": results, + "total_chunks": len(results), + } + else: + # Forced hierarchical with no client + return { + "mode": "hierarchical", + "sections": [], + "results": [], + "total_chunks": 0, + "fallback_reason": "Weaviate client unavailable", + } # ═══════════════════════════════════════════════════════════════ # STAGE 1: Search Summary collection for relevant sections @@ -338,13 +351,24 @@ def hierarchical_search( ) if not summaries_result.objects: - # No summaries found, fallback to simple search - results = simple_search(query, limit, author_filter, work_filter) - return { - "mode": "simple", - "results": results, - "total_chunks": len(results), - } + # No summaries found + if not force_hierarchical: + # Auto-detection: fallback to simple search + results = simple_search(query, limit, author_filter, work_filter) + return { + "mode": "simple", + "results": results, + "total_chunks": len(results), + } + else: + # Forced hierarchical: return empty hierarchical result + return { + "mode": "hierarchical", + "sections": [], + "results": [], + "total_chunks": 0, + "fallback_reason": f"Aucune section pertinente trouvΓ©e (0/{sections_limit} summaries)", + } # Extract section data sections_data = [] @@ -393,13 +417,27 @@ def hierarchical_search( sections_data = filtered_sections if not sections_data: - # No sections match filters, fallback to simple search - results = simple_search(query, limit, author_filter, work_filter) - return { - "mode": "simple", - "results": results, - "total_chunks": len(results), - } + # No sections match filters + if not force_hierarchical: + # Auto-detection: fallback to simple search + results = simple_search(query, limit, author_filter, work_filter) + return { + "mode": "simple", + "results": results, + "total_chunks": len(results), + } + else: + # Forced hierarchical: return empty hierarchical result + filters_str = f"author={author_filter}" if author_filter else "" + if work_filter: + filters_str += f", work={work_filter}" if filters_str else f"work={work_filter}" + return { + "mode": "hierarchical", + "sections": [], + "results": [], + "total_chunks": 0, + "fallback_reason": f"Aucune section ne correspond aux filtres ({filters_str})", + } # ═══════════════════════════════════════════════════════════════ # STAGE 2: Search Chunk collection filtered by sections @@ -461,13 +499,23 @@ def hierarchical_search( except Exception as e: print(f"Erreur recherche hiΓ©rarchique: {e}") - # Fallback to simple search on error - results = simple_search(query, limit, author_filter, work_filter) - return { - "mode": "simple", - "results": results, - "total_chunks": len(results), - } + # Fallback to simple search on error (unless forced) + if not force_hierarchical: + results = simple_search(query, limit, author_filter, work_filter) + return { + "mode": "simple", + "results": results, + "total_chunks": len(results), + } + else: + # Forced hierarchical: return error in hierarchical format + return { + "mode": "hierarchical", + "sections": [], + "results": [], + "total_chunks": 0, + "fallback_reason": f"Erreur lors de la recherche hiΓ©rarchique: {str(e)}", + } def should_use_hierarchical_search(query: str) -> bool: @@ -584,6 +632,7 @@ def search_passages( author_filter=author_filter, work_filter=work_filter, sections_limit=sections_limit, + force_hierarchical=(force_mode == "hierarchical"), # No fallback if explicitly forced ) else: results = simple_search(query, limit, author_filter, work_filter) diff --git a/generations/library_rag/templates/search.html b/generations/library_rag/templates/search.html index 1e76f01..ca4701f 100644 --- a/generations/library_rag/templates/search.html +++ b/generations/library_rag/templates/search.html @@ -130,7 +130,7 @@ {% if query %}
Β·
- {% if results_data and results_data.results %} + {% if results_data %}
@@ -160,6 +160,17 @@ {% endif %}
+ + {% if results_data.fallback_reason %} +
+ ⚠️ Mode hiérarchique forcé : {{ results_data.fallback_reason }} +
+ πŸ’‘ Essayez une requΓͺte sur un sujet prΓ©sent dans le corpus (ex: "croyance", "signe", "infΓ©rence") ou basculez en mode Auto-dΓ©tection. +
+ {% endif %} + + {% if results_data.results %} + {% if results_data.mode == "hierarchical" and results_data.sections %} {% for section in results_data.sections %}