Measure retrieval recall
After changing a search, how do you know it got better? Looking at a few answers is not enough: a convincing demo can hide a worse system. You need a number. For each test question you write down which documents hold the answer. Then you check how many of them the search returned among its first k results. That share is called recall at k: 1 means every needed document was found, and 0 means none was. Over a whole set of questions, you report the average.
Learn it
Words to know
- evaluation set
- A fixed list of test questions with their correct answers or documents, used to measure a system the same way every time.
- relevant document
- A document that holds the answer to a test question.
- recall at k
- The share of the relevant documents that appear in the first k results.
How it works
- With no relevant ids there is nothing to find. Return 0 instead of dividing by zero.
- Take the first
kresults. Anything after that does not count, and a repeated result still uses up a place. - Count how many of the relevant ids are among them. Each relevant id counts once, however often it was retrieved.
- Divide by the number of distinct relevant ids.
Worked example
Input retrieved = ["d4", "d1", "d7", "d2"] relevant = ["d1", "d2", "d3"] k = 2 Output 0.333333
| Rank | Retrieved id | In the first k? | Relevant? |
|---|---|---|---|
| 1 | d4 | yes | no |
| 2 | d1 | yes | yes: found |
| 3 | d7 | no: k is 2 | no |
| 4 | d2 | no: k is 2 | yes, but too late |
| 1 | 2 | 3 | 4 | |
|---|---|---|---|---|
| retrieved | d4 | d1 | d7 | d2 |
| in the first 2 | yes | yes | no | no |
- relevant, inside the cut
- relevant, after the cut
One of the three relevant documents is in the first two results, so recall at 2 is 1 / 3. With k raised to 4, d2 would count too, and recall would be 2 / 3.
Why it matters
The first portfolio project reports this number before and after each change, and once at the end on questions it kept aside. It measures the search only: an answer can still be wrong when recall is 1.
Your turn
Compute recall at k: the share of the relevant documents that appear in the first k results.
retrieved- the ids the search returned, best first; an id can appear twice
relevant- the ids that hold the answer; an id can appear twice
k- how many results count; 0 or more
- Returns
- A number from 0 to 1: the distinct relevant ids found in the first
kresults, divided by the number of distinct relevant ids. Do not round it. With no relevant ids, 0.
What JavaScript gives you
list.slice(0, k)- the first k items
new Set(list)- a collection that holds each item once
set.size- how many items a set holds
set.has(x)- whether x is in the set
What Python gives you
list[:k]- the first k items
set(list)- a collection that holds each item once
len(s)- how many items the set
sholds x in s- whether x is in the set
s
Example 1
Input retrieved = ["d4", "d1", "d7", "d2"] relevant = ["d1", "d2", "d3"] k = 2 Output 0.333333
As in the worked example: d1 is found, d2 comes too late, and d3 is missing. One of three.
Example 2
Input retrieved = ["a", "b", "c"] relevant = ["a", "c"] k = 2 Output 0.5
The first two results are a and b. One of the two relevant ids is there.
Example 3
Input retrieved = ["a", "a"] relevant = ["a", "b"] k = 2 Output 0.5
a is retrieved twice but counts once. b is missing, so recall is 1 / 2.
Example 4
Input retrieved = ["a"] relevant = [] k = 1 Output 0
No document is relevant, so there is nothing to find. The answer is 0, not an error.
The checks also try
- a relevant id listed twice
- k is 0
- the same search with k raised to 4
- k larger than the result list
- a relevant id listed twice, and one missing
- a repeated result uses up a place
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Where to start
Only the first
kresults count, so cut the list first. Then the question is how many relevant ids are in that cut.Hint 2: The key step
Use sets for both sides. A set holds each id once, so repeats on either side cannot change the count.
Hint 3: The outline
Make a set
targetsfromrelevant. If it is empty, return 0. Make a settopfrom the firstkitems ofretrieved. Count the ids intargetsthat are also intop, and return that count divided by the size oftargets.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Step by step
- The distinct relevant ids. A set removes repeats, so an id listed twice in
relevantis still one document to find. - With nothing to find, recall is zero. Dividing by the size of an empty set would be a division by zero. Returning 0 is the rule this exercise uses for that case.
- The first k results, each id once. Results after position
kdo not count, and a set keeps a document that was retrieved twice from being counted twice. - The share of the relevant ids that were found. The division is by the number of relevant ids, not by
k. Dividing bykwould measure precision at k: the share of the first k results that are relevant.
The same numbered steps are the comments in the solution.
JavaScript
function solve(retrieved, relevant, k) {
// 1. The distinct relevant ids
const targets = new Set(relevant);
// 2. With nothing to find, recall is zero
if (targets.size === 0) return 0;
// 3. The first k results, each id once
const top = new Set(retrieved.slice(0, k));
// 4. The share of the relevant ids that were found
let found = 0;
for (const id of targets) {
if (top.has(id)) found++;
}
return found / targets.size;
}Python
def solve(retrieved, relevant, k):
# 1. The distinct relevant ids
targets = set(relevant)
# 2. With nothing to find, recall is zero
if len(targets) == 0:
return 0
# 3. The first k results, each id once
top = set(retrieved[:k])
# 4. The share of the relevant ids that were found
found = 0
for doc_id in targets:
if doc_id in top:
found += 1
return found / len(targets)
Watch out
- Divide by the number of relevant ids, not by
kand not by the number of results. Dividing bykgives precision at k. - Recall says nothing about the answer. A search can find every document and the model can still answer wrongly; measure that separately.
- Do not adjust the search on the same questions you report. Keep some questions aside and look at them only at the end.
- In JavaScript: A JavaScript
Sethas nolengthand nofilter. Useset.size, and loop over it withfor (const id of set).
Check yourself
What does a recall at 5 of 0.5 mean?
Half of the documents that hold the answer appeared in the first five results. The other half were ranked lower or not retrieved at all.
Why is an evaluation set better than trying a few questions by hand?
The same questions give a number you can compare before and after a change. A few hand-picked tries can look good while the system got worse.