llm-search-eval 0.2.0__tar.gz

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.
Files changed (69) hide show
  1. llm_search_eval-0.2.0/.gitignore +15 -0
  2. llm_search_eval-0.2.0/CHANGELOG.md +59 -0
  3. llm_search_eval-0.2.0/LICENSE +21 -0
  4. llm_search_eval-0.2.0/PKG-INFO +440 -0
  5. llm_search_eval-0.2.0/README.md +398 -0
  6. llm_search_eval-0.2.0/action.yml +271 -0
  7. llm_search_eval-0.2.0/docs/README.md +12 -0
  8. llm_search_eval-0.2.0/docs/ci.md +118 -0
  9. llm_search_eval-0.2.0/docs/connectors.md +93 -0
  10. llm_search_eval-0.2.0/docs/cost.md +65 -0
  11. llm_search_eval-0.2.0/docs/judges.md +93 -0
  12. llm_search_eval-0.2.0/docs/releasing.md +33 -0
  13. llm_search_eval-0.2.0/docs/reports.md +52 -0
  14. llm_search_eval-0.2.0/examples/electronics/README.md +31 -0
  15. llm_search_eval-0.2.0/examples/electronics/products.jsonl +22 -0
  16. llm_search_eval-0.2.0/examples/electronics/qrels.csv +31 -0
  17. llm_search_eval-0.2.0/examples/electronics/queries.jsonl +10 -0
  18. llm_search_eval-0.2.0/examples/electronics/runs/bm25.jsonl +29 -0
  19. llm_search_eval-0.2.0/examples/electronics/runs/hybrid.trec +36 -0
  20. llm_search_eval-0.2.0/examples/electronics/runs/semantic.csv +43 -0
  21. llm_search_eval-0.2.0/examples/electronics/sample-report/cost.json +21 -0
  22. llm_search_eval-0.2.0/examples/electronics/sample-report/judgments.jsonl +49 -0
  23. llm_search_eval-0.2.0/examples/electronics/sample-report/report.html +406 -0
  24. llm_search_eval-0.2.0/examples/electronics/sample-report/report.json +1230 -0
  25. llm_search_eval-0.2.0/examples/electronics/sample-report/report.md +77 -0
  26. llm_search_eval-0.2.0/pyproject.toml +103 -0
  27. llm_search_eval-0.2.0/src/llm_search_eval/__init__.py +33 -0
  28. llm_search_eval-0.2.0/src/llm_search_eval/__main__.py +4 -0
  29. llm_search_eval-0.2.0/src/llm_search_eval/agreement.py +261 -0
  30. llm_search_eval-0.2.0/src/llm_search_eval/assets/dashboard.css +51 -0
  31. llm_search_eval-0.2.0/src/llm_search_eval/assets/dashboard.js +286 -0
  32. llm_search_eval-0.2.0/src/llm_search_eval/cli.py +557 -0
  33. llm_search_eval-0.2.0/src/llm_search_eval/connectors/__init__.py +24 -0
  34. llm_search_eval-0.2.0/src/llm_search_eval/connectors/algolia.py +117 -0
  35. llm_search_eval-0.2.0/src/llm_search_eval/connectors/base.py +150 -0
  36. llm_search_eval-0.2.0/src/llm_search_eval/connectors/elasticsearch.py +165 -0
  37. llm_search_eval-0.2.0/src/llm_search_eval/errors.py +23 -0
  38. llm_search_eval-0.2.0/src/llm_search_eval/evaluate.py +606 -0
  39. llm_search_eval-0.2.0/src/llm_search_eval/gate.py +283 -0
  40. llm_search_eval-0.2.0/src/llm_search_eval/html_report.py +116 -0
  41. llm_search_eval-0.2.0/src/llm_search_eval/io.py +342 -0
  42. llm_search_eval-0.2.0/src/llm_search_eval/judges/__init__.py +24 -0
  43. llm_search_eval-0.2.0/src/llm_search_eval/judges/base.py +132 -0
  44. llm_search_eval-0.2.0/src/llm_search_eval/judges/cache.py +84 -0
  45. llm_search_eval-0.2.0/src/llm_search_eval/judges/mock.py +80 -0
  46. llm_search_eval-0.2.0/src/llm_search_eval/judges/openai_compat.py +202 -0
  47. llm_search_eval-0.2.0/src/llm_search_eval/judges/panel.py +134 -0
  48. llm_search_eval-0.2.0/src/llm_search_eval/judges/prompts.py +50 -0
  49. llm_search_eval-0.2.0/src/llm_search_eval/metrics.py +171 -0
  50. llm_search_eval-0.2.0/src/llm_search_eval/models.py +85 -0
  51. llm_search_eval-0.2.0/src/llm_search_eval/py.typed +0 -0
  52. llm_search_eval-0.2.0/src/llm_search_eval/report.py +358 -0
  53. llm_search_eval-0.2.0/src/llm_search_eval/text.py +79 -0
  54. llm_search_eval-0.2.0/src/llm_search_eval/usage.py +269 -0
  55. llm_search_eval-0.2.0/tests/__init__.py +0 -0
  56. llm_search_eval-0.2.0/tests/conftest.py +47 -0
  57. llm_search_eval-0.2.0/tests/test_agreement.py +110 -0
  58. llm_search_eval-0.2.0/tests/test_cli.py +333 -0
  59. llm_search_eval-0.2.0/tests/test_connectors.py +213 -0
  60. llm_search_eval-0.2.0/tests/test_evaluate.py +259 -0
  61. llm_search_eval-0.2.0/tests/test_gate.py +90 -0
  62. llm_search_eval-0.2.0/tests/test_html_report.py +91 -0
  63. llm_search_eval-0.2.0/tests/test_io.py +188 -0
  64. llm_search_eval-0.2.0/tests/test_judges.py +328 -0
  65. llm_search_eval-0.2.0/tests/test_metrics.py +111 -0
  66. llm_search_eval-0.2.0/tests/test_panel.py +110 -0
  67. llm_search_eval-0.2.0/tests/test_report.py +84 -0
  68. llm_search_eval-0.2.0/tests/test_text.py +48 -0
  69. llm_search_eval-0.2.0/tests/test_usage.py +109 -0
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ coverage.xml
12
+ htmlcov/
13
+ reports/
14
+ .judge-cache*.jsonl
15
+ .DS_Store
@@ -0,0 +1,59 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.2.0] - 2026-10-30
11
+
12
+ First release intended for production use: judge panels, cost control, an interactive dashboard, CI quality gates, and live search connectors.
13
+
14
+ ### Added
15
+
16
+ - Judge panels: repeat `--judge` to combine several judges by majority, mean, median, min, or max (`--aggregate`), with a configurable quorum (`--min-votes`) and tolerance for individual judge failures.
17
+ - Inter-judge agreement in every panel report: unanimous share, Fleiss' kappa, Krippendorff's alpha (nominal, ordinal, interval), and pairwise Cohen's kappa (plain and linear or quadratic weighted).
18
+ - Judge versus gold calibration with `--calibrate` and `--calibration-sample`: exact, within-one, and binary agreement, kappa, and bias for the panel and each member. Gold labels still decide the scores.
19
+ - Token and cost tracking per model with user-supplied prices (`--price`, `--price-file`), budgets on cost, tokens, or judgments (`--max-cost`, `--max-tokens`, `--max-judgments`), and a `cost.json` report.
20
+ - Interactive `report.html` dashboard in a single self-contained file: metric charts, system comparison with per-query deltas, judge agreement, cost, and a searchable per-query drilldown.
21
+ - `llm-search-eval gate` command: metric floors and absolute or relative drop limits against the baseline or a reference report, optionally only for statistically significant drops, with Markdown and JSON output.
22
+ - `llm-search-eval report` command to re-render Markdown or HTML from a saved `report.json`.
23
+ - `llm-search-eval fetch` command and connectors for Elasticsearch, OpenSearch, and Algolia, with request templates, authentication from environment variables, retries, and optional official client extras (`elasticsearch`, `opensearch`, `algolia`).
24
+ - Reusable composite GitHub Action (`action.yml`) that runs an evaluation, applies the gate, writes the job summary, and exposes report paths as outputs.
25
+ - Tag-driven PyPI release workflow with trusted publishing or an API token.
26
+ - Panel votes and gold labels in `judgments.jsonl`.
27
+ - Documentation under `docs/` for judges, cost, reports, CI, connectors, and releasing.
28
+ - Type checking with mypy in CI, and Python 3.13 in the test matrix.
29
+
30
+ ### Changed
31
+
32
+ - `--format` now defaults to `md,json,html`.
33
+ - `--judge` accepts specs such as `openai=gpt-4o-mini,temperature=0` and can be repeated.
34
+ - The mock judge accepts `noise`, `seed`, and `label` options for reproducible disagreement in offline panel runs.
35
+ - The sample report in `examples/electronics` now includes the dashboard, judge calibration, and cost files.
36
+
37
+ ## [0.1.0] - 2026-10-30
38
+
39
+ Initial release.
40
+
41
+ ### Added
42
+
43
+ - `llm-search-eval run` command to score one or more search systems and compare them against a baseline.
44
+ - `llm-search-eval validate` command to detect unknown ids, missing results, and binary-looking labels before an evaluation.
45
+ - Dataset loaders for queries, products, and graded gold labels in JSONL, JSON, CSV, and TSV, plus TREC qrels and run files.
46
+ - Ranking metrics: nDCG@k with exponential or linear gain, precision@k, recall@k, MRR, and MAP, with configurable relevance threshold and evaluation depth.
47
+ - LLM-as-judge relevance grading on a 0 to 3 scale through any OpenAI-compatible chat completions API, with retries, backoff, and tolerant response parsing.
48
+ - Deterministic mock judge for tests, CI, and offline runs.
49
+ - Pluggable judge registry with `register_judge` and `module:Class` loading.
50
+ - Content-addressed JSONL judgment cache.
51
+ - Paired randomization significance test, win/tie/loss counts, and per-metric deltas against the baseline.
52
+ - Persian and English text normalization, language detection, and per-language metric breakdowns.
53
+ - Markdown and JSON reports plus a `judgments.jsonl` audit log.
54
+ - Bilingual electronics example dataset with three systems and a sample report.
55
+ - Continuous integration with ruff and pytest on Python 3.10, 3.11, and 3.12.
56
+
57
+ [Unreleased]: https://github.com/sadrmanesh/llm-search-eval/compare/v0.2.0...HEAD
58
+ [0.2.0]: https://github.com/sadrmanesh/llm-search-eval/compare/v0.1.0...v0.2.0
59
+ [0.1.0]: https://github.com/sadrmanesh/llm-search-eval/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Naser Sadrmanesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,440 @@
1
+ Metadata-Version: 2.5
2
+ Name: llm-search-eval
3
+ Version: 0.2.0
4
+ Summary: Evaluate product search quality with IR metrics, multi-judge LLM labeling, cost tracking, and CI quality gates.
5
+ Project-URL: Homepage, https://github.com/sadrmanesh/llm-search-eval
6
+ Project-URL: Repository, https://github.com/sadrmanesh/llm-search-eval
7
+ Project-URL: Issues, https://github.com/sadrmanesh/llm-search-eval/issues
8
+ Project-URL: Documentation, https://github.com/sadrmanesh/llm-search-eval/tree/main/docs
9
+ Project-URL: Changelog, https://github.com/sadrmanesh/llm-search-eval/blob/main/CHANGELOG.md
10
+ Author-email: Naser Sadrmanesh <info.sadrmanesh@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: algolia,ecommerce,elasticsearch,evaluation,github-actions,information-retrieval,inter-rater-agreement,llm,llm-as-judge,ndcg,opensearch,persian,recommendation,search
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
25
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
26
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
27
+ Classifier: Topic :: Software Development :: Quality Assurance
28
+ Classifier: Typing :: Typed
29
+ Requires-Python: >=3.10
30
+ Provides-Extra: algolia
31
+ Requires-Dist: algoliasearch>=4; extra == 'algolia'
32
+ Provides-Extra: dev
33
+ Requires-Dist: mypy>=1.10; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=4.1; extra == 'dev'
35
+ Requires-Dist: pytest>=7.4; extra == 'dev'
36
+ Requires-Dist: ruff>=0.5; extra == 'dev'
37
+ Provides-Extra: elasticsearch
38
+ Requires-Dist: elasticsearch>=8; extra == 'elasticsearch'
39
+ Provides-Extra: opensearch
40
+ Requires-Dist: opensearch-py>=2; extra == 'opensearch'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # llm-search-eval
44
+
45
+ [![CI](https://github.com/sadrmanesh/llm-search-eval/actions/workflows/ci.yml/badge.svg)](https://github.com/sadrmanesh/llm-search-eval/actions/workflows/ci.yml)
46
+ ![Python](https://img.shields.io/badge/python-3.10%20to%203.13-blue)
47
+ ![Dependencies](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)
48
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
49
+
50
+ **Measure product search quality with classic IR metrics and panels of LLM relevance judges, compare systems with statistical rigor, and block ranking regressions in CI.**
51
+
52
+ Built for e-commerce teams who ship ranking changes (BM25 to vectors, hybrid retrieval, rerankers, query rewriting) and need a trustworthy answer to one question: *is the new system actually better for shoppers?* First-class support for **Persian and English** queries.
53
+
54
+ ---
55
+
56
+ ## Contents
57
+
58
+ - [Why](#why)
59
+ - [Features](#features)
60
+ - [Installation](#installation)
61
+ - [Quick start](#quick-start)
62
+ - [Data formats](#data-formats)
63
+ - [Relevance judges](#relevance-judges)
64
+ - [Judge panels and agreement](#judge-panels-and-agreement)
65
+ - [Cost tracking and budgets](#cost-tracking-and-budgets)
66
+ - [Metrics](#metrics)
67
+ - [Reports and dashboard](#reports-and-dashboard)
68
+ - [Quality gates and the GitHub Action](#quality-gates-and-the-github-action)
69
+ - [Live search connectors](#live-search-connectors)
70
+ - [Python API](#python-api)
71
+ - [Persian support](#persian-support)
72
+ - [Documentation](#documentation)
73
+ - [Roadmap](#roadmap)
74
+ - [خلاصه فارسی](#خلاصه-فارسی)
75
+
76
+ ## Why
77
+
78
+ Offline search evaluation usually breaks down in the same places:
79
+
80
+ - **Labels run out.** A gold set labelled last quarter does not cover the products a new retriever surfaces today, so unlabeled results silently count as misses and the new system looks worse than it is.
81
+ - **Averages hide noise.** A +2% nDCG gain on 50 queries can easily be chance.
82
+ - **One model is one opinion.** A single LLM judge has its own biases, and nobody checks how well it agrees with the humans who wrote the gold labels.
83
+ - **Costs creep.** Judging thousands of pairs with a frontier model adds up, and nobody notices until the invoice.
84
+ - **Multilingual catalogs are messy.** Persian text mixes Arabic and Persian code points, digit systems, and zero width joiners, which breaks naive matching.
85
+
86
+ `llm-search-eval` merges your gold labels with LLM judgments for the gaps, can ask several judges and measure how much they agree with each other and with your gold labels, tracks every token against a budget, scores every system on the same label pool, attaches a paired significance test to every comparison, and turns the result into a dashboard and a pass or fail check for pull requests.
87
+
88
+ ## Features
89
+
90
+ - **Classic IR metrics:** nDCG@k (exponential or linear gain), precision@k, recall@k, MRR, MAP, with configurable relevance threshold and evaluation depth.
91
+ - **LLM-as-judge:** graded 0 to 3 relevance labels from any OpenAI-compatible endpoint (OpenAI, OpenRouter, vLLM, Ollama, LM Studio, internal gateways), with retries, backoff, and robust JSON parsing.
92
+ - **Judge panels:** combine several judges by majority, mean, median, min, or max, and tolerate individual failures.
93
+ - **Agreement statistics:** Cohen's kappa (plain and weighted), Fleiss' kappa, and Krippendorff's alpha between judges, plus calibration of every judge against your gold labels.
94
+ - **Cost tracking:** token usage per model, user-supplied prices, and budgets on cost, tokens, or judgments, with a `cost.json` report.
95
+ - **System comparison:** deltas for every metric, win/tie/loss counts, and a paired randomization test against a baseline.
96
+ - **Interactive dashboard:** one self-contained HTML file with charts, system comparison, and per-query drilldown. No server, no CDN.
97
+ - **Quality gates:** `llm-search-eval gate` and a reusable GitHub Action fail the build when a metric drops below a floor or regresses.
98
+ - **Live connectors:** fetch runs straight from Elasticsearch, OpenSearch, or Algolia.
99
+ - **Pluggable judges** by decorator or `module:Class`, a **deterministic mock judge** for CI, and a **content-addressed judgment cache** so re-runs never pay twice.
100
+ - **Flexible inputs:** JSONL, JSON, CSV, TSV, and TREC run/qrels files, with common field aliases.
101
+ - **Zero runtime dependencies**, Python 3.10 to 3.13, fully typed.
102
+
103
+ ## Installation
104
+
105
+ ```bash
106
+ pip install llm-search-eval
107
+ ```
108
+
109
+ or straight from a release tag:
110
+
111
+ ```bash
112
+ pip install "git+https://github.com/sadrmanesh/llm-search-eval@v0.2.0"
113
+ ```
114
+
115
+ Optional extras let the connectors reuse official clients: `llm-search-eval[elasticsearch]`, `[opensearch]`, and `[algolia]`. They are not needed for the default HTTP mode.
116
+
117
+ From source, for development:
118
+
119
+ ```bash
120
+ git clone https://github.com/sadrmanesh/llm-search-eval
121
+ cd llm-search-eval
122
+ python -m venv .venv && source .venv/bin/activate
123
+ pip install -e ".[dev]"
124
+ ```
125
+
126
+ ## Quick start
127
+
128
+ The repository ships a small bilingual electronics catalog with three systems to compare. Run it offline with the mock judge:
129
+
130
+ ```bash
131
+ llm-search-eval run \
132
+ --queries examples/electronics/queries.jsonl \
133
+ --products examples/electronics/products.jsonl \
134
+ --qrels examples/electronics/qrels.csv \
135
+ --system bm25=examples/electronics/runs/bm25.jsonl \
136
+ --system semantic=examples/electronics/runs/semantic.csv \
137
+ --system hybrid=examples/electronics/runs/hybrid.trec \
138
+ --judge mock \
139
+ --output-dir reports/
140
+ ```
141
+
142
+ ```text
143
+ | System | ndcg@5 | ndcg@10 | precision@5 | recall@10 | mrr | map |
144
+ | :--- | ---: | ---: | ---: | ---: | ---: | ---: |
145
+ | `bm25` | 0.5305 | 0.5305 | 0.2200 | 0.4833 | 0.8000 | 0.4306 |
146
+ | `semantic` | **0.9865** | **0.9865** | **0.4200** | **0.9667** | **1.0000** | **0.9667** |
147
+ | `hybrid` | 0.9767 | 0.9767 | **0.4200** | **0.9667** | **1.0000** | 0.9583 |
148
+ ```
149
+
150
+ The output directory holds `report.md`, `report.json`, the `report.html` dashboard, `cost.json`, and `judgments.jsonl`. See the [sample report](examples/electronics/sample-report/report.md) and the [sample dashboard](examples/electronics/sample-report/report.html).
151
+
152
+ Switch to a real LLM judge, with a price and a budget:
153
+
154
+ ```bash
155
+ export OPENAI_API_KEY=...
156
+ llm-search-eval run ... --judge openai=gpt-4o-mini --cache .judge-cache.jsonl \
157
+ --price gpt-4o-mini=0.15,0.60 --max-cost 2
158
+ ```
159
+
160
+ Check inputs before spending tokens:
161
+
162
+ ```bash
163
+ llm-search-eval validate --queries ... --products ... --qrels ... --system ...
164
+ ```
165
+
166
+ Run `llm-search-eval run --help` for every option.
167
+
168
+ ## Data formats
169
+
170
+ Every file can be `.jsonl`, `.json`, `.csv`, or `.tsv`. Field names are case-insensitive.
171
+
172
+ **Queries**: `query_id` (alias `qid`), `text` (alias `query`), optional `lang`. Language is detected when omitted.
173
+
174
+ ```json
175
+ {"query_id": "q06", "text": "هدفون بی سیم نویز کنسلینگ", "lang": "fa"}
176
+ ```
177
+
178
+ **Products**: `product_id` (alias `id`, `doc_id`), `title` (alias `name`), optional `description`, `category`, `brand`, `price`, `attributes`. Any other column becomes an attribute that judges can see.
179
+
180
+ ```json
181
+ {"product_id": "p06", "title": "Sony WH-1000XM5 Wireless Noise Cancelling Headphones", "brand": "Sony", "category": "Headphones"}
182
+ ```
183
+
184
+ **Gold labels** (optional): `query_id`, `product_id`, `relevance` (alias `label`) on the graded scale below. TREC qrels files (`.qrels`, `.txt`) are also accepted.
185
+
186
+ | Grade | Meaning |
187
+ | :---: | :--- |
188
+ | 3 | Exact match: fully satisfies the query |
189
+ | 2 | Relevant: satisfies the main intent with minor gaps |
190
+ | 1 | Partially relevant: related, but misses key requirements |
191
+ | 0 | Irrelevant |
192
+
193
+ **System runs**: one file per system, passed as `--system NAME=PATH`. Any of these layouts work:
194
+
195
+ - one row per result with `query_id`, `product_id`, and `rank` (ascending) or `score` (descending); file order is used otherwise;
196
+ - one object per query: `{"query_id": "q1", "results": ["p3", "p1", "p7"]}`;
197
+ - TREC run files (`.trec`, `.run`): `qid Q0 docid rank score tag`.
198
+
199
+ ## Relevance judges
200
+
201
+ For every (query, product) pair in each system's top `depth` results, labels are resolved in this order:
202
+
203
+ 1. **Gold label** from `--qrels`, if present. Gold labels are never re-judged.
204
+ 2. **Cached judgment**, if the same judge configuration has already seen this exact query and product content.
205
+ 3. **Judge**, if `--judge` is set. Otherwise the result is counted as unlabeled (not relevant), and the report shows the coverage gap.
206
+
207
+ All systems are scored against the same merged label pool, so the ideal ranking used by nDCG and the recall denominator are identical for every system.
208
+
209
+ | Judge spec | Use it for |
210
+ | :--- | :--- |
211
+ | `mock` | Tests, CI, and offline dry runs. Deterministic term coverage over title, brand, category, and attributes. Not a quality signal. `mock=noise=0.2,seed=1` adds reproducible disagreement for trying panels. |
212
+ | `openai` or `openai=MODEL` | Any OpenAI-compatible chat completions API. Configure with `--model`, `--base-url` (or `OPENAI_BASE_URL`), and `--api-key-env` (default `OPENAI_API_KEY`), or per judge: `openai=MODEL,base_url=URL,api_key_env=VAR`. Local servers on `localhost` need no key. |
213
+ | `module:Class` | Your own judge. |
214
+
215
+ Running against a local model with Ollama:
216
+
217
+ ```bash
218
+ llm-search-eval run ... --judge "openai=qwen2.5:14b,base_url=http://localhost:11434/v1"
219
+ ```
220
+
221
+ Writing a custom judge:
222
+
223
+ ```python
224
+ from llm_search_eval import Judge, Judgment, register_judge
225
+
226
+
227
+ @register_judge("click-model")
228
+ class ClickModelJudge(Judge):
229
+ name = "click-model"
230
+
231
+ def __init__(self, table_path: str = "ctr.parquet") -> None:
232
+ self.table = load_ctr_table(table_path)
233
+
234
+ @property
235
+ def fingerprint(self) -> str: # part of the cache key
236
+ return f"click-model:{self.table.version}"
237
+
238
+ def judge(self, query, product):
239
+ ctr = self.table.lookup(query.text, product.product_id)
240
+ return Judgment(relevance=min(3, int(ctr * 30)), reason=f"ctr={ctr:.3f}")
241
+ ```
242
+
243
+ ```bash
244
+ llm-search-eval run ... --judge my_package.judges:ClickModelJudge
245
+ ```
246
+
247
+ ## Judge panels and agreement
248
+
249
+ Repeat `--judge` to form a panel. Every member grades every pair and the panel combines the grades:
250
+
251
+ ```bash
252
+ llm-search-eval run ... \
253
+ --judge openai=gpt-4o-mini \
254
+ --judge openai=gpt-4o \
255
+ --judge "openai=qwen2.5:14b,base_url=http://localhost:11434/v1" \
256
+ --aggregate majority --calibrate --calibration-sample 200
257
+ ```
258
+
259
+ - `--aggregate` is `majority` (default), `mean`, `median`, `min`, or `max`. `--min-votes` sets how many members must answer; the default is a strict majority.
260
+ - The report shows the unanimous share, **Fleiss' kappa**, **Krippendorff's alpha** (nominal, ordinal, interval), and pairwise **Cohen's kappa** (plain and quadratic-weighted).
261
+ - `--calibrate` also sends gold-labeled pairs to the judges and reports exact, within-one, and binary agreement, kappa, and bias against the gold labels, for the panel and for each member. Gold labels still decide the scores.
262
+
263
+ Details: [docs/judges.md](docs/judges.md).
264
+
265
+ ## Cost tracking and budgets
266
+
267
+ Every judge request records its token usage. Prices are never guessed: pass them per model in USD per million tokens with `--price MODEL=INPUT,OUTPUT` or `--price-file prices.json`. A price for `gpt-4o-mini` also covers dated snapshots such as `gpt-4o-mini-2024-07-18`, and models without a price show the cost as unknown rather than zero.
268
+
269
+ `--max-cost`, `--max-tokens`, and `--max-judgments` stop new requests once a limit is reached; remaining pairs stay unlabeled and the report says how many were skipped. Cache hits are free. Details: [docs/cost.md](docs/cost.md).
270
+
271
+ ## Metrics
272
+
273
+ | Metric | Spec | Definition |
274
+ | :--- | :--- | :--- |
275
+ | nDCG | `ndcg@k` | DCG of the top k divided by the ideal DCG from all known labels for the query. Gain is `2^rel - 1` (default) or `rel` with `--gain linear`. |
276
+ | Precision | `precision@k`, `p@k` | Relevant results in the top k, divided by k. |
277
+ | Recall | `recall@k`, `r@k` | Relevant results in the top k, divided by all known relevant products. |
278
+ | MRR | `mrr`, `mrr@k` | Reciprocal rank of the first relevant result. |
279
+ | MAP | `map`, `map@k` | Average precision over relevant ranks, divided by all known relevant products. |
280
+
281
+ - Binary metrics treat grades `>= --threshold` (default `2`) as relevant.
282
+ - Rankings are cut at `--depth`, which defaults to the largest k among the selected metrics.
283
+ - A query with no relevant product in the label pool has undefined metrics. It is excluded from the mean instead of being scored as zero, and the JSON report records `n` for every aggregate.
284
+ - The **primary metric** (default: the largest-k nDCG, or `--primary`) is used to rank systems and for significance testing.
285
+
286
+ ### Significance testing
287
+
288
+ Each system is compared to the baseline (first system, or `--baseline`) with a two-sided paired randomization (sign flip) test on per-query values of the primary metric. It makes no normality assumption and is the standard choice in IR evaluation. Use `--permutations` and `--seed` to trade precision for speed and to keep results reproducible.
289
+
290
+ ## Reports and dashboard
291
+
292
+ `--output-dir` receives:
293
+
294
+ | File | Contents |
295
+ | :--- | :--- |
296
+ | `report.md` | Summary table with best values in bold, comparison with p-values and win/tie/loss, all metric deltas, primary metric by query language, label coverage, judges and agreement, and cost. |
297
+ | `report.json` | Everything above plus per-query metric values, rankings, labels, and the full configuration. |
298
+ | `report.html` | Interactive dashboard in one self-contained file: metric charts, system comparison with per-query deltas, judge agreement, cost, and a searchable per-query drilldown that shows every system's ranking side by side. |
299
+ | `cost.json` | Token usage, cost per model, and budget state. |
300
+ | `judgments.jsonl` | Every judge label with its reason, source, panel votes, and gold label for calibration pairs. |
301
+
302
+ `--format` selects any of `md`, `json`, and `html` (all by default). `llm-search-eval report report.json` re-renders the Markdown or HTML from a saved JSON report. Details: [docs/reports.md](docs/reports.md).
303
+
304
+ The `run` command exits with code `0` on success, `2` for invalid input or configuration, and `3` when some judgments failed.
305
+
306
+ ## Quality gates and the GitHub Action
307
+
308
+ `llm-search-eval gate` turns a report into a pass or fail check:
309
+
310
+ ```bash
311
+ llm-search-eval gate reports/report.json --min ndcg@10=0.6 --max-drop 0.01 --significant-only
312
+ ```
313
+
314
+ It checks metric floors and drops against the baseline system or against a `--reference` report from the default branch, optionally only when the drop is statistically significant, prints a Markdown table, and exits with `1` on failure.
315
+
316
+ The repository is also a reusable GitHub Action that runs the evaluation, applies the gate, and posts the result to the job summary:
317
+
318
+ ```yaml
319
+ - uses: sadrmanesh/llm-search-eval@v0.2.0
320
+ env:
321
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
322
+ with:
323
+ queries: eval/queries.jsonl
324
+ products: eval/products.jsonl
325
+ qrels: eval/qrels.csv
326
+ systems: |
327
+ production=eval/runs/production.jsonl
328
+ candidate=eval/runs/candidate.jsonl
329
+ judges: openai=gpt-4o-mini
330
+ max-cost: "2"
331
+ min: ndcg@10=0.55
332
+ max-drop: "0.01"
333
+ ```
334
+
335
+ All inputs and outputs: [docs/ci.md](docs/ci.md).
336
+
337
+ ## Live search connectors
338
+
339
+ Evaluate the index you actually serve. `fetch` runs every query against the backend and writes a run file:
340
+
341
+ ```bash
342
+ llm-search-eval fetch --backend elasticsearch --url http://localhost:9200 --index products \
343
+ --queries eval/queries.jsonl -k 20 -o runs/es.jsonl
344
+ llm-search-eval fetch --backend algolia --index products --queries eval/queries.jsonl -o runs/algolia.jsonl
345
+ ```
346
+
347
+ Elasticsearch and OpenSearch accept a JSON request template with `{{query}}`, `{{k}}`, and `{{lang}}` placeholders, so you can evaluate your production query as is. Details: [docs/connectors.md](docs/connectors.md).
348
+
349
+ ## Python API
350
+
351
+ ```python
352
+ from llm_search_eval import EvaluationConfig, create_judge, evaluate
353
+ from llm_search_eval.io import load_dataset, load_run
354
+ from llm_search_eval.judges import JudgmentCache
355
+ from llm_search_eval.report import render_markdown
356
+
357
+ dataset = load_dataset("queries.jsonl", "products.jsonl", "qrels.csv")
358
+ runs = [load_run("runs/bm25.jsonl", "bm25"), load_run("runs/hybrid.trec", "hybrid")]
359
+
360
+ report = evaluate(
361
+ dataset,
362
+ runs,
363
+ EvaluationConfig(metrics=["ndcg@10", "recall@50", "mrr"], baseline="bm25"),
364
+ judge=create_judge("openai", model="gpt-4o-mini"),
365
+ cache=JudgmentCache(".judge-cache.jsonl"),
366
+ )
367
+
368
+ print(report.mean("hybrid", "ndcg@10"))
369
+ print(render_markdown(report))
370
+ ```
371
+
372
+ Judge panels, cost tracking, the dashboard, and gates have Python entry points too:
373
+
374
+ ```python
375
+ from llm_search_eval import (
376
+ Budget,
377
+ CostTracker,
378
+ JudgePanel,
379
+ PriceTable,
380
+ check_report,
381
+ render_html,
382
+ )
383
+
384
+ panel = JudgePanel([create_judge("openai", model="gpt-4o-mini"), create_judge("mock")])
385
+ tracker = CostTracker(
386
+ prices=PriceTable.parse_specs(["gpt-4o-mini=0.15,0.60"]),
387
+ budget=Budget(max_cost_usd=2.0),
388
+ )
389
+ report = evaluate(dataset, runs, EvaluationConfig(calibrate=True), judge=panel, tracker=tracker)
390
+
391
+ with open("report.html", "w", encoding="utf-8") as fh:
392
+ fh.write(render_html(report))
393
+ gate = check_report(report.to_dict(), floors={"ndcg@10": 0.6}, max_drop=0.01)
394
+ print(gate.passed)
395
+ ```
396
+
397
+ ## Persian support
398
+
399
+ - Arabic `ي` and `ك` are unified with Persian `ی` and `ک`; Persian and Arabic-Indic digits become ASCII digits.
400
+ - Diacritics, tatweel, and directional marks are removed, and the zero width non-joiner is treated as a word boundary.
401
+ - Query language is detected from the script when `lang` is missing, and reports break down the primary metric by language.
402
+ - The LLM judge prompt states the query language and counts translations and transliterations between Persian and English as matches.
403
+
404
+ ## Documentation
405
+
406
+ | Guide | Topic |
407
+ | :--- | :--- |
408
+ | [docs/judges.md](docs/judges.md) | Judge specs, panels, agreement, calibration |
409
+ | [docs/cost.md](docs/cost.md) | Prices, budgets, `cost.json` |
410
+ | [docs/reports.md](docs/reports.md) | Report files and the HTML dashboard |
411
+ | [docs/ci.md](docs/ci.md) | The `gate` command and the GitHub Action |
412
+ | [docs/connectors.md](docs/connectors.md) | Elasticsearch, OpenSearch, and Algolia |
413
+ | [docs/releasing.md](docs/releasing.md) | Versioning and PyPI publishing |
414
+
415
+ ## Roadmap
416
+
417
+ Ideas for upcoming releases:
418
+
419
+ - Pairwise (A versus B) LLM judging as an alternative to pointwise grades.
420
+ - Bootstrap confidence intervals for every metric.
421
+ - Sticky pull request comments from the GitHub Action.
422
+ - More connectors, such as Typesense, Meilisearch, and Vespa.
423
+
424
+ ## Contributing
425
+
426
+ Issues and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup and conventions, and [CHANGELOG.md](CHANGELOG.md) for release notes.
427
+
428
+ ## خلاصه فارسی
429
+
430
+ <div dir="rtl">
431
+
432
+ **llm-search-eval** ابزاری متن باز برای سنجش کیفیت جستجو و پیشنهاد محصول در فروشگاه های اینترنتی است. این ابزار معیارهای کلاسیک بازیابی اطلاعات مانند nDCG و MRR و MAP را با داوری مدل های زبانی ترکیب می کند تا نتایجی که برچسب ندارند هم ارزیابی شوند. چند سیستم جستجو را کنار هم مقایسه می کند و برای هر مقایسه آزمون معناداری آماری ارائه می دهد. پرسش های فارسی و انگلیسی هر دو پشتیبانی می شوند و نویسه های عربی، ارقام فارسی و فاصله های مجازی به شکل یکسان نرمال سازی می شوند.
433
+
434
+ در نسخه 0.2.0 چند داور مدل زبانی می توانند با هم رای بدهند و میزان توافق آن ها با شاخص های کاپای کوهن، کاپای فلایس و آلفای کریپندورف گزارش می شود. هزینه و توکن هر اجرا با سقف بودجه کنترل می شود. گزارش تعاملی HTML در یک فایل مستقل ساخته می شود. یک GitHub Action آماده هم هست که در صورت افت کیفیت جستجو، بررسی را ناموفق می کند. اتصال مستقیم به Elasticsearch و OpenSearch و Algolia هم اضافه شده است.
435
+
436
+ </div>
437
+
438
+ ## License
439
+
440
+ [MIT](LICENSE) © 2026 Naser Sadrmanesh