mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-22 18:57:14 +00:00
rrf: rank fusion without the database
This commit is contained in:
Generated
+10
@@ -286,6 +286,7 @@ dependencies = [
|
||||
"cueseg",
|
||||
"mdscrub",
|
||||
"pdftriage",
|
||||
"rrf",
|
||||
"serde_json",
|
||||
"textkit",
|
||||
]
|
||||
@@ -369,6 +370,15 @@ version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rrf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"corpus-core",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
|
||||
@@ -18,6 +18,7 @@ mdscrub = { path = "crates/mdscrub", version = "0.1.0" }
|
||||
pdftriage = { path = "crates/pdftriage", version = "0.1.0" }
|
||||
cuenorm = { path = "crates/cuenorm", version = "0.1.0" }
|
||||
cueseg = { path = "crates/cueseg", version = "0.1.0" }
|
||||
rrf = { path = "crates/rrf", version = "0.1.0" }
|
||||
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
fancy-regex = "0.16"
|
||||
|
||||
@@ -58,6 +58,7 @@ $ for f in *.pdf; do pdftriage --quiet "$f" && ocrmypdf "$f" "ocr/$f"; done
|
||||
| `pdftriage` | Whether a PDF's text layer is healthy, garbled, sparse, permuted, or absent — and so whether OCR is worth its cost |
|
||||
| `cuenorm` | Any subtitle format in, one normalised cue stream out |
|
||||
| `cueseg` | A timed transcript into topic chapters |
|
||||
| `rrf` | Several ranked lists into one, without the database |
|
||||
|
||||
## The contract
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "rrf"
|
||||
description = "Reciprocal rank fusion: several ranked lists in, one ranked list out."
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap.workspace = true
|
||||
corpus-core.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,220 @@
|
||||
//! Reciprocal rank fusion: several ranked lists in, one ranked list out.
|
||||
//!
|
||||
//! The useful half of hybrid search, with none of the database. Vector search
|
||||
//! and full-text search disagree about everything except what matters, and
|
||||
//! their scores are not comparable — a cosine distance and a `ts_rank` are
|
||||
//! different quantities on different scales. Fusion sidesteps that by
|
||||
//! throwing the scores away and keeping only the ranks: a result that both
|
||||
//! channels put near the top beats one that either channel loves alone.
|
||||
//!
|
||||
//! A channel may be weighted, which is how a metadata match outranks a
|
||||
//! body-text tie: when somebody searches for a book by its title, the book
|
||||
//! rarely repeats its own title in its body, so the channel that matched the
|
||||
//! title is the one telling the truth.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use corpus_core::{Channel, Hit};
|
||||
|
||||
/// The constant that flattens the difference between rank 1 and rank 2.
|
||||
///
|
||||
/// Sixty, from the paper the method comes from and from every implementation
|
||||
/// since. It is large enough that the top of one list does not swamp the rest
|
||||
/// of another, and it is the number every other system's fused scores are
|
||||
/// comparable against, which is worth more than tuning it.
|
||||
pub const K: f64 = 60.0;
|
||||
|
||||
/// How much a metadata match counts for, against a body-text match at the
|
||||
/// same rank.
|
||||
pub const META_WEIGHT: f64 = 1.5;
|
||||
|
||||
/// What each channel's rank is worth.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Weights {
|
||||
/// The rank constant.
|
||||
pub k: f64,
|
||||
/// Per-channel weights.
|
||||
pub channels: HashMap<Channel, f64>,
|
||||
}
|
||||
|
||||
impl Default for Weights {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
k: K,
|
||||
channels: HashMap::from([
|
||||
(Channel::Vector, 1.0),
|
||||
(Channel::Fts, 1.0),
|
||||
(Channel::Meta, META_WEIGHT),
|
||||
]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Weights {
|
||||
/// What one channel's rank is worth. An unnamed channel counts for one.
|
||||
pub fn of(&self, channel: Channel) -> f64 {
|
||||
self.channels.get(&channel).copied().unwrap_or(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fuse ranked lists into one.
|
||||
///
|
||||
/// Every hit is grouped by its `id`, and each contributes `weight / (k +
|
||||
/// rank)` — the rank the producer gave it, not its position in the stream, so
|
||||
/// the lists may arrive interleaved. The record that comes back for an id is
|
||||
/// the one that ranked it best, carrying its payload, with `rank` and `score`
|
||||
/// replaced by its place and score in the fused list.
|
||||
///
|
||||
/// Ties keep the order the ids were first seen in, so the same input always
|
||||
/// produces the same output.
|
||||
pub fn fuse(hits: &[Hit], weights: &Weights) -> Vec<Hit> {
|
||||
let mut order: Vec<&str> = Vec::new();
|
||||
let mut fused: HashMap<&str, Fusing<'_>> = HashMap::new();
|
||||
|
||||
for hit in hits {
|
||||
let entry = fused.entry(hit.id.as_str()).or_insert_with(|| {
|
||||
order.push(hit.id.as_str());
|
||||
Fusing {
|
||||
best: hit,
|
||||
score: 0.0,
|
||||
seen: HashMap::new(),
|
||||
}
|
||||
});
|
||||
// A channel that ranks the same id twice is a producer contradicting
|
||||
// itself; the later word is taken as the corrected one.
|
||||
if let Some(previous) = entry.seen.insert(hit.channel, hit.rank) {
|
||||
entry.score -= weights.of(hit.channel) / (weights.k + previous as f64);
|
||||
}
|
||||
entry.score += weights.of(hit.channel) / (weights.k + hit.rank as f64);
|
||||
if hit.rank < entry.best.rank {
|
||||
entry.best = hit;
|
||||
}
|
||||
}
|
||||
|
||||
let mut ranked: Vec<(&str, f64)> = order
|
||||
.iter()
|
||||
.map(|id| (*id, fused[id].score))
|
||||
.collect::<Vec<_>>();
|
||||
// Stable, so equal scores stay in first-seen order.
|
||||
ranked.sort_by(|a, b| b.1.total_cmp(&a.1));
|
||||
|
||||
ranked
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(at, (id, score))| {
|
||||
let mut hit = fused[id].best.clone();
|
||||
hit.rank = at as u32 + 1;
|
||||
hit.score = score;
|
||||
hit
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct Fusing<'a> {
|
||||
best: &'a Hit,
|
||||
score: f64,
|
||||
seen: HashMap<Channel, u32>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn hit(id: &str, channel: Channel, rank: u32) -> Hit {
|
||||
Hit::new(id, channel, rank, 0.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_result_both_channels_like_beats_one_either_channel_loves() {
|
||||
let hits = [
|
||||
hit("both", Channel::Vector, 2),
|
||||
hit("vector-only", Channel::Vector, 1),
|
||||
hit("both", Channel::Fts, 2),
|
||||
hit("fts-only", Channel::Fts, 1),
|
||||
];
|
||||
let fused = fuse(&hits, &Weights::default());
|
||||
assert_eq!(fused[0].id, "both");
|
||||
assert_eq!(fused[0].rank, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_metadata_match_outranks_a_body_tie() {
|
||||
let hits = [hit("title", Channel::Meta, 1), hit("body", Channel::Fts, 1)];
|
||||
let fused = fuse(&hits, &Weights::default());
|
||||
assert_eq!(fused[0].id, "title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scores_are_the_ones_the_method_defines() {
|
||||
let hits = [hit("a", Channel::Vector, 1), hit("a", Channel::Fts, 3)];
|
||||
let fused = fuse(&hits, &Weights::default());
|
||||
assert_eq!(fused[0].score, 1.0 / 61.0 + 1.0 / 63.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_record_that_comes_back_is_the_one_that_ranked_best() {
|
||||
let mut best = hit("a", Channel::Vector, 1);
|
||||
best.payload.insert("from".into(), "vector".into());
|
||||
let mut worse = hit("a", Channel::Fts, 9);
|
||||
worse.payload.insert("from".into(), "fts".into());
|
||||
|
||||
let fused = fuse(&[worse, best], &Weights::default());
|
||||
assert_eq!(fused[0].payload["from"], "vector");
|
||||
assert_eq!(fused[0].channel, Channel::Vector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranks_come_out_consecutive_from_one() {
|
||||
let hits = [
|
||||
hit("a", Channel::Vector, 1),
|
||||
hit("b", Channel::Vector, 2),
|
||||
hit("c", Channel::Vector, 3),
|
||||
];
|
||||
let fused = fuse(&hits, &Weights::default());
|
||||
assert_eq!(
|
||||
fused.iter().map(|hit| hit.rank).collect::<Vec<_>>(),
|
||||
[1, 2, 3]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tie_keeps_the_order_the_ids_arrived_in() {
|
||||
let hits = [
|
||||
hit("second", Channel::Vector, 5),
|
||||
hit("first", Channel::Fts, 5),
|
||||
];
|
||||
let fused = fuse(&hits, &Weights::default());
|
||||
assert_eq!(fused[0].id, "second");
|
||||
assert_eq!(fused[1].id, "first");
|
||||
assert_eq!(fused[0].score, fused[1].score);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_channel_that_ranks_the_same_id_twice_counts_once() {
|
||||
let twice = fuse(
|
||||
&[hit("a", Channel::Vector, 9), hit("a", Channel::Vector, 1)],
|
||||
&Weights::default(),
|
||||
);
|
||||
let once = fuse(&[hit("a", Channel::Vector, 1)], &Weights::default());
|
||||
assert_eq!(twice[0].score, once[0].score);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weights_can_be_changed() {
|
||||
let weights = Weights {
|
||||
k: 1.0,
|
||||
channels: HashMap::from([(Channel::Fts, 10.0)]),
|
||||
};
|
||||
let fused = fuse(
|
||||
&[hit("meta", Channel::Meta, 1), hit("fts", Channel::Fts, 1)],
|
||||
&weights,
|
||||
);
|
||||
assert_eq!(fused[0].id, "fts");
|
||||
assert_eq!(fused[0].score, 10.0 / 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_in_nothing_out() {
|
||||
assert!(fuse(&[], &Weights::default()).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Reciprocal rank fusion: several ranked lists in, one ranked list out.
|
||||
|
||||
use std::io::{BufWriter, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
|
||||
use clap::Parser;
|
||||
use corpus_core::{Channel, Hit, exit, ndjson};
|
||||
use rrf::Weights;
|
||||
|
||||
/// Fuse several ranked lists of hits into one.
|
||||
///
|
||||
/// Reads hit records on stdin, in any order and interleaved if you like —
|
||||
/// each carries the rank its own channel gave it, so the stream does not have
|
||||
/// to be grouped. Writes the fused list, best first, with `rank` and `score`
|
||||
/// replaced by the fused ones.
|
||||
///
|
||||
/// Database-agnostic on purpose. By the time a hit reaches here it is already
|
||||
/// yours, so this is the one tool in the suite that is worth calling in
|
||||
/// process rather than as a subprocess.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
about,
|
||||
after_help = "\
|
||||
Example:
|
||||
|
||||
cat vector.ndjson fts.ndjson | rrf --limit 10
|
||||
|
||||
Weights are given as channel=number, and default to vec=1, fts=1, meta=1.5 —
|
||||
a metadata match outranks a body-text match at the same rank, because a book
|
||||
rarely repeats its own title in its body."
|
||||
)]
|
||||
struct Args {
|
||||
/// Hit stream to read. Reads stdin when absent.
|
||||
path: Option<PathBuf>,
|
||||
|
||||
/// How many results to keep. All of them when absent.
|
||||
#[arg(long, short = 'n', value_name = "COUNT")]
|
||||
limit: Option<usize>,
|
||||
|
||||
/// The rank constant.
|
||||
#[arg(long = "rrf-k", default_value_t = rrf::K, value_name = "K")]
|
||||
k: f64,
|
||||
|
||||
/// What a channel's rank is worth, as `channel=number`. Repeatable.
|
||||
#[arg(long, value_name = "CHANNEL=WEIGHT")]
|
||||
weight: Vec<String>,
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
let weights = match weights(&args) {
|
||||
Ok(weights) => weights,
|
||||
Err(why) => {
|
||||
eprintln!("rrf: {why}");
|
||||
return ExitCode::from(exit::USAGE);
|
||||
}
|
||||
};
|
||||
|
||||
let hits: Result<Vec<Hit>, _> = match args.path.as_deref() {
|
||||
Some(path) => match std::fs::File::open(path) {
|
||||
Ok(file) => ndjson::read(std::io::BufReader::new(file)).collect(),
|
||||
Err(e) => {
|
||||
eprintln!("rrf: {}: {e}", path.display());
|
||||
return ExitCode::from(exit::IO);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
let mut text = Vec::new();
|
||||
if let Err(e) = std::io::stdin().read_to_end(&mut text) {
|
||||
eprintln!("rrf: {e}");
|
||||
return ExitCode::from(exit::IO);
|
||||
}
|
||||
ndjson::read(text.as_slice()).collect()
|
||||
}
|
||||
};
|
||||
let hits = match hits {
|
||||
Ok(hits) => hits,
|
||||
Err(e) => {
|
||||
eprintln!("rrf: {e}");
|
||||
return ExitCode::from(if e.is_broken_pipe() {
|
||||
exit::OK
|
||||
} else {
|
||||
exit::DATA
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let fused = rrf::fuse(&hits, &weights);
|
||||
let fused = &fused[..args.limit.unwrap_or(fused.len()).min(fused.len())];
|
||||
|
||||
let mut out = BufWriter::new(std::io::stdout().lock());
|
||||
for hit in fused {
|
||||
if let Err(e) = ndjson::write_line(&mut out, hit) {
|
||||
// A consumer that stopped reading is not a failure.
|
||||
if e.is_broken_pipe() {
|
||||
return ExitCode::from(exit::OK);
|
||||
}
|
||||
eprintln!("rrf: {e}");
|
||||
return ExitCode::from(exit::IO);
|
||||
}
|
||||
}
|
||||
match out.flush() {
|
||||
Ok(()) => ExitCode::from(exit::OK),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => ExitCode::from(exit::OK),
|
||||
Err(e) => {
|
||||
eprintln!("rrf: {e}");
|
||||
ExitCode::from(exit::IO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn weights(args: &Args) -> Result<Weights, String> {
|
||||
if !args.k.is_finite() || args.k <= 0.0 {
|
||||
return Err(format!("--rrf-k {} must be a positive number", args.k));
|
||||
}
|
||||
let mut weights = Weights {
|
||||
k: args.k,
|
||||
..Weights::default()
|
||||
};
|
||||
for given in &args.weight {
|
||||
let (channel, weight) = given
|
||||
.split_once('=')
|
||||
.ok_or_else(|| format!("--weight {given:?} is not channel=number"))?;
|
||||
let channel: Channel = serde_json::from_value(serde_json::Value::String(channel.into()))
|
||||
.map_err(|_| format!("--weight {given:?} names no channel this knows"))?;
|
||||
let weight: f64 = weight
|
||||
.parse()
|
||||
.map_err(|_| format!("--weight {given:?} is not channel=number"))?;
|
||||
if !weight.is_finite() {
|
||||
return Err(format!("--weight {given:?} is not a number"));
|
||||
}
|
||||
weights.channels.insert(channel, weight);
|
||||
}
|
||||
Ok(weights)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ cuekit.workspace = true
|
||||
cueseg.workspace = true
|
||||
mdscrub.workspace = true
|
||||
pdftriage.workspace = true
|
||||
rrf.workspace = true
|
||||
corpus-core.workspace = true
|
||||
serde_json.workspace = true
|
||||
textkit.workspace = true
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@ in this directory are the answers that Python gave, at one commit:
|
||||
| `book-pipeline` | `7499e3257572968b437ecfdc79feccc91b96585b` |
|
||||
| `podcast-download-systemd` | `ab20139cd0defb400a0dd9ed9034e03c1d1e356d` |
|
||||
| `podcast-pipeline` | `4566302444a6aa3609bd0d6685323619bd33c72b` |
|
||||
| `knowledge-mcp` | `3d02cd5f9985e09f7354178e1907dbc28a7436da` |
|
||||
|
||||
Regenerating against a different commit will move the goldens. That is the
|
||||
point — the diff is the review. Record the new commit here in the same change.
|
||||
@@ -17,7 +18,7 @@ a checkout lives is a fact about one machine and must not be compiled in:
|
||||
|
||||
```sh
|
||||
CATENA_PARITY_PYTHON=/path/to/venv/bin/python
|
||||
CATENA_PARITY_PYTHONPATH=/path/to/book-pipeline/src:/path/to/podcast-pipeline/src
|
||||
CATENA_PARITY_PYTHONPATH=/path/to/book-pipeline/src:/path/to/podcast-pipeline/src:/path/to/knowledge-mcp/src
|
||||
CATENA_PARITY_NODE=node # only if it is not already on PATH
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{"id": "id0", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id1", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id2", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id3", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id4", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id5", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id6", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id7", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id8", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id9", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
{"id": "id5", "channel": "vec", "rank": 5, "score": 0.0}
|
||||
{"id": "id1", "channel": "fts", "rank": 5, "score": 0.0}
|
||||
{"id": "id1", "channel": "vec", "rank": 1, "score": 0.0}
|
||||
{"id": "id5", "channel": "fts", "rank": 1, "score": 0.0}
|
||||
{"id": "id3", "channel": "vec", "rank": 3, "score": 0.0}
|
||||
{"id": "id3", "channel": "fts", "rank": 3, "score": 0.0}
|
||||
{"id": "id2", "channel": "vec", "rank": 2, "score": 0.0}
|
||||
{"id": "id4", "channel": "fts", "rank": 2, "score": 0.0}
|
||||
{"id": "id4", "channel": "vec", "rank": 4, "score": 0.0}
|
||||
{"id": "id2", "channel": "fts", "rank": 4, "score": 0.0}
|
||||
@@ -0,0 +1,20 @@
|
||||
{"id": "id1", "channel": "vec", "rank": 1, "score": 1.0}
|
||||
{"id": "id2", "channel": "vec", "rank": 2, "score": 0.5}
|
||||
{"id": "id3", "channel": "vec", "rank": 3, "score": 0.3333333333333333}
|
||||
{"id": "id4", "channel": "vec", "rank": 4, "score": 0.25}
|
||||
{"id": "id5", "channel": "vec", "rank": 5, "score": 0.2}
|
||||
{"id": "id6", "channel": "vec", "rank": 6, "score": 0.16666666666666666}
|
||||
{"id": "id7", "channel": "vec", "rank": 7, "score": 0.14285714285714285}
|
||||
{"id": "id8", "channel": "vec", "rank": 8, "score": 0.125}
|
||||
{"id": "id9", "channel": "vec", "rank": 9, "score": 0.1111111111111111}
|
||||
{"id": "id10", "channel": "vec", "rank": 10, "score": 0.1}
|
||||
{"id": "id11", "channel": "vec", "rank": 11, "score": 0.09090909090909091}
|
||||
{"id": "id12", "channel": "vec", "rank": 12, "score": 0.08333333333333333}
|
||||
{"id": "id13", "channel": "vec", "rank": 13, "score": 0.07692307692307693}
|
||||
{"id": "id14", "channel": "vec", "rank": 14, "score": 0.07142857142857142}
|
||||
{"id": "id15", "channel": "vec", "rank": 15, "score": 0.06666666666666667}
|
||||
{"id": "id16", "channel": "vec", "rank": 16, "score": 0.0625}
|
||||
{"id": "id17", "channel": "vec", "rank": 17, "score": 0.058823529411764705}
|
||||
{"id": "id18", "channel": "vec", "rank": 18, "score": 0.05555555555555555}
|
||||
{"id": "id19", "channel": "vec", "rank": 19, "score": 0.05263157894736842}
|
||||
{"id": "id20", "channel": "vec", "rank": 20, "score": 0.05}
|
||||
@@ -0,0 +1,3 @@
|
||||
{"id": "a", "channel": "vec", "rank": 9, "score": 0.0}
|
||||
{"id": "a", "channel": "vec", "rank": 1, "score": 0.0}
|
||||
{"id": "b", "channel": "vec", "rank": 2, "score": 0.0}
|
||||
@@ -0,0 +1 @@
|
||||
{"id": "only", "channel": "meta", "rank": 1, "score": 0.0}
|
||||
@@ -0,0 +1,16 @@
|
||||
{"id": "a", "channel": "vec", "rank": 1, "score": 1.0}
|
||||
{"id": "b", "channel": "vec", "rank": 2, "score": 0.5}
|
||||
{"id": "c", "channel": "vec", "rank": 3, "score": 0.3333333333333333}
|
||||
{"id": "d", "channel": "vec", "rank": 4, "score": 0.25}
|
||||
{"id": "e", "channel": "vec", "rank": 5, "score": 0.2}
|
||||
{"id": "f", "channel": "vec", "rank": 6, "score": 0.16666666666666666}
|
||||
{"id": "g", "channel": "vec", "rank": 7, "score": 0.14285714285714285}
|
||||
{"id": "h", "channel": "vec", "rank": 8, "score": 0.125}
|
||||
{"id": "c", "channel": "fts", "rank": 1, "score": 9.0}
|
||||
{"id": "a", "channel": "fts", "rank": 2, "score": 8.0}
|
||||
{"id": "x", "channel": "fts", "rank": 3, "score": 7.0}
|
||||
{"id": "y", "channel": "fts", "rank": 4, "score": 6.0}
|
||||
{"id": "b", "channel": "fts", "rank": 5, "score": 5.0}
|
||||
{"id": "z", "channel": "fts", "rank": 6, "score": 4.0}
|
||||
{"id": "y", "channel": "meta", "rank": 1, "score": 0.0}
|
||||
{"id": "a", "channel": "meta", "rank": 2, "score": 0.0}
|
||||
@@ -0,0 +1,10 @@
|
||||
{"id": "id0", "rank": 1, "score": 0.015384615384615385}
|
||||
{"id": "id1", "rank": 2, "score": 0.015384615384615385}
|
||||
{"id": "id2", "rank": 3, "score": 0.015384615384615385}
|
||||
{"id": "id3", "rank": 4, "score": 0.015384615384615385}
|
||||
{"id": "id4", "rank": 5, "score": 0.015384615384615385}
|
||||
{"id": "id5", "rank": 6, "score": 0.015384615384615385}
|
||||
{"id": "id6", "rank": 7, "score": 0.015384615384615385}
|
||||
{"id": "id7", "rank": 8, "score": 0.015384615384615385}
|
||||
{"id": "id8", "rank": 9, "score": 0.015384615384615385}
|
||||
{"id": "id9", "rank": 10, "score": 0.015384615384615385}
|
||||
@@ -0,0 +1,500 @@
|
||||
{"id": "id1", "rank": 1, "score": 0.018179156908665107}
|
||||
{"id": "id500", "rank": 2, "score": 0.018179156908665107}
|
||||
{"id": "id2", "rank": 3, "score": 0.01791794102371747}
|
||||
{"id": "id499", "rank": 4, "score": 0.01791794102371747}
|
||||
{"id": "id3", "rank": 5, "score": 0.017665130568356373}
|
||||
{"id": "id498", "rank": 6, "score": 0.017665130568356373}
|
||||
{"id": "id4", "rank": 7, "score": 0.01742033213644524}
|
||||
{"id": "id497", "rank": 8, "score": 0.01742033213644524}
|
||||
{"id": "id5", "rank": 9, "score": 0.01718317653569452}
|
||||
{"id": "id496", "rank": 10, "score": 0.01718317653569452}
|
||||
{"id": "id6", "rank": 11, "score": 0.016953316953316953}
|
||||
{"id": "id495", "rank": 12, "score": 0.016953316953316953}
|
||||
{"id": "id7", "rank": 13, "score": 0.016730427285952906}
|
||||
{"id": "id494", "rank": 14, "score": 0.016730427285952906}
|
||||
{"id": "id8", "rank": 15, "score": 0.016514200616955643}
|
||||
{"id": "id493", "rank": 16, "score": 0.016514200616955643}
|
||||
{"id": "id9", "rank": 17, "score": 0.016304347826086956}
|
||||
{"id": "id492", "rank": 18, "score": 0.016304347826086956}
|
||||
{"id": "id10", "rank": 19, "score": 0.016100596318382163}
|
||||
{"id": "id491", "rank": 20, "score": 0.016100596318382163}
|
||||
{"id": "id11", "rank": 21, "score": 0.01590268886043534}
|
||||
{"id": "id490", "rank": 22, "score": 0.01590268886043534}
|
||||
{"id": "id12", "rank": 23, "score": 0.015710382513661202}
|
||||
{"id": "id489", "rank": 24, "score": 0.015710382513661202}
|
||||
{"id": "id13", "rank": 25, "score": 0.015523447655234475}
|
||||
{"id": "id488", "rank": 26, "score": 0.015523447655234475}
|
||||
{"id": "id14", "rank": 27, "score": 0.015341667078412965}
|
||||
{"id": "id487", "rank": 28, "score": 0.015341667078412965}
|
||||
{"id": "id15", "rank": 29, "score": 0.015164835164835166}
|
||||
{"id": "id486", "rank": 30, "score": 0.015164835164835166}
|
||||
{"id": "id16", "rank": 31, "score": 0.014992757122163206}
|
||||
{"id": "id485", "rank": 32, "score": 0.014992757122163206}
|
||||
{"id": "id17", "rank": 33, "score": 0.014825248281130635}
|
||||
{"id": "id484", "rank": 34, "score": 0.014825248281130635}
|
||||
{"id": "id18", "rank": 35, "score": 0.014662133446663833}
|
||||
{"id": "id483", "rank": 36, "score": 0.014662133446663833}
|
||||
{"id": "id19", "rank": 37, "score": 0.014503246298285767}
|
||||
{"id": "id482", "rank": 38, "score": 0.014503246298285767}
|
||||
{"id": "id20", "rank": 39, "score": 0.014348428835489833}
|
||||
{"id": "id481", "rank": 40, "score": 0.014348428835489833}
|
||||
{"id": "id21", "rank": 41, "score": 0.01419753086419753}
|
||||
{"id": "id480", "rank": 42, "score": 0.01419753086419753}
|
||||
{"id": "id22", "rank": 43, "score": 0.014050409520792796}
|
||||
{"id": "id479", "rank": 44, "score": 0.014050409520792796}
|
||||
{"id": "id23", "rank": 45, "score": 0.013906928830563891}
|
||||
{"id": "id478", "rank": 46, "score": 0.013906928830563891}
|
||||
{"id": "id24", "rank": 47, "score": 0.013766959297685554}
|
||||
{"id": "id477", "rank": 48, "score": 0.013766959297685554}
|
||||
{"id": "id25", "rank": 49, "score": 0.013630377524143986}
|
||||
{"id": "id476", "rank": 50, "score": 0.013630377524143986}
|
||||
{"id": "id26", "rank": 51, "score": 0.013497065855248859}
|
||||
{"id": "id475", "rank": 52, "score": 0.013497065855248859}
|
||||
{"id": "id27", "rank": 53, "score": 0.01336691204959318}
|
||||
{"id": "id474", "rank": 54, "score": 0.01336691204959318}
|
||||
{"id": "id28", "rank": 55, "score": 0.01323980897151629}
|
||||
{"id": "id473", "rank": 56, "score": 0.01323980897151629}
|
||||
{"id": "id29", "rank": 57, "score": 0.013115654304300076}
|
||||
{"id": "id472", "rank": 58, "score": 0.013115654304300076}
|
||||
{"id": "id30", "rank": 59, "score": 0.012994350282485877}
|
||||
{"id": "id471", "rank": 60, "score": 0.012994350282485877}
|
||||
{"id": "id31", "rank": 61, "score": 0.012875803441841178}
|
||||
{"id": "id470", "rank": 62, "score": 0.012875803441841178}
|
||||
{"id": "id32", "rank": 63, "score": 0.01275992438563327}
|
||||
{"id": "id469", "rank": 64, "score": 0.01275992438563327}
|
||||
{"id": "id33", "rank": 65, "score": 0.012646627565982406}
|
||||
{"id": "id468", "rank": 66, "score": 0.012646627565982406}
|
||||
{"id": "id34", "rank": 67, "score": 0.012535831079171545}
|
||||
{"id": "id467", "rank": 68, "score": 0.012535831079171545}
|
||||
{"id": "id35", "rank": 69, "score": 0.012427456473884331}
|
||||
{"id": "id466", "rank": 70, "score": 0.012427456473884331}
|
||||
{"id": "id36", "rank": 71, "score": 0.01232142857142857}
|
||||
{"id": "id465", "rank": 72, "score": 0.01232142857142857}
|
||||
{"id": "id37", "rank": 73, "score": 0.012217675297080349}
|
||||
{"id": "id464", "rank": 74, "score": 0.012217675297080349}
|
||||
{"id": "id38", "rank": 75, "score": 0.0121161275217544}
|
||||
{"id": "id463", "rank": 76, "score": 0.0121161275217544}
|
||||
{"id": "id39", "rank": 77, "score": 0.012016718913270639}
|
||||
{"id": "id462", "rank": 78, "score": 0.012016718913270639}
|
||||
{"id": "id40", "rank": 79, "score": 0.011919385796545105}
|
||||
{"id": "id461", "rank": 80, "score": 0.011919385796545105}
|
||||
{"id": "id41", "rank": 81, "score": 0.011824067022086824}
|
||||
{"id": "id460", "rank": 82, "score": 0.011824067022086824}
|
||||
{"id": "id42", "rank": 83, "score": 0.011730703842230534}
|
||||
{"id": "id459", "rank": 84, "score": 0.011730703842230534}
|
||||
{"id": "id43", "rank": 85, "score": 0.0116392397945796}
|
||||
{"id": "id458", "rank": 86, "score": 0.0116392397945796}
|
||||
{"id": "id44", "rank": 87, "score": 0.011549620592173784}
|
||||
{"id": "id457", "rank": 88, "score": 0.011549620592173784}
|
||||
{"id": "id45", "rank": 89, "score": 0.011461794019933556}
|
||||
{"id": "id456", "rank": 90, "score": 0.011461794019933556}
|
||||
{"id": "id46", "rank": 91, "score": 0.011375709836966477}
|
||||
{"id": "id455", "rank": 92, "score": 0.011375709836966477}
|
||||
{"id": "id47", "rank": 93, "score": 0.011291319684352158}
|
||||
{"id": "id454", "rank": 94, "score": 0.011291319684352158}
|
||||
{"id": "id48", "rank": 95, "score": 0.011208576998050682}
|
||||
{"id": "id453", "rank": 96, "score": 0.011208576998050682}
|
||||
{"id": "id49", "rank": 97, "score": 0.011127436926605505}
|
||||
{"id": "id452", "rank": 98, "score": 0.011127436926605505}
|
||||
{"id": "id50", "rank": 99, "score": 0.011047856253335705}
|
||||
{"id": "id451", "rank": 100, "score": 0.011047856253335705}
|
||||
{"id": "id51", "rank": 101, "score": 0.010969793322734499}
|
||||
{"id": "id450", "rank": 102, "score": 0.010969793322734499}
|
||||
{"id": "id52", "rank": 103, "score": 0.010893207970811114}
|
||||
{"id": "id449", "rank": 104, "score": 0.010893207970811114}
|
||||
{"id": "id53", "rank": 105, "score": 0.010818061459131768}
|
||||
{"id": "id448", "rank": 106, "score": 0.010818061459131768}
|
||||
{"id": "id54", "rank": 107, "score": 0.010744316412332606}
|
||||
{"id": "id447", "rank": 108, "score": 0.010744316412332606}
|
||||
{"id": "id55", "rank": 109, "score": 0.010671936758893281}
|
||||
{"id": "id446", "rank": 110, "score": 0.010671936758893281}
|
||||
{"id": "id56", "rank": 111, "score": 0.010600887674974394}
|
||||
{"id": "id445", "rank": 112, "score": 0.010600887674974394}
|
||||
{"id": "id57", "rank": 113, "score": 0.010531135531135532}
|
||||
{"id": "id444", "rank": 114, "score": 0.010531135531135532}
|
||||
{"id": "id58", "rank": 115, "score": 0.01046264784176298}
|
||||
{"id": "id443", "rank": 116, "score": 0.01046264784176298}
|
||||
{"id": "id59", "rank": 117, "score": 0.010395393217047775}
|
||||
{"id": "id442", "rank": 118, "score": 0.010395393217047775}
|
||||
{"id": "id60", "rank": 119, "score": 0.010329341317365268}
|
||||
{"id": "id441", "rank": 120, "score": 0.010329341317365268}
|
||||
{"id": "id61", "rank": 121, "score": 0.010264462809917356}
|
||||
{"id": "id440", "rank": 122, "score": 0.010264462809917356}
|
||||
{"id": "id62", "rank": 123, "score": 0.010200729327507475}
|
||||
{"id": "id439", "rank": 124, "score": 0.010200729327507475}
|
||||
{"id": "id63", "rank": 125, "score": 0.010138113429327064}
|
||||
{"id": "id438", "rank": 126, "score": 0.010138113429327064}
|
||||
{"id": "id64", "rank": 127, "score": 0.010076588563639903}
|
||||
{"id": "id437", "rank": 128, "score": 0.010076588563639903}
|
||||
{"id": "id65", "rank": 129, "score": 0.010016129032258065}
|
||||
{"id": "id436", "rank": 130, "score": 0.010016129032258065}
|
||||
{"id": "id66", "rank": 131, "score": 0.009956709956709955}
|
||||
{"id": "id435", "rank": 132, "score": 0.009956709956709955}
|
||||
{"id": "id67", "rank": 133, "score": 0.009898307246007204}
|
||||
{"id": "id434", "rank": 134, "score": 0.009898307246007204}
|
||||
{"id": "id68", "rank": 135, "score": 0.00984089756592292}
|
||||
{"id": "id433", "rank": 136, "score": 0.00984089756592292}
|
||||
{"id": "id69", "rank": 137, "score": 0.009784458309699377}
|
||||
{"id": "id432", "rank": 138, "score": 0.009784458309699377}
|
||||
{"id": "id70", "rank": 139, "score": 0.0097289675701081}
|
||||
{"id": "id431", "rank": 140, "score": 0.0097289675701081}
|
||||
{"id": "id71", "rank": 141, "score": 0.009674404112790153}
|
||||
{"id": "id430", "rank": 142, "score": 0.009674404112790153}
|
||||
{"id": "id72", "rank": 143, "score": 0.0096207473508087}
|
||||
{"id": "id429", "rank": 144, "score": 0.0096207473508087}
|
||||
{"id": "id73", "rank": 145, "score": 0.009567977320350056}
|
||||
{"id": "id428", "rank": 146, "score": 0.009567977320350056}
|
||||
{"id": "id74", "rank": 147, "score": 0.009516074657513255}
|
||||
{"id": "id427", "rank": 148, "score": 0.009516074657513255}
|
||||
{"id": "id75", "rank": 149, "score": 0.009465020576131689}
|
||||
{"id": "id426", "rank": 150, "score": 0.009465020576131689}
|
||||
{"id": "id76", "rank": 151, "score": 0.00941479684657368}
|
||||
{"id": "id425", "rank": 152, "score": 0.00941479684657368}
|
||||
{"id": "id77", "rank": 153, "score": 0.00936538577547204}
|
||||
{"id": "id424", "rank": 154, "score": 0.00936538577547204}
|
||||
{"id": "id78", "rank": 155, "score": 0.009316770186335404}
|
||||
{"id": "id423", "rank": 156, "score": 0.009316770186335404}
|
||||
{"id": "id79", "rank": 157, "score": 0.009268933400997045}
|
||||
{"id": "id422", "rank": 158, "score": 0.009268933400997045}
|
||||
{"id": "id80", "rank": 159, "score": 0.009221859221859221}
|
||||
{"id": "id421", "rank": 160, "score": 0.009221859221859221}
|
||||
{"id": "id81", "rank": 161, "score": 0.009175531914893616}
|
||||
{"id": "id420", "rank": 162, "score": 0.009175531914893616}
|
||||
{"id": "id82", "rank": 163, "score": 0.009129936193360581}
|
||||
{"id": "id419", "rank": 164, "score": 0.009129936193360581}
|
||||
{"id": "id83", "rank": 165, "score": 0.009085057202212013}
|
||||
{"id": "id418", "rank": 166, "score": 0.009085057202212013}
|
||||
{"id": "id84", "rank": 167, "score": 0.009040880503144654}
|
||||
{"id": "id417", "rank": 168, "score": 0.009040880503144654}
|
||||
{"id": "id85", "rank": 169, "score": 0.008997392060272384}
|
||||
{"id": "id416", "rank": 170, "score": 0.008997392060272384}
|
||||
{"id": "id86", "rank": 171, "score": 0.008954578226387887}
|
||||
{"id": "id415", "rank": 172, "score": 0.008954578226387887}
|
||||
{"id": "id87", "rank": 173, "score": 0.008912425729785584}
|
||||
{"id": "id414", "rank": 174, "score": 0.008912425729785584}
|
||||
{"id": "id88", "rank": 175, "score": 0.008870921661619336}
|
||||
{"id": "id413", "rank": 176, "score": 0.008870921661619336}
|
||||
{"id": "id89", "rank": 177, "score": 0.008830053463769765}
|
||||
{"id": "id412", "rank": 178, "score": 0.008830053463769765}
|
||||
{"id": "id90", "rank": 179, "score": 0.008789808917197453}
|
||||
{"id": "id411", "rank": 180, "score": 0.008789808917197453}
|
||||
{"id": "id91", "rank": 181, "score": 0.008750176130759476}
|
||||
{"id": "id410", "rank": 182, "score": 0.008750176130759476}
|
||||
{"id": "id92", "rank": 183, "score": 0.00871114353046796}
|
||||
{"id": "id409", "rank": 184, "score": 0.00871114353046796}
|
||||
{"id": "id93", "rank": 185, "score": 0.008672699849170438}
|
||||
{"id": "id408", "rank": 186, "score": 0.008672699849170438}
|
||||
{"id": "id94", "rank": 187, "score": 0.008634834116632833}
|
||||
{"id": "id407", "rank": 188, "score": 0.008634834116632833}
|
||||
{"id": "id95", "rank": 189, "score": 0.008597535650006923}
|
||||
{"id": "id406", "rank": 190, "score": 0.008597535650006923}
|
||||
{"id": "id96", "rank": 191, "score": 0.008560794044665011}
|
||||
{"id": "id405", "rank": 192, "score": 0.008560794044665011}
|
||||
{"id": "id97", "rank": 193, "score": 0.00852459916538546}
|
||||
{"id": "id404", "rank": 194, "score": 0.00852459916538546}
|
||||
{"id": "id98", "rank": 195, "score": 0.008488941137873527}
|
||||
{"id": "id403", "rank": 196, "score": 0.008488941137873527}
|
||||
{"id": "id99", "rank": 197, "score": 0.008453810340602793}
|
||||
{"id": "id402", "rank": 198, "score": 0.008453810340602793}
|
||||
{"id": "id100", "rank": 199, "score": 0.008419197396963124}
|
||||
{"id": "id401", "rank": 200, "score": 0.008419197396963124}
|
||||
{"id": "id101", "rank": 201, "score": 0.008385093167701863}
|
||||
{"id": "id400", "rank": 202, "score": 0.008385093167701863}
|
||||
{"id": "id102", "rank": 203, "score": 0.008351488743645606}
|
||||
{"id": "id399", "rank": 204, "score": 0.008351488743645606}
|
||||
{"id": "id103", "rank": 205, "score": 0.008318375438690493}
|
||||
{"id": "id398", "rank": 206, "score": 0.008318375438690493}
|
||||
{"id": "id104", "rank": 207, "score": 0.008285744783049581}
|
||||
{"id": "id397", "rank": 208, "score": 0.008285744783049581}
|
||||
{"id": "id105", "rank": 209, "score": 0.008253588516746412}
|
||||
{"id": "id396", "rank": 210, "score": 0.008253588516746412}
|
||||
{"id": "id106", "rank": 211, "score": 0.008221898583344368}
|
||||
{"id": "id395", "rank": 212, "score": 0.008221898583344368}
|
||||
{"id": "id107", "rank": 213, "score": 0.008190667123901976}
|
||||
{"id": "id394", "rank": 214, "score": 0.008190667123901976}
|
||||
{"id": "id108", "rank": 215, "score": 0.00815988647114475}
|
||||
{"id": "id393", "rank": 216, "score": 0.00815988647114475}
|
||||
{"id": "id109", "rank": 217, "score": 0.008129549143844582}
|
||||
{"id": "id392", "rank": 218, "score": 0.008129549143844582}
|
||||
{"id": "id110", "rank": 219, "score": 0.008099647841398199}
|
||||
{"id": "id391", "rank": 220, "score": 0.008099647841398199}
|
||||
{"id": "id111", "rank": 221, "score": 0.008070175438596491}
|
||||
{"id": "id390", "rank": 222, "score": 0.008070175438596491}
|
||||
{"id": "id112", "rank": 223, "score": 0.008041124980576993}
|
||||
{"id": "id389", "rank": 224, "score": 0.008041124980576993}
|
||||
{"id": "id113", "rank": 225, "score": 0.008012489677952105}
|
||||
{"id": "id388", "rank": 226, "score": 0.008012489677952105}
|
||||
{"id": "id114", "rank": 227, "score": 0.007984262902105993}
|
||||
{"id": "id387", "rank": 228, "score": 0.007984262902105993}
|
||||
{"id": "id115", "rank": 229, "score": 0.007956438180653428}
|
||||
{"id": "id386", "rank": 230, "score": 0.007956438180653428}
|
||||
{"id": "id116", "rank": 231, "score": 0.007929009193054138}
|
||||
{"id": "id385", "rank": 232, "score": 0.007929009193054138}
|
||||
{"id": "id117", "rank": 233, "score": 0.007901969766376546}
|
||||
{"id": "id384", "rank": 234, "score": 0.007901969766376546}
|
||||
{"id": "id118", "rank": 235, "score": 0.007875313871205011}
|
||||
{"id": "id383", "rank": 236, "score": 0.007875313871205011}
|
||||
{"id": "id119", "rank": 237, "score": 0.007849035617684977}
|
||||
{"id": "id382", "rank": 238, "score": 0.007849035617684977}
|
||||
{"id": "id120", "rank": 239, "score": 0.007823129251700681}
|
||||
{"id": "id381", "rank": 240, "score": 0.007823129251700681}
|
||||
{"id": "id121", "rank": 241, "score": 0.007797589151180311}
|
||||
{"id": "id380", "rank": 242, "score": 0.007797589151180311}
|
||||
{"id": "id122", "rank": 243, "score": 0.0077724098225237185}
|
||||
{"id": "id379", "rank": 244, "score": 0.0077724098225237185}
|
||||
{"id": "id123", "rank": 245, "score": 0.00774758589714799}
|
||||
{"id": "id378", "rank": 246, "score": 0.00774758589714799}
|
||||
{"id": "id124", "rank": 247, "score": 0.0077231121281464525}
|
||||
{"id": "id377", "rank": 248, "score": 0.0077231121281464525}
|
||||
{"id": "id125", "rank": 249, "score": 0.007698983387056782}
|
||||
{"id": "id376", "rank": 250, "score": 0.007698983387056782}
|
||||
{"id": "id126", "rank": 251, "score": 0.0076751946607341495}
|
||||
{"id": "id375", "rank": 252, "score": 0.0076751946607341495}
|
||||
{"id": "id127", "rank": 253, "score": 0.007651741048325488}
|
||||
{"id": "id374", "rank": 254, "score": 0.007651741048325488}
|
||||
{"id": "id128", "rank": 255, "score": 0.007628617758341113}
|
||||
{"id": "id373", "rank": 256, "score": 0.007628617758341113}
|
||||
{"id": "id129", "rank": 257, "score": 0.007605820105820105}
|
||||
{"id": "id372", "rank": 258, "score": 0.007605820105820105}
|
||||
{"id": "id130", "rank": 259, "score": 0.00758334350958603}
|
||||
{"id": "id371", "rank": 260, "score": 0.00758334350958603}
|
||||
{"id": "id131", "rank": 261, "score": 0.007561183489589675}
|
||||
{"id": "id370", "rank": 262, "score": 0.007561183489589675}
|
||||
{"id": "id132", "rank": 263, "score": 0.007539335664335664}
|
||||
{"id": "id369", "rank": 264, "score": 0.007539335664335664}
|
||||
{"id": "id133", "rank": 265, "score": 0.007517795748389908}
|
||||
{"id": "id368", "rank": 266, "score": 0.007517795748389908}
|
||||
{"id": "id134", "rank": 267, "score": 0.007496559549964992}
|
||||
{"id": "id367", "rank": 268, "score": 0.007496559549964992}
|
||||
{"id": "id135", "rank": 269, "score": 0.007475622968580715}
|
||||
{"id": "id366", "rank": 270, "score": 0.007475622968580715}
|
||||
{"id": "id136", "rank": 271, "score": 0.007454981992797118}
|
||||
{"id": "id365", "rank": 272, "score": 0.007454981992797118}
|
||||
{"id": "id137", "rank": 273, "score": 0.00743463269801743}
|
||||
{"id": "id364", "rank": 274, "score": 0.00743463269801743}
|
||||
{"id": "id138", "rank": 275, "score": 0.007414571244358479}
|
||||
{"id": "id363", "rank": 276, "score": 0.007414571244358479}
|
||||
{"id": "id139", "rank": 277, "score": 0.007394793874586201}
|
||||
{"id": "id362", "rank": 278, "score": 0.007394793874586201}
|
||||
{"id": "id140", "rank": 279, "score": 0.0073752969121140145}
|
||||
{"id": "id361", "rank": 280, "score": 0.0073752969121140145}
|
||||
{"id": "id141", "rank": 281, "score": 0.007356076759061834}
|
||||
{"id": "id360", "rank": 282, "score": 0.007356076759061834}
|
||||
{"id": "id142", "rank": 283, "score": 0.007337129894373686}
|
||||
{"id": "id359", "rank": 284, "score": 0.007337129894373686}
|
||||
{"id": "id143", "rank": 285, "score": 0.0073184528719918915}
|
||||
{"id": "id358", "rank": 286, "score": 0.0073184528719918915}
|
||||
{"id": "id144", "rank": 287, "score": 0.0073000423190859075}
|
||||
{"id": "id357", "rank": 288, "score": 0.0073000423190859075}
|
||||
{"id": "id145", "rank": 289, "score": 0.007281894934333959}
|
||||
{"id": "id356", "rank": 290, "score": 0.007281894934333959}
|
||||
{"id": "id146", "rank": 291, "score": 0.007264007486255702}
|
||||
{"id": "id355", "rank": 292, "score": 0.007264007486255702}
|
||||
{"id": "id147", "rank": 293, "score": 0.007246376811594202}
|
||||
{"id": "id354", "rank": 294, "score": 0.007246376811594202}
|
||||
{"id": "id148", "rank": 295, "score": 0.007228999813745577}
|
||||
{"id": "id353", "rank": 296, "score": 0.007228999813745577}
|
||||
{"id": "id149", "rank": 297, "score": 0.007211873461234728}
|
||||
{"id": "id352", "rank": 298, "score": 0.007211873461234728}
|
||||
{"id": "id150", "rank": 299, "score": 0.007194994786235663}
|
||||
{"id": "id351", "rank": 300, "score": 0.007194994786235663}
|
||||
{"id": "id151", "rank": 301, "score": 0.0071783608831348985}
|
||||
{"id": "id350", "rank": 302, "score": 0.0071783608831348985}
|
||||
{"id": "id152", "rank": 303, "score": 0.007161968907136596}
|
||||
{"id": "id349", "rank": 304, "score": 0.007161968907136596}
|
||||
{"id": "id153", "rank": 305, "score": 0.0071458160729080365}
|
||||
{"id": "id348", "rank": 306, "score": 0.0071458160729080365}
|
||||
{"id": "id154", "rank": 307, "score": 0.007129899653264139}
|
||||
{"id": "id347", "rank": 308, "score": 0.007129899653264139}
|
||||
{"id": "id155", "rank": 309, "score": 0.007114216977889793}
|
||||
{"id": "id346", "rank": 310, "score": 0.007114216977889793}
|
||||
{"id": "id156", "rank": 311, "score": 0.007098765432098765}
|
||||
{"id": "id345", "rank": 312, "score": 0.007098765432098765}
|
||||
{"id": "id157", "rank": 313, "score": 0.007083542455628051}
|
||||
{"id": "id344", "rank": 314, "score": 0.007083542455628051}
|
||||
{"id": "id158", "rank": 315, "score": 0.007068545541466524}
|
||||
{"id": "id343", "rank": 316, "score": 0.007068545541466524}
|
||||
{"id": "id159", "rank": 317, "score": 0.0070537722347168265}
|
||||
{"id": "id342", "rank": 318, "score": 0.0070537722347168265}
|
||||
{"id": "id160", "rank": 319, "score": 0.007039220131489458}
|
||||
{"id": "id341", "rank": 320, "score": 0.007039220131489458}
|
||||
{"id": "id161", "rank": 321, "score": 0.007024886877828054}
|
||||
{"id": "id340", "rank": 322, "score": 0.007024886877828054}
|
||||
{"id": "id162", "rank": 323, "score": 0.007010770168664905}
|
||||
{"id": "id339", "rank": 324, "score": 0.007010770168664905}
|
||||
{"id": "id163", "rank": 325, "score": 0.006996867746805777}
|
||||
{"id": "id338", "rank": 326, "score": 0.006996867746805777}
|
||||
{"id": "id164", "rank": 327, "score": 0.006983177401943145}
|
||||
{"id": "id337", "rank": 328, "score": 0.006983177401943145}
|
||||
{"id": "id165", "rank": 329, "score": 0.0069696969696969695}
|
||||
{"id": "id336", "rank": 330, "score": 0.0069696969696969695}
|
||||
{"id": "id166", "rank": 331, "score": 0.0069564243306822}
|
||||
{"id": "id335", "rank": 332, "score": 0.0069564243306822}
|
||||
{"id": "id167", "rank": 333, "score": 0.006943357409602183}
|
||||
{"id": "id334", "rank": 334, "score": 0.006943357409602183}
|
||||
{"id": "id168", "rank": 335, "score": 0.006930494174367216}
|
||||
{"id": "id333", "rank": 336, "score": 0.006930494174367216}
|
||||
{"id": "id169", "rank": 337, "score": 0.0069178326352375}
|
||||
{"id": "id332", "rank": 338, "score": 0.0069178326352375}
|
||||
{"id": "id170", "rank": 339, "score": 0.00690537084398977}
|
||||
{"id": "id331", "rank": 340, "score": 0.00690537084398977}
|
||||
{"id": "id171", "rank": 341, "score": 0.0068931068931068935}
|
||||
{"id": "id330", "rank": 342, "score": 0.0068931068931068935}
|
||||
{"id": "id172", "rank": 343, "score": 0.006881038914989805}
|
||||
{"id": "id329", "rank": 344, "score": 0.006881038914989805}
|
||||
{"id": "id173", "rank": 345, "score": 0.006869165081191097}
|
||||
{"id": "id328", "rank": 346, "score": 0.006869165081191097}
|
||||
{"id": "id174", "rank": 347, "score": 0.006857483601669649}
|
||||
{"id": "id327", "rank": 348, "score": 0.006857483601669649}
|
||||
{"id": "id175", "rank": 349, "score": 0.006845992724065704}
|
||||
{"id": "id326", "rank": 350, "score": 0.006845992724065704}
|
||||
{"id": "id176", "rank": 351, "score": 0.006834690732995818}
|
||||
{"id": "id325", "rank": 352, "score": 0.006834690732995818}
|
||||
{"id": "id177", "rank": 353, "score": 0.0068235759493670885}
|
||||
{"id": "id324", "rank": 354, "score": 0.0068235759493670885}
|
||||
{"id": "id178", "rank": 355, "score": 0.006812646729710161}
|
||||
{"id": "id323", "rank": 356, "score": 0.006812646729710161}
|
||||
{"id": "id179", "rank": 357, "score": 0.0068019014655304605}
|
||||
{"id": "id322", "rank": 358, "score": 0.0068019014655304605}
|
||||
{"id": "id180", "rank": 359, "score": 0.006791338582677165}
|
||||
{"id": "id321", "rank": 360, "score": 0.006791338582677165}
|
||||
{"id": "id181", "rank": 361, "score": 0.006780956540729417}
|
||||
{"id": "id320", "rank": 362, "score": 0.006780956540729417}
|
||||
{"id": "id182", "rank": 363, "score": 0.006770753832399311}
|
||||
{"id": "id319", "rank": 364, "score": 0.006770753832399311}
|
||||
{"id": "id183", "rank": 365, "score": 0.006760728982951205}
|
||||
{"id": "id318", "rank": 366, "score": 0.006760728982951205}
|
||||
{"id": "id184", "rank": 367, "score": 0.00675088054963691}
|
||||
{"id": "id317", "rank": 368, "score": 0.00675088054963691}
|
||||
{"id": "id185", "rank": 369, "score": 0.006741207121146332}
|
||||
{"id": "id316", "rank": 370, "score": 0.006741207121146332}
|
||||
{"id": "id186", "rank": 371, "score": 0.0067317073170731715}
|
||||
{"id": "id315", "rank": 372, "score": 0.0067317073170731715}
|
||||
{"id": "id187", "rank": 373, "score": 0.006722379787395268}
|
||||
{"id": "id314", "rank": 374, "score": 0.006722379787395268}
|
||||
{"id": "id188", "rank": 375, "score": 0.006713223211969212}
|
||||
{"id": "id313", "rank": 376, "score": 0.006713223211969212}
|
||||
{"id": "id189", "rank": 377, "score": 0.006704236300038865}
|
||||
{"id": "id312", "rank": 378, "score": 0.006704236300038865}
|
||||
{"id": "id190", "rank": 379, "score": 0.006695417789757413}
|
||||
{"id": "id311", "rank": 380, "score": 0.006695417789757413}
|
||||
{"id": "id191", "rank": 381, "score": 0.006686766447722623}
|
||||
{"id": "id310", "rank": 382, "score": 0.006686766447722623}
|
||||
{"id": "id192", "rank": 383, "score": 0.006678281068524971}
|
||||
{"id": "id309", "rank": 384, "score": 0.006678281068524971}
|
||||
{"id": "id193", "rank": 385, "score": 0.0066699604743083}
|
||||
{"id": "id308", "rank": 386, "score": 0.0066699604743083}
|
||||
{"id": "id194", "rank": 387, "score": 0.006661803514342724}
|
||||
{"id": "id307", "rank": 388, "score": 0.006661803514342724}
|
||||
{"id": "id195", "rank": 389, "score": 0.00665380906460945}
|
||||
{"id": "id306", "rank": 390, "score": 0.00665380906460945}
|
||||
{"id": "id196", "rank": 391, "score": 0.00664597602739726}
|
||||
{"id": "id305", "rank": 392, "score": 0.00664597602739726}
|
||||
{"id": "id197", "rank": 393, "score": 0.0066383033309103355}
|
||||
{"id": "id304", "rank": 394, "score": 0.0066383033309103355}
|
||||
{"id": "id198", "rank": 395, "score": 0.0066307899288871805}
|
||||
{"id": "id303", "rank": 396, "score": 0.0066307899288871805}
|
||||
{"id": "id199", "rank": 397, "score": 0.00662343480023038}
|
||||
{"id": "id302", "rank": 398, "score": 0.00662343480023038}
|
||||
{"id": "id200", "rank": 399, "score": 0.006616236948646921}
|
||||
{"id": "id301", "rank": 400, "score": 0.006616236948646921}
|
||||
{"id": "id201", "rank": 401, "score": 0.0066091954022988505}
|
||||
{"id": "id300", "rank": 402, "score": 0.0066091954022988505}
|
||||
{"id": "id202", "rank": 403, "score": 0.006602309213464033}
|
||||
{"id": "id299", "rank": 404, "score": 0.006602309213464033}
|
||||
{"id": "id203", "rank": 405, "score": 0.006595577458206768}
|
||||
{"id": "id298", "rank": 406, "score": 0.006595577458206768}
|
||||
{"id": "id204", "rank": 407, "score": 0.00658899923605806}
|
||||
{"id": "id297", "rank": 408, "score": 0.00658899923605806}
|
||||
{"id": "id205", "rank": 409, "score": 0.006582573669705321}
|
||||
{"id": "id296", "rank": 410, "score": 0.006582573669705321}
|
||||
{"id": "id206", "rank": 411, "score": 0.006576299904691305}
|
||||
{"id": "id295", "rank": 412, "score": 0.006576299904691305}
|
||||
{"id": "id207", "rank": 413, "score": 0.0065701771091220725}
|
||||
{"id": "id294", "rank": 414, "score": 0.0065701771091220725}
|
||||
{"id": "id208", "rank": 415, "score": 0.006564204473383789}
|
||||
{"id": "id293", "rank": 416, "score": 0.006564204473383789}
|
||||
{"id": "id209", "rank": 417, "score": 0.006558381209868199}
|
||||
{"id": "id292", "rank": 418, "score": 0.006558381209868199}
|
||||
{"id": "id210", "rank": 419, "score": 0.0065527065527065526}
|
||||
{"id": "id291", "rank": 420, "score": 0.0065527065527065526}
|
||||
{"id": "id211", "rank": 421, "score": 0.006547179757511861}
|
||||
{"id": "id290", "rank": 422, "score": 0.006547179757511861}
|
||||
{"id": "id212", "rank": 423, "score": 0.006541800101129277}
|
||||
{"id": "id289", "rank": 424, "score": 0.006541800101129277}
|
||||
{"id": "id213", "rank": 425, "score": 0.0065365668813944676}
|
||||
{"id": "id288", "rank": 426, "score": 0.0065365668813944676}
|
||||
{"id": "id214", "rank": 427, "score": 0.006531479416899808}
|
||||
{"id": "id287", "rank": 428, "score": 0.006531479416899808}
|
||||
{"id": "id215", "rank": 429, "score": 0.00652653704676826}
|
||||
{"id": "id286", "rank": 430, "score": 0.00652653704676826}
|
||||
{"id": "id216", "rank": 431, "score": 0.006521739130434783}
|
||||
{"id": "id285", "rank": 432, "score": 0.006521739130434783}
|
||||
{"id": "id217", "rank": 433, "score": 0.006517085047435144}
|
||||
{"id": "id284", "rank": 434, "score": 0.006517085047435144}
|
||||
{"id": "id218", "rank": 435, "score": 0.006512574197202005}
|
||||
{"id": "id283", "rank": 436, "score": 0.006512574197202005}
|
||||
{"id": "id219", "rank": 437, "score": 0.006508205998868138}
|
||||
{"id": "id282", "rank": 438, "score": 0.006508205998868138}
|
||||
{"id": "id220", "rank": 439, "score": 0.006503979891076666}
|
||||
{"id": "id281", "rank": 440, "score": 0.006503979891076666}
|
||||
{"id": "id221", "rank": 441, "score": 0.0064998953317981995}
|
||||
{"id": "id280", "rank": 442, "score": 0.0064998953317981995}
|
||||
{"id": "id222", "rank": 443, "score": 0.006495951798154773}
|
||||
{"id": "id279", "rank": 444, "score": 0.006495951798154773}
|
||||
{"id": "id223", "rank": 445, "score": 0.006492148786250445}
|
||||
{"id": "id278", "rank": 446, "score": 0.006492148786250445}
|
||||
{"id": "id224", "rank": 447, "score": 0.006488485811008485}
|
||||
{"id": "id277", "rank": 448, "score": 0.006488485811008485}
|
||||
{"id": "id225", "rank": 449, "score": 0.006484962406015038}
|
||||
{"id": "id276", "rank": 450, "score": 0.006484962406015038}
|
||||
{"id": "id226", "rank": 451, "score": 0.006481578123369168}
|
||||
{"id": "id275", "rank": 452, "score": 0.006481578123369168}
|
||||
{"id": "id227", "rank": 453, "score": 0.006478332533539194}
|
||||
{"id": "id274", "rank": 454, "score": 0.006478332533539194}
|
||||
{"id": "id228", "rank": 455, "score": 0.006475225225225225}
|
||||
{"id": "id273", "rank": 456, "score": 0.006475225225225225}
|
||||
{"id": "id229", "rank": 457, "score": 0.006472255805227832}
|
||||
{"id": "id272", "rank": 458, "score": 0.006472255805227832}
|
||||
{"id": "id230", "rank": 459, "score": 0.006469423898322742}
|
||||
{"id": "id271", "rank": 460, "score": 0.006469423898322742}
|
||||
{"id": "id231", "rank": 461, "score": 0.006466729147141519}
|
||||
{"id": "id270", "rank": 462, "score": 0.006466729147141519}
|
||||
{"id": "id232", "rank": 463, "score": 0.006464171212058126}
|
||||
{"id": "id269", "rank": 464, "score": 0.006464171212058126}
|
||||
{"id": "id233", "rank": 465, "score": 0.006461749771081328}
|
||||
{"id": "id268", "rank": 466, "score": 0.006461749771081328}
|
||||
{"id": "id234", "rank": 467, "score": 0.006459464519752855}
|
||||
{"id": "id267", "rank": 468, "score": 0.006459464519752855}
|
||||
{"id": "id235", "rank": 469, "score": 0.006457315171051263}
|
||||
{"id": "id266", "rank": 470, "score": 0.006457315171051263}
|
||||
{"id": "id236", "rank": 471, "score": 0.0064553014553014555}
|
||||
{"id": "id265", "rank": 472, "score": 0.0064553014553014555}
|
||||
{"id": "id237", "rank": 473, "score": 0.006453423120089787}
|
||||
{"id": "id264", "rank": 474, "score": 0.006453423120089787}
|
||||
{"id": "id238", "rank": 475, "score": 0.006451679930184719}
|
||||
{"id": "id263", "rank": 476, "score": 0.006451679930184719}
|
||||
{"id": "id239", "rank": 477, "score": 0.006450071667462972}
|
||||
{"id": "id262", "rank": 478, "score": 0.006450071667462972}
|
||||
{"id": "id240", "rank": 479, "score": 0.006448598130841122}
|
||||
{"id": "id261", "rank": 480, "score": 0.006448598130841122}
|
||||
{"id": "id241", "rank": 481, "score": 0.006447259136212625}
|
||||
{"id": "id260", "rank": 482, "score": 0.006447259136212625}
|
||||
{"id": "id242", "rank": 483, "score": 0.00644605451639021}
|
||||
{"id": "id259", "rank": 484, "score": 0.00644605451639021}
|
||||
{"id": "id243", "rank": 485, "score": 0.006444984121053615}
|
||||
{"id": "id258", "rank": 486, "score": 0.006444984121053615}
|
||||
{"id": "id244", "rank": 487, "score": 0.006444047816702639}
|
||||
{"id": "id257", "rank": 488, "score": 0.006444047816702639}
|
||||
{"id": "id245", "rank": 489, "score": 0.006443245486615481}
|
||||
{"id": "id256", "rank": 490, "score": 0.006443245486615481}
|
||||
{"id": "id246", "rank": 491, "score": 0.006442577030812325}
|
||||
{"id": "id255", "rank": 492, "score": 0.006442577030812325}
|
||||
{"id": "id247", "rank": 493, "score": 0.006442042366024192}
|
||||
{"id": "id254", "rank": 494, "score": 0.006442042366024192}
|
||||
{"id": "id248", "rank": 495, "score": 0.006441641425666985}
|
||||
{"id": "id253", "rank": 496, "score": 0.006441641425666985}
|
||||
{"id": "id249", "rank": 497, "score": 0.006441374159820762}
|
||||
{"id": "id252", "rank": 498, "score": 0.006441374159820762}
|
||||
{"id": "id250", "rank": 499, "score": 0.00644124053521419}
|
||||
{"id": "id251", "rank": 500, "score": 0.00644124053521419}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"id": "id5", "rank": 1, "score": 0.03177805800756621}
|
||||
{"id": "id1", "rank": 2, "score": 0.03177805800756621}
|
||||
{"id": "id2", "rank": 3, "score": 0.031754032258064516}
|
||||
{"id": "id4", "rank": 4, "score": 0.031754032258064516}
|
||||
{"id": "id3", "rank": 5, "score": 0.031746031746031744}
|
||||
@@ -0,0 +1,20 @@
|
||||
{"id": "id1", "rank": 1, "score": 0.01639344262295082}
|
||||
{"id": "id2", "rank": 2, "score": 0.016129032258064516}
|
||||
{"id": "id3", "rank": 3, "score": 0.015873015873015872}
|
||||
{"id": "id4", "rank": 4, "score": 0.015625}
|
||||
{"id": "id5", "rank": 5, "score": 0.015384615384615385}
|
||||
{"id": "id6", "rank": 6, "score": 0.015151515151515152}
|
||||
{"id": "id7", "rank": 7, "score": 0.014925373134328358}
|
||||
{"id": "id8", "rank": 8, "score": 0.014705882352941176}
|
||||
{"id": "id9", "rank": 9, "score": 0.014492753623188406}
|
||||
{"id": "id10", "rank": 10, "score": 0.014285714285714285}
|
||||
{"id": "id11", "rank": 11, "score": 0.014084507042253521}
|
||||
{"id": "id12", "rank": 12, "score": 0.013888888888888888}
|
||||
{"id": "id13", "rank": 13, "score": 0.0136986301369863}
|
||||
{"id": "id14", "rank": 14, "score": 0.013513513513513514}
|
||||
{"id": "id15", "rank": 15, "score": 0.013333333333333334}
|
||||
{"id": "id16", "rank": 16, "score": 0.013157894736842105}
|
||||
{"id": "id17", "rank": 17, "score": 0.012987012987012988}
|
||||
{"id": "id18", "rank": 18, "score": 0.01282051282051282}
|
||||
{"id": "id19", "rank": 19, "score": 0.012658227848101266}
|
||||
{"id": "id20", "rank": 20, "score": 0.0125}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"id": "a", "rank": 1, "score": 0.01639344262295082}
|
||||
{"id": "b", "rank": 2, "score": 0.016129032258064516}
|
||||
@@ -0,0 +1 @@
|
||||
{"id": "only", "rank": 1, "score": 0.02459016393442623}
|
||||
@@ -0,0 +1,11 @@
|
||||
{"id": "a", "rank": 1, "score": 0.05671602326811211}
|
||||
{"id": "y", "rank": 2, "score": 0.04021516393442623}
|
||||
{"id": "c", "rank": 3, "score": 0.032266458495966696}
|
||||
{"id": "b", "rank": 4, "score": 0.0315136476426799}
|
||||
{"id": "x", "rank": 5, "score": 0.015873015873015872}
|
||||
{"id": "d", "rank": 6, "score": 0.015625}
|
||||
{"id": "e", "rank": 7, "score": 0.015384615384615385}
|
||||
{"id": "f", "rank": 8, "score": 0.015151515151515152}
|
||||
{"id": "z", "rank": 9, "score": 0.015151515151515152}
|
||||
{"id": "g", "rank": 10, "score": 0.014925373134328358}
|
||||
{"id": "h", "rank": 11, "score": 0.014705882352941176}
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Fuse ranked lists the way the retrieval layer fuses them.
|
||||
|
||||
The pipeline's fusion is written inline in a database query function, so the
|
||||
scoring loop is spelled out again here — but the two constants that decide
|
||||
everything are imported from it, so a change to either shows up as a parity
|
||||
failure rather than as a silently different ranking.
|
||||
|
||||
Reads hit records on stdin and writes `{"id", "rank", "score"}` per line.
|
||||
|
||||
CATENA_PARITY_PYTHONPATH=/path/to/knowledge-mcp/src \
|
||||
python3 oracle/rrf.py < hits.ndjson
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
for path in os.environ.get("CATENA_PARITY_PYTHONPATH", "").split(os.pathsep):
|
||||
if path:
|
||||
sys.path.insert(0, path)
|
||||
|
||||
from knowledge_mcp.retrieval import META_WEIGHT, RRF_K # noqa: E402
|
||||
|
||||
WEIGHTS = {"vec": 1.0, "fts": 1.0, "meta": META_WEIGHT}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
pool: dict[str, dict] = {}
|
||||
for line in sys.stdin.buffer.read().decode("utf-8").split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
hit = json.loads(line)
|
||||
entry = pool.setdefault(hit["id"], {"score": 0.0, "seen": {}})
|
||||
channel = hit["channel"]
|
||||
weight = WEIGHTS.get(channel, 1.0)
|
||||
previous = entry["seen"].get(channel)
|
||||
if previous is not None:
|
||||
entry["score"] -= weight / (RRF_K + previous)
|
||||
entry["seen"][channel] = hit["rank"]
|
||||
entry["score"] += weight / (RRF_K + hit["rank"])
|
||||
|
||||
# Python's dict keeps insertion order and its sort is stable, so ties come
|
||||
# out in the order the ids were first seen.
|
||||
ranked = sorted(pool.items(), key=lambda kv: kv[1]["score"], reverse=True)
|
||||
|
||||
out = sys.stdout
|
||||
for rank, (hit_id, entry) in enumerate(ranked, 1):
|
||||
out.write(json.dumps({"id": hit_id, "rank": rank, "score": entry["score"]}) + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+17
-1
@@ -19,7 +19,7 @@ type Written = Result<Vec<String>, String>;
|
||||
type Job = (&'static str, fn() -> Written);
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let jobs: [Job; 7] = [
|
||||
let jobs: [Job; 8] = [
|
||||
("tokens", tokens),
|
||||
("unescape", unescape),
|
||||
("chunkmd", chunkmd),
|
||||
@@ -27,6 +27,7 @@ fn main() -> ExitCode {
|
||||
("pdftriage", pdftriage),
|
||||
("cuenorm", cuenorm),
|
||||
("cueseg", cueseg),
|
||||
("rrf", rrf),
|
||||
];
|
||||
|
||||
for (name, job) in jobs {
|
||||
@@ -70,6 +71,21 @@ fn unescape() -> Written {
|
||||
Ok(vec![path.display().to_string()])
|
||||
}
|
||||
|
||||
fn rrf() -> Written {
|
||||
let dir = parity::golden("rrf");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut written = Vec::new();
|
||||
for case in parity::cases_with_extension("rrf", "ndjson") {
|
||||
let hits = std::fs::read_to_string(&case).map_err(|e| e.to_string())?;
|
||||
let fused = parity::run_oracle("rrf.py", &hits)?;
|
||||
let path = dir.join(format!("{}.ndjson", parity::case_name(&case)));
|
||||
std::fs::write(&path, fused).map_err(|e| e.to_string())?;
|
||||
written.push(path.display().to_string());
|
||||
}
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
fn cueseg() -> Written {
|
||||
let dir = parity::golden("cueseg");
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
//! Fused ranks and scores must match the retrieval layer exactly.
|
||||
//!
|
||||
//! A fused score is a sum of reciprocals, so the order it produces depends on
|
||||
//! the arithmetic being done the same way and on ties being broken the same
|
||||
//! way. Neither is visible in a result list — it just comes out in a slightly
|
||||
//! different order than the system it is meant to replace.
|
||||
|
||||
use corpus_core::Hit;
|
||||
use serde_json::Value;
|
||||
|
||||
fn hits(case: &std::path::Path) -> Vec<Hit> {
|
||||
let text = std::fs::read_to_string(case).expect("a readable case");
|
||||
corpus_core::ndjson::read(text.as_bytes())
|
||||
.collect::<Result<_, _>>()
|
||||
.expect("valid hit records")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_case_fuses_the_same_as_the_retrieval_layer() {
|
||||
let cases = parity::cases_with_extension("rrf", "ndjson");
|
||||
assert!(!cases.is_empty(), "no cases found");
|
||||
|
||||
for case in cases {
|
||||
let name = parity::case_name(&case);
|
||||
let golden = parity::read_golden_text(&format!("rrf/{name}.ndjson"));
|
||||
let theirs: Vec<Value> = golden
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| serde_json::from_str(line).expect("valid JSON"))
|
||||
.collect();
|
||||
let ours = rrf::fuse(&hits(&case), &rrf::Weights::default());
|
||||
|
||||
for (at, (ours, theirs)) in ours.iter().zip(&theirs).enumerate() {
|
||||
assert_eq!(
|
||||
ours.id,
|
||||
theirs["id"].as_str().expect("an id"),
|
||||
"{name}: position {at} holds a different hit"
|
||||
);
|
||||
assert_eq!(
|
||||
ours.rank as u64,
|
||||
theirs["rank"].as_u64().expect("a rank"),
|
||||
"{name}: {} ranks differently",
|
||||
ours.id
|
||||
);
|
||||
assert_eq!(
|
||||
ours.score,
|
||||
theirs["score"].as_f64().expect("a score"),
|
||||
"{name}: {} scores differently",
|
||||
ours.id
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
ours.len(),
|
||||
theirs.len(),
|
||||
"{name}: python fused {} hits, rust {}",
|
||||
theirs.len(),
|
||||
ours.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Catches a golden file drifting away from a retrieval layer that has moved
|
||||
/// on — in particular a change to the rank constant or the metadata weight.
|
||||
#[test]
|
||||
#[cfg(feature = "python")]
|
||||
fn the_golden_files_still_match_live_python() {
|
||||
for case in parity::cases_with_extension("rrf", "ndjson") {
|
||||
let name = parity::case_name(&case);
|
||||
let input = std::fs::read_to_string(&case).expect("a readable case");
|
||||
let live = parity::run_oracle("rrf.py", &input).expect("the oracle runs");
|
||||
assert_eq!(
|
||||
live,
|
||||
parity::read_golden_text(&format!("rrf/{name}.ndjson")),
|
||||
"golden file for {name} is stale"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user