Files
poimen-memory/tasks/M8.6-query-endpoint-upgrade.md
T
rock df29334ef9
Build and Push / Test (push) Failing after 1m52s
Build and Push / Build and push image (push) Skipped
feat: Mark M8.3, M8.4, M8.6 as COMPLETE
2026-08-28 13:30:30 -07:00

128 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M8.6 — Upgrade GET /memory/query to hybrid with fallback
| Field | Value |
|---|---|
| Phase | M8 — Hybrid Search |
| Size | M — 12 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, `QueryOptimizer` decides.
**Handler logic:**
```rust
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:**
```rust
pub struct AppState {
// ... existing fields ...
pub hybrid_query_worker: Option<Arc<HybridQueryWorker>>, // None if OpenSearch not configured
}
```
Constructed in `start_server()`:
```rust
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
1. Add `hybrid_query_worker` field to `AppState`.
2. Construct `HybridQueryWorker` in `start_server()`, gated on `OPENSEARCH_HOSTS` env.
3. Update `query_handler()` with method override + fallback chain.
4. Add `?method=` query parameter parsing.
5. Update response format to include `search_strategy` and `metrics`.
6. Write integration test: query with `method=hybrid`, verify response has `metrics`.
7. Write integration test: query without OpenSearch, verify fallback to semantic.
## Acceptance
1. `GET /memory/query?query=test&project=p` returns `"search_strategy": "Hybrid"` when OpenSearch configured.
2. `GET /memory/query?query=test&project=p&method=semantic` forces semantic-only, response says `"search_strategy": "SemanticOnly"`.
3. If OpenSearch is down, query still succeeds with `"search_strategy": "semantic_fallback"`.
4. Response includes `metrics` block with timing data.
5. Existing clients that don't send `method` param get the same results as before (backward compat).
6. JWT token is forwarded to OpenSearch (not a new token, not admin credentials).
## Verify
```bash
# 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`