-
Notifications
You must be signed in to change notification settings - Fork 7
fix: fall back to FTS when search embeddings stall #523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,7 @@ | |
| _ORIGIN_ORDER_LABEL = "- Order: origin (earliest among expanded hybrid candidates)" | ||
| _HELPER_SOCKET_GLOB = "/tmp/brainbar-hybrid-*.sock" | ||
| _HELPER_SOCKET_TIMEOUT_SECONDS = 2.0 | ||
| _DEFAULT_EMBED_TIMEOUT_MS = 1000.0 | ||
|
|
||
| from ._format import format_kg_search, format_recalled_context, format_search_results, format_stats | ||
| from ._shared import ( | ||
|
|
@@ -60,6 +61,17 @@ def _get_vector_store(): | |
| return _get_search_vector_store() | ||
|
|
||
|
|
||
| def _embed_timeout_ms() -> float: | ||
| raw = os.environ.get("BRAINLAYER_EMBED_TIMEOUT_MS", str(_DEFAULT_EMBED_TIMEOUT_MS)) | ||
| try: | ||
| value = float(raw) | ||
| except (TypeError, ValueError): | ||
| return _DEFAULT_EMBED_TIMEOUT_MS | ||
| if not value or value < 0: | ||
| return _DEFAULT_EMBED_TIMEOUT_MS | ||
| return min(value, 30_000.0) | ||
|
|
||
|
|
||
| def _origin_candidate_count(num_results: int) -> int: | ||
| return min(_MAX_PUBLIC_NUM_RESULTS, max(num_results, _ORIGIN_CANDIDATE_LIMIT)) | ||
|
|
||
|
|
@@ -1767,20 +1779,44 @@ async def _search( | |
|
|
||
| normalized_project = _normalize_project_name(project) | ||
| loop = asyncio.get_running_loop() | ||
| model = _get_embedding_model() | ||
| embed_started = search_profile.now() | ||
| query_embedding = None | ||
| search_mode = "hybrid" | ||
| fallback_reason = None | ||
| try: | ||
| query_embedding = await loop.run_in_executor(None, model.embed_query, query) | ||
| embed_timeout_ms = _embed_timeout_ms() | ||
| query_embedding = await asyncio.wait_for( | ||
| loop.run_in_executor( | ||
| None, | ||
| lambda: _get_embedding_model().embed_query(query), | ||
| ), | ||
| timeout=embed_timeout_ms / 1000.0, | ||
|
Comment on lines
+1789
to
+1793
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the embedding backend/model load is the thing that stalls, this Useful? React with 👍 / 👎.
Comment on lines
+1788
to
+1793
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the embedding backend hangs in the long-lived MCP server, Useful? React with 👍 / 👎. |
||
| ) | ||
| except TimeoutError as exc: | ||
| search_mode = "fts_only" | ||
| fallback_reason = "embed_timeout" | ||
| search_profile.emit( | ||
| profile_scope, | ||
| "embed", | ||
| profile_query_id, | ||
| search_profile.dur_ms(embed_started), | ||
| error=exc.__class__.__name__, | ||
| timeout_ms=embed_timeout_ms, | ||
| fallback="fts_only", | ||
| ) | ||
| except Exception as exc: | ||
| search_mode = "fts_only" | ||
| fallback_reason = f"embed_error:{exc.__class__.__name__}" | ||
| search_profile.emit( | ||
| profile_scope, | ||
| "embed", | ||
| profile_query_id, | ||
| search_profile.dur_ms(embed_started), | ||
| error=exc.__class__.__name__, | ||
| fallback="fts_only", | ||
| ) | ||
| raise | ||
| search_profile.emit(profile_scope, "embed", profile_query_id, search_profile.dur_ms(embed_started)) | ||
| else: | ||
| search_profile.emit(profile_scope, "embed", profile_query_id, search_profile.dur_ms(embed_started)) | ||
|
|
||
| if source == "all": | ||
| source_filter = None | ||
|
|
@@ -1890,7 +1926,14 @@ async def _search( | |
| } | ||
| ) | ||
| structured_results.append(item) | ||
| structured = {"query": query, "total": len(structured_results), "results": structured_results} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Empty results omit fallback metadataLow Severity After an embedding timeout or error, successful code paths attach Reviewed by Cursor Bugbot for commit 2ae7109. Configure here. |
||
| structured = { | ||
| "query": query, | ||
| "total": len(structured_results), | ||
| "results": structured_results, | ||
| "search_mode": search_mode, | ||
| } | ||
| if fallback_reason: | ||
| structured["fallback_reason"] = fallback_reason | ||
| if order == "origin": | ||
| structured["order"] = order | ||
| structured["order_scope"] = _ORIGIN_ORDER_SCOPE | ||
|
|
@@ -1900,9 +1943,16 @@ async def _search( | |
| len(structured_results), | ||
| order=order if order == "origin" else None, | ||
| ) | ||
| if search_mode == "fts_only": | ||
| formatted_text = ( | ||
| f"{formatted_text}\n\n" | ||
| f"Search mode: FTS-only fallback ({fallback_reason}); vector embedding was skipped." | ||
| ) | ||
| return ([TextContent(type="text", text=formatted_text)], structured) | ||
|
|
||
| output_parts = [f"## Search Results for: {query}\n"] | ||
| if search_mode == "fts_only": | ||
| output_parts.append(f"Search mode: FTS-only fallback ({fallback_reason}); vector embedding was skipped.") | ||
| if order == "origin": | ||
| output_parts.append(_ORIGIN_ORDER_LABEL) | ||
| structured_results = [] | ||
|
|
@@ -1973,7 +2023,14 @@ async def _search( | |
| output_parts.append(doc) | ||
| output_parts.append("\n---") | ||
|
|
||
| structured = {"query": query, "total": len(structured_results), "results": structured_results} | ||
| structured = { | ||
| "query": query, | ||
| "total": len(structured_results), | ||
| "results": structured_results, | ||
| "search_mode": search_mode, | ||
| } | ||
| if fallback_reason: | ||
| structured["fallback_reason"] = fallback_reason | ||
| if order == "origin": | ||
| structured["order"] = order | ||
| structured["order_scope"] = _ORIGIN_ORDER_SCOPE | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Timeout leaves background work running
Medium Severity
When the embed/hybrid deadline is exceeded, the caller falls back to FTS, but the timed-out work keeps running in a daemon thread or default executor. Stalled embedding and hybrid work can continue to hold the model and database, competing with the FTS path and later searches in the same process.
Additional Locations (1)
src/brainlayer/mcp/search_handler.py#L1787-L1794Reviewed by Cursor Bugbot for commit 2ae7109. Configure here.