retrieval-eval 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +342 -0
- package/dist/cli.js +1344 -0
- package/dist/index.d.ts +376 -0
- package/dist/index.js +971 -0
- package/package.json +74 -0
- package/spec/README.md +31 -0
- package/spec/chunk-id.md +51 -0
- package/spec/drift.md +49 -0
- package/spec/fixtures/README.md +48 -0
- package/spec/fixtures/basic/expected.json +43 -0
- package/spec/fixtures/basic/judgments.jsonl +4 -0
- package/spec/fixtures/basic/run.jsonl +2 -0
- package/spec/fixtures/chunk-id/expected.json +32 -0
- package/spec/fixtures/drift/corpus.json +38 -0
- package/spec/fixtures/drift/expected.json +22 -0
- package/spec/fixtures/drift/judgments.jsonl +4 -0
- package/spec/fixtures/duplicate-ranking/expected.json +56 -0
- package/spec/fixtures/duplicate-ranking/judgments.jsonl +3 -0
- package/spec/fixtures/duplicate-ranking/run-duplicate-query.jsonl +2 -0
- package/spec/fixtures/duplicate-ranking/run-non-string-key.jsonl +1 -0
- package/spec/fixtures/duplicate-ranking/run.jsonl +2 -0
- package/spec/fixtures/merge/corpus.json +22 -0
- package/spec/fixtures/merge/expected.json +21 -0
- package/spec/fixtures/merge/judgments.jsonl +3 -0
- package/spec/fixtures/no-positives/expected.json +17 -0
- package/spec/fixtures/no-positives/judgments.jsonl +4 -0
- package/spec/fixtures/no-positives/run.jsonl +2 -0
- package/spec/fixtures/nothing-scored/expected.json +9 -0
- package/spec/fixtures/nothing-scored/judgments.jsonl +1 -0
- package/spec/fixtures/nothing-scored/run.jsonl +1 -0
- package/spec/fixtures/qrels/beir.tsv +4 -0
- package/spec/fixtures/qrels/expected.json +14 -0
- package/spec/fixtures/qrels/trec.qrels +4 -0
- package/spec/fixtures/strata/expected.json +40 -0
- package/spec/fixtures/strata/judgments.jsonl +5 -0
- package/spec/fixtures/strata/run.jsonl +5 -0
- package/spec/fixtures/stratum-order/expected.json +30 -0
- package/spec/fixtures/stratum-order/judgments.jsonl +5 -0
- package/spec/fixtures/stratum-order/run.jsonl +5 -0
- package/spec/fixtures/summarize/expected.json +53 -0
- package/spec/fixtures/unsorted-queries/expected.json +23 -0
- package/spec/fixtures/unsorted-queries/judgments.jsonl +3 -0
- package/spec/fixtures/unsound-judgments/expected.json +19 -0
- package/spec/fixtures/unsound-judgments/judgments.jsonl +3 -0
- package/spec/fixtures/unsound-judgments/run.jsonl +1 -0
- package/spec/judgments.schema.json +54 -0
- package/spec/report.schema.json +227 -0
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "retrieval-eval",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Your RAG test suite decays every time you re-chunk. Drift detection that tells you which relevance labels are still true after a chunker or corpus change and re-anchors the ones it can, plus deterministic retrieval metrics and a portable, TREC qrels-compatible judgment format. Zero dependencies, no API key.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rag",
|
|
7
|
+
"drift",
|
|
8
|
+
"label-drift",
|
|
9
|
+
"chunking",
|
|
10
|
+
"retrieval",
|
|
11
|
+
"evaluation",
|
|
12
|
+
"eval",
|
|
13
|
+
"rag-evaluation",
|
|
14
|
+
"ndcg",
|
|
15
|
+
"recall",
|
|
16
|
+
"precision",
|
|
17
|
+
"mrr",
|
|
18
|
+
"map",
|
|
19
|
+
"qrels",
|
|
20
|
+
"trec",
|
|
21
|
+
"information-retrieval",
|
|
22
|
+
"golden-dataset",
|
|
23
|
+
"regression-testing",
|
|
24
|
+
"ci",
|
|
25
|
+
"llm",
|
|
26
|
+
"vector-search",
|
|
27
|
+
"embeddings",
|
|
28
|
+
"reranking",
|
|
29
|
+
"benchmark",
|
|
30
|
+
"typescript"
|
|
31
|
+
],
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"type": "module",
|
|
34
|
+
"bin": {
|
|
35
|
+
"retrieval-eval": "./dist/cli.js"
|
|
36
|
+
},
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"import": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"files": ["dist", "spec", "README.md", "LICENSE"],
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20.11"
|
|
47
|
+
},
|
|
48
|
+
"sideEffects": false,
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "node ../../scripts/sync-spec.mjs && tsup",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test:coverage": "vitest run --coverage"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^22.9.0",
|
|
58
|
+
"@vitest/coverage-v8": "^2.1.4",
|
|
59
|
+
"tsup": "^8.3.5",
|
|
60
|
+
"typescript": "^5.6.3",
|
|
61
|
+
"vitest": "^2.1.4"
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/aton-of-data/retrieval-eval.git",
|
|
66
|
+
"directory": "packages/retrieval-eval"
|
|
67
|
+
},
|
|
68
|
+
"homepage": "https://github.com/aton-of-data/retrieval-eval#readme",
|
|
69
|
+
"bugs": "https://github.com/aton-of-data/retrieval-eval/issues",
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public",
|
|
72
|
+
"provenance": true
|
|
73
|
+
}
|
|
74
|
+
}
|
package/spec/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# The retrieval-eval spec
|
|
2
|
+
|
|
3
|
+
Two file formats and one hash definition. Small on purpose: if implementing this takes more than
|
|
4
|
+
an afternoon in any language, the spec has failed.
|
|
5
|
+
|
|
6
|
+
| File | Purpose |
|
|
7
|
+
|---|---|
|
|
8
|
+
| [`judgments.schema.json`](judgments.schema.json) | relevance labels that survive a re-chunk |
|
|
9
|
+
| [`report.schema.json`](report.schema.json) | results any CI or dashboard can ingest |
|
|
10
|
+
| [`chunk-id.md`](chunk-id.md) | the content-addressed chunk identity |
|
|
11
|
+
| [`drift.md`](drift.md) | the five label statuses and how they are resolved |
|
|
12
|
+
| [`fixtures/`](fixtures/) | conformance cases every implementation must pass, and [their format](fixtures/README.md) |
|
|
13
|
+
|
|
14
|
+
## Design rules
|
|
15
|
+
|
|
16
|
+
1. **JSONL for data, JSON for reports.** Streamable, greppable, diffable in code review.
|
|
17
|
+
2. **A strict superset of TREC qrels.** Any judgments file converts losslessly to
|
|
18
|
+
`query_id 0 doc_id relevance`, so `trec_eval`, `ir_measures`, BEIR and `ir_datasets` all work.
|
|
19
|
+
3. **Zero required dependencies.** SHA-256 and JSON only, both in every standard library.
|
|
20
|
+
4. **Unknown fields are preserved, never rejected.** Tools must round-trip fields they don't know.
|
|
21
|
+
5. **One canonical way to write a judgments file.** Fields in schema order, unknown fields last in
|
|
22
|
+
their original order, compact JSON, one object per line. Two implementations writing the same
|
|
23
|
+
labels produce the same bytes, so a rewrite shows a diff of what changed rather than of the
|
|
24
|
+
whole file.
|
|
25
|
+
6. **Every implementation passes `fixtures/`.** That is what makes a conformance claim mean
|
|
26
|
+
something: machine-readable, runnable black-box, stable over time. Their shape is documented
|
|
27
|
+
in [`fixtures/README.md`](fixtures/README.md), because a normative test suite nobody can read
|
|
28
|
+
without reverse-engineering it is not a spec.
|
|
29
|
+
7. **Byte-identical applies to judgments files and rendered output.** A JSON report agrees as
|
|
30
|
+
*data*: JavaScript writes a whole number as `1` and Python writes `1.0`, so reports are
|
|
31
|
+
compared by parsing, which is what `scripts/check_parity.py` does and says.
|
package/spec/chunk-id.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Chunk identity
|
|
2
|
+
|
|
3
|
+
A relevance label must point at *text*, not at "position 7 of whatever the chunker produced that
|
|
4
|
+
day." Everything `drift` can do follows from that.
|
|
5
|
+
|
|
6
|
+
## Definition
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
chunk_id = "c1:" + lowercase_hex( SHA256( canonical )[0..16] ) # 128 bits, 32 hex chars
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`canonical` is the UTF-8 encoding of these five fields joined by `U+001F` (unit separator):
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
doc_uri ␟ doc_revision ␟ ordinal ␟ normalize(text) ␟ chunker_fingerprint
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Field | Notes |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `doc_uri` | canonical and stable (`notion://page/abc`, `s3://bucket/key`). Not a local path. |
|
|
21
|
+
| `doc_revision` | source etag/version, or `normalize(text)`'s own hash when the source has none |
|
|
22
|
+
| `ordinal` | 0-based chunk index within the document, base-10, no padding |
|
|
23
|
+
| `text` | the chunk body, normalized as below |
|
|
24
|
+
| `chunker_fingerprint` | opaque, stable string identifying chunker + settings (e.g. `recursive/512/64`) |
|
|
25
|
+
|
|
26
|
+
## `normalize(text)`
|
|
27
|
+
|
|
28
|
+
Exactly three steps, in order. Any deviation breaks cross-language agreement:
|
|
29
|
+
|
|
30
|
+
1. Unicode **NFC** normalization.
|
|
31
|
+
2. Line endings `\r\n` and `\r` → `\n`.
|
|
32
|
+
3. Strip leading and trailing whitespace from the **whole string** (not per line).
|
|
33
|
+
|
|
34
|
+
Deliberately *not* done: case folding, punctuation stripping, internal whitespace collapsing.
|
|
35
|
+
Those lose information a retrieval system legitimately depends on.
|
|
36
|
+
|
|
37
|
+
## `text_sha`
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
text_sha = "t1:" + lowercase_hex( SHA256( normalize(text) )[0..16] )
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Recorded on a judgment so a label is **self-describing**: `drift` can re-anchor it after a
|
|
44
|
+
re-chunk without storing the text itself. Store `chunk_text` too when you want `SPLIT` detection
|
|
45
|
+
and can accept the size; use `text_sha` alone when the corpus is sensitive.
|
|
46
|
+
|
|
47
|
+
## Why SHA-256 and not BLAKE3
|
|
48
|
+
|
|
49
|
+
BLAKE3 is faster and would be the better hash. SHA-256 is in the standard library of every
|
|
50
|
+
language that matters, which keeps implementations dependency-free, worth more than throughput
|
|
51
|
+
for files this size. The `c1:`/`t1:` prefixes exist so a future `c2:` can switch.
|
package/spec/drift.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Judgment drift
|
|
2
|
+
|
|
3
|
+
A relevance label is a statement about *text a human read*. When the corpus is re-chunked, the
|
|
4
|
+
chunk ids move but the text mostly does not, so most labels are still sound, and the ones that
|
|
5
|
+
are not need to be identified rather than silently kept.
|
|
6
|
+
|
|
7
|
+
`drift` classifies every label against a live corpus.
|
|
8
|
+
|
|
9
|
+
| Status | Meaning | Recoverable? |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `VALID` | The labeled `chunk_id` is still present in the corpus. | already fine |
|
|
12
|
+
| `RE_ANCHORABLE` | The id is stale, but the exact labeled text is still a live chunk. | yes, `--fix` |
|
|
13
|
+
| `MERGED` | The labeled text was absorbed into a coarser chunk. | yes, `--fix` |
|
|
14
|
+
| `SPLIT` | The labeled text now spans two or more live chunks. | no, needs re-judgment |
|
|
15
|
+
| `ORPHANED` | The text, or its document, is gone. | no, needs re-judgment |
|
|
16
|
+
|
|
17
|
+
## Resolution order
|
|
18
|
+
|
|
19
|
+
1. `chunk_id` present in the corpus → `VALID`.
|
|
20
|
+
2. `text_sha` matches a live chunk exactly → `RE_ANCHORABLE`. Cheapest and most precise, so it
|
|
21
|
+
is tried first. A match in the same `doc_uri` is preferred over one elsewhere.
|
|
22
|
+
3. Two or more live chunks are substrings of `chunk_text` → `SPLIT`, reporting every covering id.
|
|
23
|
+
4. `chunk_text` is a substring of exactly one live chunk → `MERGED`, re-anchoring to it. When
|
|
24
|
+
several chunks contain it, the **shortest** wins: it is the tightest evidence.
|
|
25
|
+
5. Otherwise → `ORPHANED`.
|
|
26
|
+
|
|
27
|
+
Steps 3 and 4 need `chunk_text` on the judgment. With only `text_sha`, re-anchoring still works
|
|
28
|
+
but splits and merges are indistinguishable from orphans, which is the tradeoff for not storing
|
|
29
|
+
corpus text in your labels.
|
|
30
|
+
|
|
31
|
+
## Why MERGED re-anchors but SPLIT does not
|
|
32
|
+
|
|
33
|
+
`MERGED` is a sound inference: a chunk containing text a human judged relevant still contains
|
|
34
|
+
the answer, so the coarser chunk is relevant too. It is reported separately from an exact match
|
|
35
|
+
because the new chunk carries extra material, which dilutes precision-style metrics.
|
|
36
|
+
|
|
37
|
+
`SPLIT` is not sound. The labeled text broke into several chunks and there is no way to know
|
|
38
|
+
which of them carries the answer: possibly only one, possibly all. Assigning the label to all
|
|
39
|
+
of them would inflate recall; picking one would be a guess. So `drift --fix` leaves it, and says
|
|
40
|
+
so.
|
|
41
|
+
|
|
42
|
+
**`--fix` never invents ground truth.** That rule is worth more than the convenience of a fully
|
|
43
|
+
green report.
|
|
44
|
+
|
|
45
|
+
## invalid_ratio
|
|
46
|
+
|
|
47
|
+
The share of labels that are not `VALID`. It counts recoverable labels too, because a metric
|
|
48
|
+
computed before you re-anchor them *is* wrong. The CLI prints the recoverable/needs-a-human
|
|
49
|
+
split alongside it so the number is actionable rather than merely alarming.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Conformance fixtures
|
|
2
|
+
|
|
3
|
+
Every implementation must pass these. That is what makes "parity by spec, not by port" a claim
|
|
4
|
+
rather than a slogan, so the fixtures are normative and this file describes their shape.
|
|
5
|
+
|
|
6
|
+
Each directory is one case. Inputs use the wire formats the spec already defines; `expected.json`
|
|
7
|
+
holds the answers, and **its shape is per-case rather than fixed**, because the cases check
|
|
8
|
+
different things: some check numbers, some check a classification, some check a refusal.
|
|
9
|
+
|
|
10
|
+
| File | Meaning |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `judgments.jsonl` | input, per [`judgments.schema.json`](../judgments.schema.json) |
|
|
13
|
+
| `run.jsonl` | input, one `{"query_id", "ranking"}` object per line |
|
|
14
|
+
| `corpus.json` | input, a corpus snapshot: `{"corpus_fingerprint", "chunker_fingerprint", "chunks"}` |
|
|
15
|
+
| `*.tsv`, `*.qrels` | input, TREC or BEIR qrels |
|
|
16
|
+
| `expected.json` | the answers, in the shape the table below gives |
|
|
17
|
+
|
|
18
|
+
`expected.json` may carry a `note` in any case. It is prose for a human reader, never asserted.
|
|
19
|
+
|
|
20
|
+
## What each case asserts
|
|
21
|
+
|
|
22
|
+
| Case | Asserts |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `basic` | `metrics` at `k`, `metrics_k1` at k=1, `per_query` detail, `qrels_lines` |
|
|
25
|
+
| `chunk-id` | vectors for `normalize`, `text_sha` and `chunk_id` |
|
|
26
|
+
| `drift` | per-label `statuses`, `summary` counts, `reanchor` and `split_into` targets |
|
|
27
|
+
| `merge` | `statuses` and `summary` for a re-chunk that merges paragraphs |
|
|
28
|
+
| `qrels` | the judgments a `beir` and a `trec` file convert to, and the `qrels_lines` back out |
|
|
29
|
+
| `strata` | `metrics`, `per_stratum` scores, and `worst_stratum` |
|
|
30
|
+
| `summarize` | `cases`, each with `samples` and the expected measurement |
|
|
31
|
+
| `no-positives` | `queries_without_positives`, and the metrics over what remains |
|
|
32
|
+
| `nothing-scored` | that no metric is emitted and the verdict is `INDETERMINATE` |
|
|
33
|
+
| `duplicate-ranking` | that a repeated key counts once, plus the runs that are refused outright |
|
|
34
|
+
| `unsound-judgments` | that a set `validate` rejects cannot be scored to a `PASS` |
|
|
35
|
+
| `unsorted-queries` | the order validation issues are emitted in |
|
|
36
|
+
|
|
37
|
+
## Rules a fixture follows
|
|
38
|
+
|
|
39
|
+
1. **Numbers are given to twelve decimal places.** Both implementations round to 1e-12, so a
|
|
40
|
+
fixture that agrees to fewer digits is not testing agreement.
|
|
41
|
+
2. **Inputs are the published formats.** A fixture never invents a file type an implementation
|
|
42
|
+
would not otherwise read.
|
|
43
|
+
3. **A refusal is an answer.** Where the right behaviour is to reject the input, the fixture
|
|
44
|
+
carries the input file and the exact message, because two implementations that reject the
|
|
45
|
+
same file with different words are still two tools.
|
|
46
|
+
4. **A case earns its place by catching something.** `unsorted-queries` exists because every
|
|
47
|
+
other fixture happened to have sorted query ids, which hid a real divergence between the two
|
|
48
|
+
implementations.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"k": 3,
|
|
3
|
+
"metrics": {
|
|
4
|
+
"precision@3": 0.333333333333,
|
|
5
|
+
"recall@3": 0.5,
|
|
6
|
+
"ndcg@3": 0.329500902401,
|
|
7
|
+
"mrr@3": 0.25,
|
|
8
|
+
"map@3": 0.291666666667,
|
|
9
|
+
"hit_rate@3": 0.5
|
|
10
|
+
},
|
|
11
|
+
"metrics_k1": {
|
|
12
|
+
"precision@1": 0.0,
|
|
13
|
+
"recall@1": 0.0,
|
|
14
|
+
"ndcg@1": 0.0,
|
|
15
|
+
"mrr@1": 0.0,
|
|
16
|
+
"map@1": 0.0,
|
|
17
|
+
"hit_rate@1": 0.0
|
|
18
|
+
},
|
|
19
|
+
"per_query": {
|
|
20
|
+
"q1": {
|
|
21
|
+
"precision": 0.666666666667,
|
|
22
|
+
"recall": 1.0,
|
|
23
|
+
"ndcg": 0.659001804802,
|
|
24
|
+
"mrr": 0.5,
|
|
25
|
+
"ap": 0.583333333333,
|
|
26
|
+
"hit_rate": 1.0
|
|
27
|
+
},
|
|
28
|
+
"q2": {
|
|
29
|
+
"precision": 0.0,
|
|
30
|
+
"recall": 0.0,
|
|
31
|
+
"ndcg": 0.0,
|
|
32
|
+
"mrr": 0.0,
|
|
33
|
+
"ap": 0.0,
|
|
34
|
+
"hit_rate": 0.0
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"qrels_lines": [
|
|
38
|
+
"q1 0 d1 2",
|
|
39
|
+
"q1 0 d2 1",
|
|
40
|
+
"q1 0 d3 0",
|
|
41
|
+
"q2 0 d5 1"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"query_id": "q1", "query": "query q1", "doc_uri": "d1", "relevance": 2, "labeled_by": "human:fixture"}
|
|
2
|
+
{"query_id": "q1", "query": "query q1", "doc_uri": "d2", "relevance": 1, "labeled_by": "human:fixture"}
|
|
3
|
+
{"query_id": "q1", "query": "query q1", "doc_uri": "d3", "relevance": 0, "labeled_by": "human:fixture"}
|
|
4
|
+
{"query_id": "q2", "query": "query q2", "doc_uri": "d5", "relevance": 1, "labeled_by": "human:fixture"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"normalize": [
|
|
3
|
+
{
|
|
4
|
+
"in": " hello \r\n world ",
|
|
5
|
+
"out": "hello \n world"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"in": "café",
|
|
9
|
+
"out": "café"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"in": "\n\nx\n\n",
|
|
13
|
+
"out": "x"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"text_sha": [
|
|
17
|
+
{
|
|
18
|
+
"text": "hello world",
|
|
19
|
+
"out": "t1:b94d27b9934d3e08a52e52d7da7dabfa"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"chunk_id": [
|
|
23
|
+
{
|
|
24
|
+
"doc_uri": "s3://b/k",
|
|
25
|
+
"doc_revision": "v1",
|
|
26
|
+
"ordinal": 0,
|
|
27
|
+
"text": "hello world",
|
|
28
|
+
"chunker_fingerprint": "recursive/512/64",
|
|
29
|
+
"out": "c1:06095518ff131adc3d109bfcf1521efa"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"corpus_fingerprint": "recursive/384/64",
|
|
3
|
+
"chunker_fingerprint": "recursive/384/64",
|
|
4
|
+
"chunks": [
|
|
5
|
+
{
|
|
6
|
+
"chunk_id": "c1:926073213714b1c617790e335bb0fabc",
|
|
7
|
+
"doc_uri": "notion://page/9f2a",
|
|
8
|
+
"doc_revision": "rev-7",
|
|
9
|
+
"ordinal": 0,
|
|
10
|
+
"text": "Annual plans may be refunded within 30 days of renewal.",
|
|
11
|
+
"text_sha": "t1:2bd6e363ab6a85703fabe2546b4ab2f8"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"chunk_id": "c1:4e556228a30930f72ad70a7f040abb44",
|
|
15
|
+
"doc_uri": "notion://page/9f2a",
|
|
16
|
+
"doc_revision": "rev-7",
|
|
17
|
+
"ordinal": 1,
|
|
18
|
+
"text": "Monthly plans are non-refundable after the billing date.",
|
|
19
|
+
"text_sha": "t1:943718a0217044cdb953c232f163f931"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"chunk_id": "c1:a4d7f269f473170d90c13b7e1af802c8",
|
|
23
|
+
"doc_uri": "notion://page/9f2a",
|
|
24
|
+
"doc_revision": "rev-7",
|
|
25
|
+
"ordinal": 2,
|
|
26
|
+
"text": "Enterprise agreements are governed by the master service agreement.",
|
|
27
|
+
"text_sha": "t1:a7202b2d8254f135bd469512898ab347"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"chunk_id": "c1:26d9eb35e49d5b7b76645144e439a694",
|
|
31
|
+
"doc_uri": "notion://page/9f2a",
|
|
32
|
+
"doc_revision": "rev-7",
|
|
33
|
+
"ordinal": 3,
|
|
34
|
+
"text": "Refund eligibility for enterprise plans is negotiated per contract.",
|
|
35
|
+
"text_sha": "t1:e3abd792d087bf155aecd300d9c6b311"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"statuses": {
|
|
3
|
+
"q1": "VALID",
|
|
4
|
+
"q2": "RE_ANCHORABLE",
|
|
5
|
+
"q3": "SPLIT",
|
|
6
|
+
"q4": "ORPHANED"
|
|
7
|
+
},
|
|
8
|
+
"summary": {
|
|
9
|
+
"valid": 1,
|
|
10
|
+
"re_anchorable": 1,
|
|
11
|
+
"merged": 0,
|
|
12
|
+
"split": 1,
|
|
13
|
+
"orphaned": 1,
|
|
14
|
+
"invalid_ratio": 0.75
|
|
15
|
+
},
|
|
16
|
+
"reanchor": {
|
|
17
|
+
"q2": "c1:4e556228a30930f72ad70a7f040abb44"
|
|
18
|
+
},
|
|
19
|
+
"split_into": {
|
|
20
|
+
"q3": ["c1:a4d7f269f473170d90c13b7e1af802c8", "c1:26d9eb35e49d5b7b76645144e439a694"]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"query_id": "q1", "query": "refund window annual", "doc_uri": "notion://page/9f2a", "chunk_id": "c1:926073213714b1c617790e335bb0fabc", "text_sha": "t1:2bd6e363ab6a85703fabe2546b4ab2f8", "relevance": 2, "corpus_fingerprint": "recursive/384/64", "labeled_by": "human:ana"}
|
|
2
|
+
{"query_id": "q2", "query": "monthly refund", "doc_uri": "notion://page/9f2a", "chunk_id": "c1:a718502bc7696d9dad49d30a6d5f3ee5", "text_sha": "t1:943718a0217044cdb953c232f163f931", "relevance": 1, "corpus_fingerprint": "recursive/512/64", "labeled_by": "human:ana"}
|
|
3
|
+
{"query_id": "q3", "query": "enterprise refund terms", "doc_uri": "notion://page/9f2a", "chunk_id": "c1:3dfd8e297d137dc87cdceca26f5fa07c", "text_sha": "t1:0973fc56d32bfc9d45473dac002029c6", "chunk_text": "Enterprise agreements are governed by the master service agreement.\n\nRefund eligibility for enterprise plans is negotiated per contract.", "relevance": 2, "corpus_fingerprint": "recursive/512/64", "labeled_by": "human:ana"}
|
|
4
|
+
{"query_id": "q4", "query": "removed policy", "doc_uri": "notion://page/deleted", "chunk_id": "c1:41a0ba2c26579c92ffb5486781200d7b", "text_sha": "t1:4b6f2850b5f76bdae351b46d52358a5c", "relevance": 1, "corpus_fingerprint": "recursive/512/64", "labeled_by": "human:ana"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"k": 5,
|
|
3
|
+
"note": "A retriever that returns the same key twice must not be paid twice for it. q1 returns d1 five times and finds one of two relevant documents, so recall is 0.5, not 2.5. Counted naively every repeat scores as a fresh hit and recall, map and ndcg all rise above 1.0, which can carry a failing run past a gate. Only the first occurrence counts, and the report names the queries it happened to rather than correcting them silently.",
|
|
4
|
+
"queries_with_duplicates": [
|
|
5
|
+
"q1",
|
|
6
|
+
"q2"
|
|
7
|
+
],
|
|
8
|
+
"deduped_rankings": {
|
|
9
|
+
"q1": [
|
|
10
|
+
"d1"
|
|
11
|
+
],
|
|
12
|
+
"q2": [
|
|
13
|
+
"d9",
|
|
14
|
+
"d3"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"metrics": {
|
|
18
|
+
"precision@5": 0.2,
|
|
19
|
+
"recall@5": 0.75,
|
|
20
|
+
"ndcg@5": 0.72858220535,
|
|
21
|
+
"mrr@5": 0.75,
|
|
22
|
+
"map@5": 0.5,
|
|
23
|
+
"hit_rate@5": 1.0
|
|
24
|
+
},
|
|
25
|
+
"verdict": "PASS",
|
|
26
|
+
"reason": "2 queries repeated a key in their ranking; only the first occurrence of each was counted",
|
|
27
|
+
"rejected": {
|
|
28
|
+
"note": "These are malformed rather than merely unsound, so parse_run refuses them outright. Two entries for one query mean the file no longer says what that query's ranking is, and picking one silently would be a guess.",
|
|
29
|
+
"cases": [
|
|
30
|
+
{
|
|
31
|
+
"file": "run-duplicate-query.jsonl",
|
|
32
|
+
"message": "run:2: duplicate entry for query q1, already on line 1"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"file": "run-non-string-key.jsonl",
|
|
36
|
+
"message": "run:1: ranking[1] must be a non-empty string"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"per_query": {
|
|
41
|
+
"q1": {
|
|
42
|
+
"note": "d1 five times: one of two relevant documents found, so recall is 0.5",
|
|
43
|
+
"recall": 0.5,
|
|
44
|
+
"precision": 0.2,
|
|
45
|
+
"mrr": 1.0,
|
|
46
|
+
"ap": 0.5
|
|
47
|
+
},
|
|
48
|
+
"q2": {
|
|
49
|
+
"note": "d9,d3 twice each: the one relevant document found at rank 2",
|
|
50
|
+
"recall": 1.0,
|
|
51
|
+
"precision": 0.2,
|
|
52
|
+
"mrr": 0.5,
|
|
53
|
+
"ap": 0.5
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"query_id": "q1", "query": "how do I reset my password", "doc_uri": "d1", "relevance": 2, "labeled_by": "human:fixture"}
|
|
2
|
+
{"query_id": "q1", "query": "how do I reset my password", "doc_uri": "d2", "relevance": 1, "labeled_by": "human:fixture"}
|
|
3
|
+
{"query_id": "q2", "query": "where is my invoice", "doc_uri": "d3", "relevance": 2, "labeled_by": "human:fixture"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"query_id": "q1", "ranking": ["d1", 42]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"corpus_fingerprint": "recursive/384",
|
|
3
|
+
"chunker_fingerprint": "recursive/384",
|
|
4
|
+
"chunks": [
|
|
5
|
+
{
|
|
6
|
+
"chunk_id": "c1:d577264d22f91060a5abc8d3df2b1cea",
|
|
7
|
+
"doc_uri": "wiki://refunds",
|
|
8
|
+
"doc_revision": "r3",
|
|
9
|
+
"ordinal": 0,
|
|
10
|
+
"text": "Annual plans may be refunded within 30 days of renewal.\n\nMonthly plans are non-refundable after the billing date.",
|
|
11
|
+
"text_sha": "t1:fe457555142c231ba419124e398f4b84"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"chunk_id": "c1:59b4853f2ca27466c9d2931c73ae489c",
|
|
15
|
+
"doc_uri": "wiki://refunds",
|
|
16
|
+
"doc_revision": "r3",
|
|
17
|
+
"ordinal": 1,
|
|
18
|
+
"text": "Enterprise refunds are negotiated per contract.",
|
|
19
|
+
"text_sha": "t1:4029941ea9e639a37fc53f410246171f"
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"statuses": {
|
|
3
|
+
"m1": "MERGED",
|
|
4
|
+
"m2": "MERGED",
|
|
5
|
+
"m3": "RE_ANCHORABLE"
|
|
6
|
+
},
|
|
7
|
+
"summary": {
|
|
8
|
+
"valid": 0,
|
|
9
|
+
"re_anchorable": 1,
|
|
10
|
+
"merged": 2,
|
|
11
|
+
"split": 0,
|
|
12
|
+
"orphaned": 0,
|
|
13
|
+
"invalid_ratio": 1.0
|
|
14
|
+
},
|
|
15
|
+
"reanchor": {
|
|
16
|
+
"m1": "c1:d577264d22f91060a5abc8d3df2b1cea",
|
|
17
|
+
"m2": "c1:d577264d22f91060a5abc8d3df2b1cea",
|
|
18
|
+
"m3": "c1:59b4853f2ca27466c9d2931c73ae489c"
|
|
19
|
+
},
|
|
20
|
+
"note": "Both m1 and m2 re-anchor to the same coarser chunk: it now contains both paragraphs."
|
|
21
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
{"query_id": "m1", "query": "annual plan refund window", "doc_uri": "wiki://refunds", "chunk_id": "c1:5e7383036261e5a707b24dcfa6f591d0", "text_sha": "t1:2bd6e363ab6a85703fabe2546b4ab2f8", "chunk_text": "Annual plans may be refunded within 30 days of renewal.", "relevance": 2, "corpus_fingerprint": "recursive/512", "labeled_by": "human:ana"}
|
|
2
|
+
{"query_id": "m2", "query": "are monthly plans refundable", "doc_uri": "wiki://refunds", "chunk_id": "c1:92f79d5e1a7e764d0493fd9808a4a883", "text_sha": "t1:943718a0217044cdb953c232f163f931", "chunk_text": "Monthly plans are non-refundable after the billing date.", "relevance": 2, "corpus_fingerprint": "recursive/512", "labeled_by": "human:ana"}
|
|
3
|
+
{"query_id": "m3", "query": "enterprise refund policy", "doc_uri": "wiki://refunds", "chunk_id": "c1:c3e4013ec7956f184dcfbdf05a971e26", "text_sha": "t1:4029941ea9e639a37fc53f410246171f", "chunk_text": "Enterprise refunds are negotiated per contract.", "relevance": 1, "corpus_fingerprint": "recursive/512", "labeled_by": "human:ana"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"k": 3,
|
|
3
|
+
"note": "q2 has no label at relevance >= 1, so it is excluded from the averages rather than scored zero. Recall, nDCG, MRR and AP are undefined when there is nothing relevant to find.",
|
|
4
|
+
"queries": 2,
|
|
5
|
+
"queries_scored": 1,
|
|
6
|
+
"queries_without_positives": [
|
|
7
|
+
"q2"
|
|
8
|
+
],
|
|
9
|
+
"metrics": {
|
|
10
|
+
"precision@3": 0.666666666667,
|
|
11
|
+
"recall@3": 1.0,
|
|
12
|
+
"ndcg@3": 0.963940433317,
|
|
13
|
+
"mrr@3": 1.0,
|
|
14
|
+
"map@3": 0.833333333333,
|
|
15
|
+
"hit_rate@3": 1.0
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{"query_id": "q1", "query": "answerable", "doc_uri": "d1", "relevance": 2, "labeled_by": "human:fixture"}
|
|
2
|
+
{"query_id": "q1", "query": "answerable", "doc_uri": "d2", "relevance": 1, "labeled_by": "human:fixture"}
|
|
3
|
+
{"query_id": "q2", "query": "nothing in the corpus answers this", "doc_uri": "d5", "relevance": 0, "labeled_by": "human:fixture"}
|
|
4
|
+
{"query_id": "q2", "query": "nothing in the corpus answers this", "doc_uri": "d6", "relevance": 0, "labeled_by": "human:fixture"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Every label is below the threshold, so the averages are undefined. The report omits the metrics and the verdict is INDETERMINATE. Emitting 0 would fail an absolute gate over an empty set.",
|
|
3
|
+
"threshold": 2,
|
|
4
|
+
"queries": 1,
|
|
5
|
+
"queries_scored": 0,
|
|
6
|
+
"status": "INDETERMINATE",
|
|
7
|
+
"reason": "no query had a label at relevance >= 2, so no metric was computed",
|
|
8
|
+
"gate_reason": "recall@3:0.5: no query was scored, so 'recall@3' was not computed"
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"query_id": "q1", "query": "only a weak label", "doc_uri": "d1", "relevance": 1, "labeled_by": "human:fixture", "stratum": "billing"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"query_id": "q1", "ranking": ["d1"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"note": "Both files describe the same labels. The BEIR form is 3 columns with a header; the TREC form is 4 columns. A parser that accepts only one of them cannot read the collections it claims to read.",
|
|
3
|
+
"beir": [
|
|
4
|
+
{ "query_id": "q1", "doc_uri": "d3", "relevance": 1 },
|
|
5
|
+
{ "query_id": "q1", "doc_uri": "d7", "relevance": 2 },
|
|
6
|
+
{ "query_id": "q2", "doc_uri": "d4", "relevance": 1 }
|
|
7
|
+
],
|
|
8
|
+
"trec": [
|
|
9
|
+
{ "query_id": "q1", "doc_uri": "d3", "relevance": 1 },
|
|
10
|
+
{ "query_id": "q1", "doc_uri": "d7", "relevance": 2 },
|
|
11
|
+
{ "query_id": "q2", "doc_uri": "d4", "relevance": 0 }
|
|
12
|
+
],
|
|
13
|
+
"qrels_lines": ["q1 0 d3 1", "q1 0 d7 2", "q2 0 d4 1"]
|
|
14
|
+
}
|