// The unit every stage of the pipeline moves: a piece of text with an optional identity and
// arbitrary metadata. Split, load, embed, store and retrieve all read or write only this type, so a
// document outlives the pipeline that produced it.
///|
/// A piece of text, with an optional identity and metadata.
///
/// `id` is what a store keys a document by. Leave it `None` and the store assigns one; set it and the
/// document keeps the same identity across re-indexing, which is what makes an update replace the old
/// copy instead of piling up beside it. `meta` carries whatever the caller needs — a source path, a
/// page number, a heading path — and is what a `Filter` reads.
#derive(Eq, Debug)
pub(all) struct Document {
id : String?
text : String
meta : Map[String, Json]
}
///|
/// A document with `text`, no identity, and no metadata.
pub fn Document::new(
text : String,
id? : String,
meta? : Map[String, Json],
) -> Document {
{ id, text, meta: meta.unwrap_or(Map([])), }
}
///|
/// The same document carrying `metadata`, for a filter to match on.
pub fn Document::with_meta(
self : Document,
metadata : Map[String, Json],
) -> Document {
{ ..self, meta: metadata, }
}
///|
/// The same document with `key` set to `value`; a key already present is replaced.
pub fn Document::set_meta(
self : Document,
key : String,
value : Json,
) -> Document {
let meta = self.meta.copy()
meta[key] = value
{ ..self, meta, }
}
///|
/// The metadata value stored under `key`, or `None`.
pub fn Document::meta_of(self : Document, key : String) -> Json? {
self.meta.get(key)
}
///|
/// The metadata value stored under `key` when it is a string, or `None`.
pub fn Document::str_meta(self : Document, key : String) -> String? {
match self.meta.get(key) {
Some(Json::String(text)) => Some(text)
Some(_) => None
None => None
}
}
///|
/// The same document under `id`, for a store that keys by caller-chosen identity.
pub fn Document::with_id(self : Document, id : String) -> Document {
{ ..self, id: Some(id), }
}
///|
/// A document cut out of `parent`: same metadata plus the chunk number, a derived id when the parent
/// had one, and the offset it started at when the splitter knows it.
///
/// Splitters are not the only producers of chunks — a caller streaming pages out of a PDF has the same
/// needs — so the bookkeeping lives here rather than in each of them.
pub fn Document::derived(
self : Document,
index : Int32,
text : String,
start_index? : Int32,
) -> Document {
let meta = self.meta.copy()
meta["chunk"] = Json::number(index.to_double(), repr=index.to_string())
if start_index is Some(start) {
meta["start_index"] = Json::number(
start.to_double(),
repr=start.to_string(),
)
}
let id = match self.id {
Some(id) => Some(id + "#" + index.to_string())
None => None
}
{ id, text, meta, }
}
///|
/// A document and the score it earned: what one search returns.
#derive(Eq, Debug)
pub(all) struct Scored {
document : Document
score : Double
}
///|
/// The documents in `hits`, dropping the scores.
pub fn Scored::documents(hits : ArrayView[Scored]) -> Array[Document] {
hits.map(fn(hit) { hit.document })
}
///|
pub impl ToJson for Document with fn to_json(self) {
let fields : Map[String, Json] = Map([])
match self.id {
Some(id) => fields["id"] = Json::string(id)
None => ()
}
fields["text"] = Json::string(self.text)
fields["meta"] = Json::object(self.meta.copy())
Json::object(fields)
}
///|
pub impl ToJson for Scored with fn to_json(self) {
Json::object(
Map([
("document", ToJson::to_json(self.document)),
("score", Json::number(self.score)),
]),
)
}
///|
pub impl FromJson for Document with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "want an object"))
}
let id = match fields.get("id") {
Some(Json::String(id)) => Some(id)
Some(_) => raise JsonDecodeError((path.add_key("id"), "want a string"))
None => None
}
let text = match fields.get("text") {
Some(Json::String(text)) => text
Some(_) => raise JsonDecodeError((path.add_key("text"), "want a string"))
None => raise JsonDecodeError((path.add_key("text"), "want a string"))
}
let meta = match fields.get("meta") {
Some(Json::Object(meta)) => meta
Some(_) => raise JsonDecodeError((path.add_key("meta"), "want an object"))
None => Map([])
}
{ id, text, meta, }
}
///|
pub impl FromJson for Scored with fn from_json(json, path) {
guard json is Object(fields) else {
raise JsonDecodeError((path, "want an object"))
}
let document = match fields.get("document") {
Some(value) => @json.from_json(value, path=path.add_key("document"))
None => raise JsonDecodeError((path.add_key("document"), "want a document"))
}
let score = match fields.get("score") {
Some(Json::Number(score, ..)) => score
Some(_) => raise JsonDecodeError((path.add_key("score"), "want a number"))
None => raise JsonDecodeError((path.add_key("score"), "want a number"))
}
{ document, score, }
}
///|
pub extend Document with Eq::{not_equal, equal}
///|
pub extend Document with Debug::{to_repr}
///|
pub extend Document with ToJson::{to_json}
///|
pub extend Document with FromJson::{from_json}
///|
pub extend Scored with Eq::{not_equal, equal}
///|
pub extend Scored with Debug::{to_repr}
///|
pub extend Scored with ToJson::{to_json}
///|
pub extend Scored with FromJson::{from_json}