feat: Add force_hierarchical mode to prevent fallback
## 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 <noreply@anthropic.com>
This commit is contained in:
@@ -291,6 +291,7 @@ def hierarchical_search(
|
|||||||
author_filter: Optional[str] = None,
|
author_filter: Optional[str] = None,
|
||||||
work_filter: Optional[str] = None,
|
work_filter: Optional[str] = None,
|
||||||
sections_limit: int = 5,
|
sections_limit: int = 5,
|
||||||
|
force_hierarchical: bool = False,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Two-stage hierarchical semantic search: Summary → Chunks.
|
"""Two-stage hierarchical semantic search: Summary → Chunks.
|
||||||
|
|
||||||
@@ -303,6 +304,7 @@ def hierarchical_search(
|
|||||||
author_filter: Filter by author name.
|
author_filter: Filter by author name.
|
||||||
work_filter: Filter by work title.
|
work_filter: Filter by work title.
|
||||||
sections_limit: Number of top sections to retrieve (default: 5).
|
sections_limit: Number of top sections to retrieve (default: 5).
|
||||||
|
force_hierarchical: If True, never fallback to simple search (for testing).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with hierarchical search results:
|
Dictionary with hierarchical search results:
|
||||||
@@ -310,17 +312,28 @@ def hierarchical_search(
|
|||||||
- sections: List of section dictionaries with nested chunks
|
- sections: List of section dictionaries with nested chunks
|
||||||
- results: Flat list of all chunks (for compatibility)
|
- results: Flat list of all chunks (for compatibility)
|
||||||
- total_chunks: Total number of chunks found
|
- total_chunks: Total number of chunks found
|
||||||
|
- fallback_reason: Explanation if forced but 0 results (optional)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with get_weaviate_client() as client:
|
with get_weaviate_client() as client:
|
||||||
if client is None:
|
if client is None:
|
||||||
# Fallback to simple search
|
# Fallback to simple search only if not forced
|
||||||
|
if not force_hierarchical:
|
||||||
results = simple_search(query, limit, author_filter, work_filter)
|
results = simple_search(query, limit, author_filter, work_filter)
|
||||||
return {
|
return {
|
||||||
"mode": "simple",
|
"mode": "simple",
|
||||||
"results": results,
|
"results": results,
|
||||||
"total_chunks": len(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
|
# STAGE 1: Search Summary collection for relevant sections
|
||||||
@@ -338,13 +351,24 @@ def hierarchical_search(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not summaries_result.objects:
|
if not summaries_result.objects:
|
||||||
# No summaries found, fallback to simple search
|
# No summaries found
|
||||||
|
if not force_hierarchical:
|
||||||
|
# Auto-detection: fallback to simple search
|
||||||
results = simple_search(query, limit, author_filter, work_filter)
|
results = simple_search(query, limit, author_filter, work_filter)
|
||||||
return {
|
return {
|
||||||
"mode": "simple",
|
"mode": "simple",
|
||||||
"results": results,
|
"results": results,
|
||||||
"total_chunks": len(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
|
# Extract section data
|
||||||
sections_data = []
|
sections_data = []
|
||||||
@@ -393,13 +417,27 @@ def hierarchical_search(
|
|||||||
sections_data = filtered_sections
|
sections_data = filtered_sections
|
||||||
|
|
||||||
if not sections_data:
|
if not sections_data:
|
||||||
# No sections match filters, fallback to simple search
|
# No sections match filters
|
||||||
|
if not force_hierarchical:
|
||||||
|
# Auto-detection: fallback to simple search
|
||||||
results = simple_search(query, limit, author_filter, work_filter)
|
results = simple_search(query, limit, author_filter, work_filter)
|
||||||
return {
|
return {
|
||||||
"mode": "simple",
|
"mode": "simple",
|
||||||
"results": results,
|
"results": results,
|
||||||
"total_chunks": len(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
|
# STAGE 2: Search Chunk collection filtered by sections
|
||||||
@@ -461,13 +499,23 @@ def hierarchical_search(
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erreur recherche hiérarchique: {e}")
|
print(f"Erreur recherche hiérarchique: {e}")
|
||||||
# Fallback to simple search on error
|
# Fallback to simple search on error (unless forced)
|
||||||
|
if not force_hierarchical:
|
||||||
results = simple_search(query, limit, author_filter, work_filter)
|
results = simple_search(query, limit, author_filter, work_filter)
|
||||||
return {
|
return {
|
||||||
"mode": "simple",
|
"mode": "simple",
|
||||||
"results": results,
|
"results": results,
|
||||||
"total_chunks": len(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:
|
def should_use_hierarchical_search(query: str) -> bool:
|
||||||
@@ -584,6 +632,7 @@ def search_passages(
|
|||||||
author_filter=author_filter,
|
author_filter=author_filter,
|
||||||
work_filter=work_filter,
|
work_filter=work_filter,
|
||||||
sections_limit=sections_limit,
|
sections_limit=sections_limit,
|
||||||
|
force_hierarchical=(force_mode == "hierarchical"), # No fallback if explicitly forced
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
results = simple_search(query, limit, author_filter, work_filter)
|
results = simple_search(query, limit, author_filter, work_filter)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
{% if query %}
|
{% if query %}
|
||||||
<div class="ornament">·</div>
|
<div class="ornament">·</div>
|
||||||
|
|
||||||
{% if results_data and results_data.results %}
|
{% if results_data %}
|
||||||
<!-- Mode indicator and stats -->
|
<!-- Mode indicator and stats -->
|
||||||
<div class="mb-3" style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
<div class="mb-3" style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||||
<div>
|
<div>
|
||||||
@@ -160,6 +160,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Fallback reason warning (when forced hierarchical but no results) -->
|
||||||
|
{% if results_data.fallback_reason %}
|
||||||
|
<div class="alert" style="background-color: #fff3cd; border: 1px solid #ffc107; color: #856404; padding: 1rem; border-radius: 4px; margin-bottom: 1rem;">
|
||||||
|
<strong>⚠️ Mode hiérarchique forcé :</strong> {{ results_data.fallback_reason }}
|
||||||
|
<br>
|
||||||
|
<small>💡 Essayez une requête sur un sujet présent dans le corpus (ex: "croyance", "signe", "inférence") ou basculez en mode Auto-détection.</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if results_data.results %}
|
||||||
|
|
||||||
<!-- Hierarchical display -->
|
<!-- Hierarchical display -->
|
||||||
{% if results_data.mode == "hierarchical" and results_data.sections %}
|
{% if results_data.mode == "hierarchical" and results_data.sections %}
|
||||||
{% for section in results_data.sections %}
|
{% for section in results_data.sections %}
|
||||||
|
|||||||
Reference in New Issue
Block a user