4.8 KiB
4.8 KiB
M8.6 — Upgrade GET /memory/query to hybrid with fallback
| Field | Value |
|---|---|
| Phase | M8 — Hybrid Search |
| Size | M — 1–2 days |
| Status | ✅ COMPLETE |
| Flags | — |
| Spec | inlined below |
| Blocks | M8.8, M8.9 |
| Depends | M8.3, M8.4 (query optimizer, RRF fusion complete) |
Goal
Replace the QueryWorker call in GET /memory/query with HybridQueryWorker. Add ?method= parameter for explicit strategy override. Implement fallback chain: hybrid → semantic → error.
Design
Updated request:
GET /memory/query?query=...&project=...&limit=10&method=hybrid
Authorization: Bearer <JWT>
New parameter:
method(optional) —hybrid(default),semantic,lexical. If omitted,QueryOptimizerdecides.
Handler logic:
async fn query_handler(...) -> HttpResponse {
let (claims, token) = validate_auth(...)?;
check_capability(&claims, "memory:read")?;
check_rate_limit(&claims, &state, "/memory/query")?;
let method_override = query.get("method").map(|m| match m.as_str() {
"semantic" => SearchStrategy::SemanticOnly,
"lexical" => SearchStrategy::LexicalOnly,
_ => SearchStrategy::Hybrid, // includes "hybrid" and unknown values
});
// Try hybrid worker first
if let Some(ref hybrid) = state.hybrid_query_worker {
match hybrid.query(&project, &question, limit, &token, method_override).await {
Ok(response) => return HttpResponse::Ok().json(response),
Err(e) => {
tracing::warn!("hybrid query failed, falling back: {}", e);
}
}
}
// Fallback: existing semantic-only worker
match state.query_worker.query(&project, &question, Some(limit)).await {
Ok(results) => HttpResponse::Ok().json(json!({
"query": question,
"project": project,
"search_strategy": "semantic_fallback",
"results": results,
})),
Err(e) => HttpResponse::InternalServerError().json(json!({"error": "query_failed"})),
}
}
AppState change:
pub struct AppState {
// ... existing fields ...
pub hybrid_query_worker: Option<Arc<HybridQueryWorker>>, // None if OpenSearch not configured
}
Constructed in start_server():
let hybrid = if std::env::var("OPENSEARCH_HOSTS").is_ok() {
let os_client = OpenSearchClient::new(hosts);
Some(Arc::new(HybridQueryWorker::new(vector_store, embeddings, Some(Arc::new(os_client)))))
} else {
// No OpenSearch configured — hybrid worker without lexical
Some(Arc::new(HybridQueryWorker::new(vector_store, embeddings, None)))
};
Backward compatibility: If OPENSEARCH_HOSTS is not set, the worker still works but always uses SemanticOnly. Existing clients see the same results with an added search_strategy field.
Steps
- Add
hybrid_query_workerfield toAppState. - Construct
HybridQueryWorkerinstart_server(), gated onOPENSEARCH_HOSTSenv. - Update
query_handler()with method override + fallback chain. - Add
?method=query parameter parsing. - Update response format to include
search_strategyandmetrics. - Write integration test: query with
method=hybrid, verify response hasmetrics. - Write integration test: query without OpenSearch, verify fallback to semantic.
Acceptance
GET /memory/query?query=test&project=preturns"search_strategy": "Hybrid"when OpenSearch configured.GET /memory/query?query=test&project=p&method=semanticforces semantic-only, response says"search_strategy": "SemanticOnly".- If OpenSearch is down, query still succeeds with
"search_strategy": "semantic_fallback". - Response includes
metricsblock with timing data. - Existing clients that don't send
methodparam get the same results as before (backward compat). - JWT token is forwarded to OpenSearch (not a new token, not admin credentials).
Verify
# With OpenSearch running
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/memory/query?query=kubernetes+port&project=poimen" | jq .search_strategy
# Should output: "Hybrid"
# Force semantic
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/memory/query?query=kubernetes+port&project=poimen&method=semantic" | jq .search_strategy
# Should output: "SemanticOnly"
# Kill OpenSearch, retry
curl -H "Authorization: Bearer $TOKEN" \
"http://localhost:8080/memory/query?query=kubernetes+port&project=poimen" | jq .search_strategy
# Should output: "semantic_fallback"
False pass: Handler catches the hybrid error silently and always falls back to semantic — user never sees hybrid results even when OpenSearch is healthy. Assert that with OpenSearch up, metrics.lexical_candidates is Some(n) where n > 0.
Artifacts
- Modified
crates/mem-cli/src/http_server.rs