* @brief Returns up to k highest-BM25-scoring chunks. */
| 144 | * @brief Returns up to k highest-BM25-scoring chunks. |
| 145 | */ |
| 146 | QList<AI::DocSearch::Hit> AI::DocSearch::search(const QString& query, int k) |
| 147 | { |
| 148 | if (!m_loaded || m_docs.isEmpty()) |
| 149 | return {}; |
| 150 | |
| 151 | const auto qTokens = tokenize(query); |
| 152 | if (qTokens.isEmpty()) |
| 153 | return {}; |
| 154 | |
| 155 | QList<QPair<QString, double>> qIdf; |
| 156 | qIdf.reserve(qTokens.size()); |
| 157 | for (const auto& t : qTokens) { |
| 158 | const double w = m_idf.value(t, 0.0); |
| 159 | if (w > 0.0) |
| 160 | qIdf.append({t, w}); |
| 161 | } |
| 162 | if (qIdf.isEmpty()) |
| 163 | return {}; |
| 164 | |
| 165 | QList<QPair<int, double>> scored; |
| 166 | scored.reserve(m_docs.size()); |
| 167 | for (int i = 0; i < m_docs.size(); ++i) { |
| 168 | const auto& d = m_docs[i]; |
| 169 | const double dl = d.length > 0 ? double(d.length) : 1.0; |
| 170 | const double K = m_k1 * (1.0 - m_b + m_b * dl / std::max(1.0, m_avgdl)); |
| 171 | |
| 172 | double s = 0.0; |
| 173 | for (const auto& [term, w] : qIdf) { |
| 174 | const int tf = d.tf.value(term, 0); |
| 175 | if (tf == 0) |
| 176 | continue; |
| 177 | |
| 178 | s += w * (tf * (m_k1 + 1.0)) / (tf + K); |
| 179 | } |
| 180 | if (s > 0.0) |
| 181 | scored.append({i, s}); |
| 182 | } |
| 183 | |
| 184 | std::sort( |
| 185 | scored.begin(), scored.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); |
| 186 | |
| 187 | const int kMax = std::min<int>(k > 0 ? k : 5, scored.size()); |
| 188 | QList<Hit> out; |
| 189 | out.reserve(kMax); |
| 190 | for (int i = 0; i < kMax; ++i) { |
| 191 | const auto& d = m_docs[scored[i].first]; |
| 192 | Hit h; |
| 193 | h.id = d.id; |
| 194 | h.source = d.source; |
| 195 | h.title = d.title; |
| 196 | h.body = d.body; |
| 197 | h.score = scored[i].second; |
| 198 | out.append(std::move(h)); |
| 199 | } |
| 200 | return out; |
| 201 | } |
| 202 | |
| 203 | /** |