Skip to content

[Detail Bug] Hybrid search returns no results when any Qdrant collection query fails (partial failure handling regressed) #170

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_befd6425-a158-4e24-9d4d-1e5c08769515/bugs/bug_c1e3e808-e251-46eb-8bad-9b4714c18c54

Introduced in #141 by @WilliamAGH on Jul 27, 2026

Summary

  • Context: Hybrid search retrieves documents from multiple Qdrant collections and originally supported configurable partial failure handling via failOnPartialSearchError property
  • Bug: Refactoring in commit 4130ba2 removed conditional failure logic and made the throw unconditional, leaving toNotice() method and SearchOutcome.notices field as dead code
  • Actual vs. expected: Code should support partial failures (original design); instead it always throws, breaking original intent
  • Impact: Users lose all search results when one collection fails; retry mechanism retries entire operation rather than proceeding with partial results

Code with Bug

HybridSearchService.java:

if (!collectionFailures.isEmpty()) {
    throw new HybridSearchPartialFailureException(
            "Qdrant retrieval failed for " + collectionFailures.size() + " collection(s)",
            collectionFailures,
            dependencyFailures);
    // <-- BUG 🔴 unconditional throw prevents returning partial results + notices
}

List<Document> rankedDocuments = scoredPointsByUuid.values().stream()
        .sorted(Comparator.comparingDouble(ScoredPointMatch::score).reversed())
        .limit(topK)
        .map(scoredPointMatch -> QdrantScoredPointDocumentMapper.toDocument(
                scoredPointMatch.point(),
                scoredPointMatch.id(),
                scoredPointMatch.score(),
                scoredPointMatch.collectionName()))
        .toList();
List<HybridSearchNotice> retrievalNotices =
        collectionFailures.stream().map(HybridSearchService::toNotice).toList();  // <-- BUG 🔴 unreachable
return new SearchOutcome(rankedDocuments, retrievalNotices);

Explanation

  • The service collects per-collection failures (collectionFailures) during fan-out.
  • Current code throws HybridSearchPartialFailureException whenever any collection fails, even if other collections succeeded and there are ranked documents to return.
  • This makes the downstream “notices” pipeline unreachable: HybridSearchService::toNotice exists and is referenced, but the code path can never reach it due to the unconditional throw.
  • Git history shows this is a regression: the earlier implementation conditionally threw based on a failure policy / failOnPartialSearchError, allowing partial results + notices when configured.

Codebase Inconsistency

The codebase contains a full propagation path for retrieval notices, implying HybridSearchService is expected to return partial results with notices in some cases:

RetrievalService.java:

retrievedDocuments.addAll(searchOutcome.documents());
searchOutcome.notices().stream()
        .map(searchNotice -> new RetrievalNotice(searchNotice.summary(), searchNotice.details()))
        .forEach(retrievalNotices::add);

This pipeline is effectively orphaned because HybridSearchService never produces notices.

Recommended Fix

Restore conditional failure behavior so partial successes can return documents plus notices (at minimum: only throw when no documents were retrieved):

if (!collectionFailures.isEmpty()) {
    boolean allCollectionsFailed = scoredPointsByUuid.isEmpty();
    if (allCollectionsFailed) {
        throw new HybridSearchPartialFailureException(
                "Qdrant retrieval failed for " + collectionFailures.size() + " collection(s)",
                collectionFailures,
                dependencyFailures);
    }
    // Otherwise continue with partial results and include notices
}

List<HybridSearchNotice> retrievalNotices =
        collectionFailures.stream().map(HybridSearchService::toNotice).toList();
return new SearchOutcome(rankedDocuments, retrievalNotices);

History

This bug was introduced in commit 4130ba2. The original implementation (commit dc1a952) correctly used a conditional failure policy: if (!collectionFailures.isEmpty() && failOnPartialSearchError), which allowed the search to continue with degraded results and notices when the policy was disabled. Commit 4130ba2 removed the failOnPartialSearchError condition, making the throw unconditional and rendering the entire notices pipeline unreachable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions