Repository: nomic-ai/gpt4all
File: gpt4all-chat/src/database.cpp (C++)

Code before:
    for (const QString &w : oWords)
        if (!stopWords.contains(w.toLower()))
            rWords << w;

    QStringList quotedWords;
    for (const QString &w : rWords)
        quotedWords << "\"" + w + "\"";

    BM25Query b;
    b.type = 1; // broad search
    b.input = oWords.join(" ");
    b.query = "(" + quotedWords.join(" OR ") + ")";
    b.qlength = 1; // length of phrase
    b.ilength = oWords.size();
    b.rlength = oWords.size() - rWords.size();
    queries << b;
    return queries;
}

QList<int> Database::searchBM25(const QString &query, const QList<QString> &collections, BM25Query &bm25q, int k)
{
    struct SearchResult { int chunkId; float dist; };
    QList<BM25Query> bm25Queries = queriesForFTS5(query);

    QList<SearchResult> results;
    for (auto bm25Query : bm25Queries) {
        QSqlQuery sqlQuery(m_db);
        sqlQuery.prepare(SELECT_CHUNKS_FTS_SQL.arg(k));
        sqlQuery.addBindValue(bm25Query.query);

        if (!sqlQuery.exec()) {
            qWarning() << "Database ERROR: Failed to execute BM25 query:" << sqlQuery.lastError();
            return {};
        }

        if (!sqlQuery.next())
            continue;

        bm25q = bm25Query;

        do {
            const int chunkId = sqlQuery.value(0).toInt();
            const float score = sqlQuery.value(1).toFloat();
            results.append({chunkId, score});
        } while (sqlQuery.next());
        break;
    }

    k = qMin(k, results.size());
    std::partial_sort(
        results.begin(), results.begin() + k, results.end(),

Review comment (suggestion):
Please avoid the detach() and copies:
```suggestion
    for (auto &bm25Query : std::as_const(bm25Queries)) {
```

Code after:
    QHash<int, int> bm25Ranks;
    for (int i = 0; i < bm25Results.size(); ++i) {
        if (!results.contains(bm25Results[i]))
            missingScores.append(bm25Results[i]);
        bm25Ranks[bm25Results[i]] = i + 1;
    }

    if (!missingScores.isEmpty()) {
        QList<int> scored = scoreChunks(query, missingScores);
        results << scored;
    }

    QHash<int, int> embeddingRanks;
    for (int i = 0; i < results.size(); ++i)
        embeddingRanks[results[i]] = i + 1;

    const float bmWeight = bm25Results.isEmpty() ? 0 : computeBM25Weight(bm25q);

    // From the paper: "Reciprocal Rank Fusion outperforms Condorcet and individual Rank Learning Methods"
    // doi: 10.1145/1571941.157211
    const int fusion_k = 60;

    std::stable_sort(
        results.begin(), results.end(),
        [&](const int &a, const int &b) {
            // Reciprocal Rank Fusion (RRF)
            const int aBm25Rank = bm25Ranks.value(a, bm25Results.size() + 1);
            const int aEmbeddingRank = embeddingRanks.value(a, embeddingResults.size() + 1);
            Q_ASSERT(embeddingRanks.contains(a));

            const int bBm25Rank = bm25Ranks.value(b, bm25Results.size() + 1);
            const int bEmbeddingRank = embeddingRanks.value(b, embeddingResults.size() + 1);
            Q_ASSERT(embeddingRanks.contains(b));

            const float aBm25Score = 1.0f / (fusion_k + aBm25Rank);
            const float bBm25Score = 1.0f / (fusion_k + bBm25Rank);
            const float aEmbeddingScore = 1.0f / (fusion_k + aEmbeddingRank);
            const float bEmbeddingScore = 1.0f / (fusion_k + bEmbeddingRank);
            const float aWeightedScore = bmWeight * aBm25Score + (1.f - bmWeight) * aEmbeddingScore;
            const float bWeightedScore = bmWeight * bBm25Score + (1.f - bmWeight) * bEmbeddingScore;

            // Higher RRF score means better ranking, so we use greater than for sorting
            return aWeightedScore > bWeightedScore;
        }
    );

    k = qMin(k, results.size());
    results.resize(k);
    return results;
}