searchHybrid method

Future<List<HybridSearchResult>> searchHybrid(
  1. String query, {
  2. int topK = 10,
  3. double vectorWeight = 0.2,
  4. double bm25Weight = 0.8,
  5. List<int>? sourceIds,
})

Hybrid search combining vector and keyword (BM25) search.

This method uses Reciprocal Rank Fusion (RRF) to combine:

  • Vector search: semantic similarity using embeddings
  • BM25 search: keyword matching for exact terms

Use this for better results when searching for:

  • Proper nouns (names, product models)
  • Technical terms or code snippets
  • Exact keyword matches

Parameters:

  • query: The search query text
  • topK: Number of results to return (default: 10)
  • vectorWeight: Weight for vector search (0.0-1.0, default: 0.2)
  • bm25Weight: Weight for BM25 search (0.0-1.0, default: 0.8)

Implementation

Future<List<hybrid.HybridSearchResult>> searchHybrid(
  String query, {
  int topK = 10,
  double vectorWeight = 0.2,
  double bm25Weight = 0.8,
  List<int>? sourceIds,
}) async {
  // 1. Generate query embedding
  final queryEmbedding = await EmbeddingService.embed(query);

  // 2. Ensure hybrid indexes are switched to this collection.
  await rust_rag.activateCollectionForHybridSearch(
    collectionId: collectionId,
    basePath: _indexPath,
  );

  // 3. Perform hybrid search with RRF fusion
  late List<hybrid.HybridSearchResult> results;
  try {
    // Improve post-filtering recall:
    // If filtering by source, fetch more candidates internally to avoid
    // the dominant source occupying all top-k slots before filtering.
    final effectiveTopK = sourceIds != null
        ? (topK * 10).clamp(50, 200)
        : topK;

    results = await hybrid.searchHybrid(
      queryText: query,
      queryEmbedding: queryEmbedding,
      topK: effectiveTopK,
      config: hybrid.RrfConfig(
        k: 60,
        vectorWeight: vectorWeight,
        bm25Weight: bm25Weight,
      ),
      filter: hybrid.SearchFilter(
        sourceIds: sourceIds != null ? _toInt64List(sourceIds) : null,
        collectionId: collectionId,
      ),
    );
  } on RagError catch (e) {
    e.when(
      databaseError: (msg) =>
          debugPrint('[SmartError] Hybrid search DB error: $msg'),
      ioError: (_) {},
      modelLoadError: (_) {},
      invalidInput: (_) {},
      internalError: (msg) =>
          log('[SmartError] Hybrid search engine error: $msg'),
      unknown: (msg) => log('[SmartError] Hybrid search unknown error: $msg'),
    );
    rethrow;
  }

  return results.take(topK).toList();
}