1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
//! All things related to experimental runs, including efficiency and precision runs.
extern crate failure;
extern crate strum;
extern crate strum_macros;
extern crate tempdir;
extern crate yaml_rust;

use crate::config::ParseYaml;
use crate::{
    command::ExtCommand,
    config::{Collection, CollectionMap, YamlExt},
    error::Error,
    executor::PisaExecutor,
};
use failure::ResultExt;
use std::{
    fs,
    path::{Path, PathBuf},
    rc::Rc,
};
use strum_macros::{Display, EnumIter, EnumString};
use tempdir::TempDir;
use yaml_rust::Yaml;
use RunData::{Benchmark, Evaluate};

/// Represents one of the three available fields in a TREC topic file
/// to be used as an input for query processing.
#[derive(Debug, Clone, PartialEq, EnumString, Display, EnumIter)]
pub enum TrecTopicField {
    /// Short query from `<title>`
    #[strum(serialize = "title")]
    Title,
    /// Mid-length query from `<desc>`
    #[strum(serialize = "desc")]
    Description,
    /// Long query from `<narr>`
    #[strum(serialize = "narr")]
    Narrative,
}

/// Format in which query topics are provided.
#[derive(Debug, Clone, PartialEq)]
pub enum TopicsFormat {
    /// Each query is in format: `qid:query terms`; one query per line.
    Simple,
    /// TREC format; example: [](https://trec.nist.gov/data/terabyte/04/04topics.701-750.txt)
    Trec(TrecTopicField),
}

/// Data for evaluation run.
#[derive(Debug, Clone)]
pub struct EvaluateData {
    /// Path to topics in TREC format
    pub topics: PathBuf,
    /// Format of the file with topics (queries)
    pub topics_format: TopicsFormat,
    /// Path to a [TREC qrels
    /// file](https://www-nlpir.nist.gov/projects/trecvid/trecvid.tools/trec_eval_video/A.README)
    pub qrels: PathBuf,
    /// Where the output of `trec_eval` will be written.
    pub output_file: PathBuf,
}

/// An experimental run
#[derive(Debug, Clone)]
pub struct Run {
    /// Pointer to evalated collection
    pub collection: Rc<Collection>,
    /// Data specific to type of run
    pub data: RunData,
}

/// An experimental run.
#[derive(Debug, Clone)]
pub enum RunData {
    /// Report selected precision metrics.
    Evaluate(EvaluateData),
    /// Report query times
    Benchmark,
}
impl RunData {
    /// Cast to `EvaluateData` if run is `Evaluate`, or return `None`.
    pub fn as_evaluate(&self) -> Option<&EvaluateData> {
        match self {
            Evaluate(eval_data) => Some(eval_data),
            _ => None,
        }
    }
}
impl Run {
    fn parse_topics_format(yaml: &Yaml) -> Result<Option<TopicsFormat>, Error> {
        let topics_format = &yaml["topics_format"];
        if let Yaml::BadValue = topics_format {
            Ok(None)
        } else if let Yaml::String(topics_format) = topics_format {
            match topics_format.as_ref() {
                "simple" => Ok(Some(TopicsFormat::Simple)),
                "trec" => {
                    let field = yaml.require_string("trec_topic_field")?;
                    Ok(Some(TopicsFormat::Trec(
                        field
                            .parse::<TrecTopicField>()
                            .context("failed to parse trec topic field")?,
                    )))
                }
                invalid => Err(Error::from(format!("invalid topics format: {}", invalid))),
            }
        } else {
            Err(Error::from("topics_format is not a string value"))
        }
    }

    fn parse_evaluate<P>(yaml: &Yaml, collection: Rc<Collection>, workdir: P) -> Result<Self, Error>
    where
        P: AsRef<Path>,
    {
        let topics = yaml.require_string("topics")?;
        let qrels = yaml.require_string("qrels")?;
        let output_file = yaml.require_string("output")?;
        Ok(Self {
            collection,
            data: Evaluate(EvaluateData {
                topics: PathBuf::from(topics),
                topics_format: Self::parse_topics_format(yaml)?
                    .unwrap_or(TopicsFormat::Trec(TrecTopicField::Title)),
                qrels: PathBuf::from(qrels),
                output_file: match PathBuf::from(output_file) {
                    ref abs if abs.is_absolute() => abs.clone(),
                    ref rel => workdir.as_ref().join(rel),
                },
            }),
        })
    }

    /// Constructs from a YAML object, given a collection map.
    ///
    /// Fails if failed to parse, or when the referenced collection is missing form
    /// the mapping.
    ///
    /// # Example
    /// ```
    /// # extern crate yaml_rust;
    /// # extern crate stdbench;
    /// # use stdbench::run::Run;
    /// # use stdbench::config::*;
    /// # use std::collections::HashMap;
    /// # use std::path::PathBuf;
    /// # use std::rc::Rc;
    /// let yaml = yaml_rust::YamlLoader::load_from_str("
    /// collection: wapo
    /// type: evaluate
    /// topics: /path/to/query/topics
    /// qrels: /path/to/query/relevance
    /// output: /output").unwrap();
    ///
    /// let mut collections: CollectionMap = HashMap::new();
    /// let run = Run::parse(&yaml[0], &collections, PathBuf::from("work"));
    /// assert!(run.is_err());
    ///
    /// collections.insert(String::from("wapo"), Rc::new(Collection {
    ///     name: "wapo".to_string(),
    ///     kind: WashingtonPostCollection::boxed(),
    ///     collection_dir: PathBuf::from("/coll/dir"),
    ///     forward_index: PathBuf::from("fwd"),
    ///     inverted_index: PathBuf::from("inv"),
    ///     encodings: vec![Encoding::from("block_simdbp")]
    /// }));
    /// let run = Run::parse(&yaml[0], &collections, PathBuf::from("work")).unwrap();
    /// assert_eq!(run.collection.name, "wapo");
    /// ```
    pub fn parse<P>(yaml: &Yaml, collections: &CollectionMap, workdir: P) -> Result<Self, Error>
    where
        P: AsRef<Path>,
    {
        let collection_name: String = yaml.parse_field("collection")?;
        let collection = collections
            .get(&collection_name)
            .ok_or_else(|| format!("collection {} not found in config", collection_name))?;
        let typ: String = yaml.parse_field("type")?;
        match typ.as_ref() {
            "evaluate" => Self::parse_evaluate(yaml, Rc::clone(collection), workdir),
            unknown => Err(Error::from(format!("unknown run type: {}", unknown))),
        }
    }

    /// Returns the type of this run.
    ///
    /// # Example
    ///
    /// ```
    /// # extern crate yaml_rust;
    /// # extern crate stdbench;
    /// # use stdbench::run::*;
    /// # use stdbench::config::*;
    /// # use std::collections::HashMap;
    /// # use std::path::PathBuf;
    /// # use std::rc::Rc;
    /// let collection = Rc::new(Collection {
    ///     name: "wapo".to_string(),
    ///     kind: WashingtonPostCollection::boxed(),
    ///     collection_dir: PathBuf::from("/coll/dir"),
    ///     forward_index: PathBuf::from("fwd"),
    ///     inverted_index: PathBuf::from("inv"),
    ///     encodings: vec![Encoding::from("block_simdbp")]
    /// });
    /// assert_eq!(
    ///     Run {
    ///         collection: Rc::clone(&collection),
    ///         data: RunData::Evaluate(EvaluateData {
    ///             topics: PathBuf::new(),
    ///             topics_format: TopicsFormat::Simple,
    ///             qrels: PathBuf::new(),
    ///             output_file: PathBuf::from("output")
    ///         })
    ///     }.run_type(),
    ///     "evaluate"
    /// );
    /// assert_eq!(
    ///     Run {
    ///         collection,
    ///         data: RunData::Benchmark
    ///     }.run_type(),
    ///     "benchmark"
    /// );
    /// ```
    pub fn run_type(&self) -> String {
        match &self.data {
            Evaluate(_) => String::from("evaluate"),
            Benchmark => String::from("benchmark"),
        }
    }
}

/// Runs query evaluation for on a given executor, for a given run.
///
/// Fails if the run is not of type `Evaluate`.
pub fn evaluate(executor: &dyn PisaExecutor, run: &Run) -> Result<String, Error> {
    if let Evaluate(data) = &run.data {
        let queries = if let TopicsFormat::Trec(field) = &data.topics_format {
            executor.extract_topics(&data.topics, &data.topics)?;
            format!("{}.{}", &data.topics.display(), field)
        } else {
            data.topics.to_str().unwrap().to_string()
        };
        executor.evaluate_queries(&run.collection, &data, &queries)
    } else {
        Err(Error::from(format!(
            "Run of type {} cannot be evaluated",
            run.run_type()
        )))
    }
}

/// Process a run (e.g., single precision evaluation or benchmark).
pub fn process_run(executor: &dyn PisaExecutor, run: &Run) -> Result<(), Error> {
    match &run.data {
        Evaluate(eval) => {
            let output = evaluate(executor, &run)?;
            let tmp = TempDir::new("evaluate_queries").expect("Failed to create temp directory");
            let results_path = tmp.path().join("results.trec");
            std::fs::write(&results_path, &output)?;
            let output = ExtCommand::new("trec_eval")
                .arg("-q")
                .arg("-a")
                .arg(eval.qrels.to_str().unwrap())
                .arg(results_path.to_str().unwrap())
                .output()?;
            let eval_result =
                String::from_utf8(output.stdout).context("unable to parse result of trec_eval")?;
            fs::write(&eval.output_file, eval_result)?;
            Ok(())
        }
        Benchmark => {
            unimplemented!("Benchmark runs are currently unimplemented");
        }
    }
}

#[cfg(test)]
mod tests;