Skip to content
Evaluation & shipping · 1 of 2 · about 25 minutes

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

  1. With no relevant ids there is nothing to find. Return 0 instead of dividing by zero.
  2. Take the first k results. Anything after that does not count, and a repeated result still uses up a place.
  3. Count how many of the relevant ids are among them. Each relevant id counts once, however often it was retrieved.
  4. 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
RankRetrieved idIn the first k?Relevant?
1d4yesno
2d1yesyes: found
3d7no: k is 2no
4d2no: k is 2yes, but too late
1234
retrievedd4d1d7d2
in the first 2yesyesnono
  • relevant, inside the cut
  • relevant, after the cut
The search results in order, with a cut after rank 2. Only d1 is both relevant and inside the cut. d2 is relevant but sits at rank 4, and d3 was never retrieved.

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 k results, 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 s holds
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.

  1. Hint 1: Where to start

    Only the first k results count, so cut the list first. Then the question is how many relevant ids are in that cut.

  2. 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.

  3. Hint 3: The outline

    Make a set targets from relevant. If it is empty, return 0. Make a set top from the first k items of retrieved. Count the ids in targets that are also in top, and return that count divided by the size of targets.

Solution

From here on the page gives the method away. If you are still solving, go back to the hints.

Step by step

  1. The distinct relevant ids. A set removes repeats, so an id listed twice in relevant is still one document to find.
  2. 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.
  3. The first k results, each id once. Results after position k do not count, and a set keeps a document that was retrieved twice from being counted twice.
  4. The share of the relevant ids that were found. The division is by the number of relevant ids, not by k. Dividing by k would 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 k and not by the number of results. Dividing by k gives 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 Set has no length and no filter. Use set.size, and loop over it with for (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.