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

4.8 KiB
Raw Blame History

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:

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

  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

# 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