rrf.asciidoc 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. [[rrf]]
  2. === Reciprocal rank fusion
  3. preview::[]
  4. https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf[Reciprocal rank fusion (RRF)]
  5. is a method for combining multiple result sets with different relevance
  6. indicators into a single result set. RRF requires no tuning, and the different
  7. relevance indicators do not have to be related to each other to achieve high-quality
  8. results.
  9. RRF uses the following formula to determine the score for ranking each document:
  10. [source,python]
  11. ----
  12. score = 0.0
  13. for q in queries:
  14. if d in result(q):
  15. score += 1.0 / ( k + rank( result(q), d ) )
  16. return score
  17. # where
  18. # k is a ranking constant
  19. # q is a query in the set of queries
  20. # d is a document in the result set of q
  21. # result(q) is the result set of q
  22. # rank( result(q), d ) is d's rank within the result(q) starting from 1
  23. ----
  24. // NOTCONSOLE
  25. [[rrf-api]]
  26. ==== Reciprocal rank fusion API
  27. You can use RRF as part of a <<search-search, search>> to combine and rank
  28. documents using multiple result sets from
  29. * 1 query and 1 or more kNN searches
  30. * 2 or more kNN searches
  31. The `rrf` parameter is an optional object defined as part of a search request's
  32. <<request-body-rank, rank parameter>>. The `rrf` object contains the following
  33. parameters:
  34. `rank_constant`::
  35. (Optional, integer) This value determines how much influence documents in individual
  36. result sets per query have over the final ranked result set. A higher value indicates
  37. that lower ranked documents have more influence. This value must be greater than or
  38. equal to `1`. Defaults to `60`.
  39. `window_size`::
  40. (Optional, integer) This value determines the size of the individual result sets per
  41. query. A higher value will improve result relevance at the cost of performance. The final
  42. ranked result set is pruned down to the search request's <<search-size-param, size>.
  43. `window_size` must be greater than or equal to `size` and greater than or equal to `1`.
  44. Defaults to `100`.
  45. An example request using RRF:
  46. [source,console]
  47. ----
  48. GET example-index/_search
  49. {
  50. "query": {
  51. "term": {
  52. "text": "shoes"
  53. }
  54. },
  55. "knn": {
  56. "field": "vector",
  57. "query_vector": [1.25, 2, 3.5],
  58. "k": 50,
  59. "num_candidates": 100
  60. },
  61. "rank": {
  62. "rrf": {
  63. "window_size": 50,
  64. "rank_constant": 20
  65. }
  66. }
  67. }
  68. ----
  69. // TEST[skip:example fragment]
  70. In the above example, we first execute the kNN search to get its global top 50 results.
  71. Then we execute the query to get its global top 50 results. Afterwards, on a coordinating
  72. node, we combine the knn search results with the query results and rank them based on the
  73. RRF method to get the final top 10 results.
  74. Note that if `k` from a knn search is larger than `window_size`, the results are
  75. truncated to `window_size`. If `k` is smaller than `window_size`, the results are
  76. `k` size.
  77. [[rrf-supported-features]]
  78. ==== Reciprocal rank fusion supported features
  79. RRF does support:
  80. * <<search-from-param, from>>
  81. * <<search-aggregations, aggregations>>
  82. RRF does not currently support:
  83. * <<search-api-scroll-query-param, scroll>>
  84. * <<search-api-pit, point in time>>
  85. * <<search-sort-param, sort>>
  86. * <<rescore, rescore>>
  87. * <<search-suggesters, suggesters>>
  88. * <<highlighting, highlighting>>
  89. * <<collapse-search-results, collapse>>
  90. * <<request-body-search-explain, explain>>
  91. * <<profiling-queries, profiling>>
  92. Using unsupported features as part of a search using RRF will result
  93. in an exception.
  94. [[rrf-full-example]]
  95. ==== Reciprocal rank fusion full example
  96. We begin by creating a mapping for an index with a text field, a vector field,
  97. and an integer field along with indexing several documents. For this example we
  98. are going to use a vector with only a single dimension to make the ranking easier
  99. to explain.
  100. [source,console]
  101. ----
  102. PUT example-index
  103. {
  104. "mappings": {
  105. "properties": {
  106. "text" : {
  107. "type" : "text"
  108. },
  109. "vector": {
  110. "type": "dense_vector",
  111. "dims": 1,
  112. "index": true,
  113. "similarity": "l2_norm"
  114. },
  115. "integer" : {
  116. "type" : "integer"
  117. }
  118. }
  119. }
  120. }
  121. PUT example-index/_doc/1
  122. {
  123. "text" : "rrf",
  124. "vector" : [5],
  125. "integer": 1
  126. }
  127. PUT example-index/_doc/2
  128. {
  129. "text" : "rrf rrf",
  130. "vector" : [4],
  131. "integer": 2
  132. }
  133. PUT example-index/_doc/3
  134. {
  135. "text" : "rrf rrf rrf",
  136. "vector" : [3],
  137. "integer": 1
  138. }
  139. PUT example-index/_doc/4
  140. {
  141. "text" : "rrf rrf rrf rrf",
  142. "integer": 2
  143. }
  144. PUT example-index/_doc/5
  145. {
  146. "vector" : [0],
  147. "integer": 1
  148. }
  149. POST example-index/_refresh
  150. ----
  151. // TEST
  152. We now execute a search using RRF with a query, a kNN search, and
  153. a terms aggregation.
  154. [source,console]
  155. ----
  156. GET example-index/_search
  157. {
  158. "query": {
  159. "term": {
  160. "text": "rrf"
  161. }
  162. },
  163. "knn": {
  164. "field": "vector",
  165. "query_vector": [3],
  166. "k": 5,
  167. "num_candidates": 5
  168. },
  169. "rank": {
  170. "rrf": {
  171. "window_size": 5,
  172. "rank_constant": 1
  173. }
  174. },
  175. "size": 3,
  176. "aggs": {
  177. "int_count": {
  178. "terms": {
  179. "field": "integer"
  180. }
  181. }
  182. }
  183. }
  184. ----
  185. // TEST[continued]
  186. And we receive the response with ranked `hits` and the terms
  187. aggregation result. Note that `_score` is `null`, and we instead
  188. use `_rank` to show our top-ranked documents.
  189. [source,console-response]
  190. ----
  191. {
  192. "took": ...,
  193. "timed_out" : false,
  194. "_shards" : {
  195. "total" : 1,
  196. "successful" : 1,
  197. "skipped" : 0,
  198. "failed" : 0
  199. },
  200. "hits" : {
  201. "total" : {
  202. "value" : 5,
  203. "relation" : "eq"
  204. },
  205. "max_score" : null,
  206. "hits" : [
  207. {
  208. "_index" : "example-index",
  209. "_id" : "3",
  210. "_score" : null,
  211. "_rank" : 1,
  212. "_source" : {
  213. "integer" : 1,
  214. "vector" : [
  215. 3
  216. ],
  217. "text" : "rrf rrf rrf"
  218. }
  219. },
  220. {
  221. "_index" : "example-index",
  222. "_id" : "2",
  223. "_score" : null,
  224. "_rank" : 2,
  225. "_source" : {
  226. "integer" : 2,
  227. "vector" : [
  228. 4
  229. ],
  230. "text" : "rrf rrf"
  231. }
  232. },
  233. {
  234. "_index" : "example-index",
  235. "_id" : "4",
  236. "_score" : null,
  237. "_rank" : 3,
  238. "_source" : {
  239. "integer" : 2,
  240. "text" : "rrf rrf rrf rrf"
  241. }
  242. }
  243. ]
  244. },
  245. "aggregations" : {
  246. "int_count" : {
  247. "doc_count_error_upper_bound" : 0,
  248. "sum_other_doc_count" : 0,
  249. "buckets" : [
  250. {
  251. "key" : 1,
  252. "doc_count" : 3
  253. },
  254. {
  255. "key" : 2,
  256. "doc_count" : 2
  257. }
  258. ]
  259. }
  260. }
  261. }
  262. ----
  263. // TESTRESPONSE[s/: \.\.\./: $body.$_path/]
  264. Let's break down how these hits were ranked. We
  265. start by running the query and the kNN search
  266. separately to collect what their individual hits are.
  267. First, we look at the hits for the query.
  268. [source,console-result]
  269. ----
  270. "hits" : [
  271. {
  272. "_index" : "example-index",
  273. "_id" : "4",
  274. "_score" : 0.16152832, <1>
  275. "_source" : {
  276. "integer" : 2,
  277. "text" : "rrf rrf rrf rrf"
  278. }
  279. },
  280. {
  281. "_index" : "example-index",
  282. "_id" : "3", <2>
  283. "_score" : 0.15876243,
  284. "_source" : {
  285. "integer" : 1,
  286. "vector" : [3],
  287. "text" : "rrf rrf rrf"
  288. }
  289. },
  290. {
  291. "_index" : "example-index",
  292. "_id" : "2", <3>
  293. "_score" : 0.15350538,
  294. "_source" : {
  295. "integer" : 2,
  296. "vector" : [4],
  297. "text" : "rrf rrf"
  298. }
  299. },
  300. {
  301. "_index" : "example-index",
  302. "_id" : "1", <4>
  303. "_score" : 0.13963442,
  304. "_source" : {
  305. "integer" : 1,
  306. "vector" : [5],
  307. "text" : "rrf"
  308. }
  309. }
  310. ]
  311. ----
  312. // TEST[skip:example fragment]
  313. <1> rank 1, `_id` 4
  314. <2> rank 2, `_id` 3
  315. <3> rank 3, `_id` 2
  316. <4> rank 4, `_id` 1
  317. Note that our first hit doesn't have a value for the `vector` field. Now,
  318. we look at the results for the kNN search.
  319. [source,console-result]
  320. ----
  321. "hits" : [
  322. {
  323. "_index" : "example-index",
  324. "_id" : "3", <1>
  325. "_score" : 1.0,
  326. "_source" : {
  327. "integer" : 1,
  328. "vector" : [3],
  329. "text" : "rrf rrf rrf"
  330. }
  331. },
  332. {
  333. "_index" : "example-index",
  334. "_id" : "2", <2>
  335. "_score" : 0.5,
  336. "_source" : {
  337. "integer" : 2,
  338. "vector" : [4],
  339. "text" : "rrf rrf"
  340. }
  341. },
  342. {
  343. "_index" : "example-index",
  344. "_id" : "1", <3>
  345. "_score" : 0.2,
  346. "_source" : {
  347. "integer" : 1,
  348. "vector" : [5],
  349. "text" : "rrf"
  350. }
  351. },
  352. {
  353. "_index" : "example-index",
  354. "_id" : "5", <4>
  355. "_score" : 0.1,
  356. "_source" : {
  357. "integer" : 1,
  358. "vector" : [0]
  359. }
  360. }
  361. ]
  362. ----
  363. // TEST[skip:example fragment]
  364. <1> rank 1, `_id` 3
  365. <2> rank 2, `_id` 2
  366. <3> rank 3, `_id` 1
  367. <4> rank 4, `_id` 5
  368. We can now take the two individually ranked result sets and apply the
  369. RRF formula to them to get our final ranking.
  370. [source,python]
  371. ----
  372. # doc | query | knn | score
  373. _id: 1 = 1.0/(1+4) + 1.0/(1+3) = 0.4500
  374. _id: 2 = 1.0/(1+3) + 1.0/(1+2) = 0.5833
  375. _id: 3 = 1.0/(1+2) + 1.0/(1+1) = 0.8333
  376. _id: 4 = 1.0/(1+1) = 0.5000
  377. _id: 5 = 1.0/(1+4) = 0.2000
  378. ----
  379. // NOTCONSOLE
  380. We rank the documents based on the RRF formula with a `window_size` of `5`
  381. truncating the bottom `2` docs in our RRF result set with a `size` of `3`.
  382. We end with `_id: 3` as `_rank: 1`, `_id: 2` as `_rank: 2`, and
  383. `_id: 4` as `_rank: 3`. This ranking matches the result set from the
  384. original RRF search as expected.