searchez 1.0.0

A searchable-model layer for Rust: make a type searchable, keep the index in sync, and search with real relevance ranking — over a pluggable backend, with a batteries-included in-memory engine that needs no external service.
Documentation
// searchez/src/document.rs
//
// The neutral representation of a record in the index. A `Searchable` type maps
// itself to a `Document`; the backend tokenizes its text for full-text search,
// keeps every field for filtering, and returns it on a hit.

use serde_json::{Map, Value};

/// A flat bag of fields describing one record. Build it fluently, or straight
/// from a `serde_json::json!({...})` object.
///
/// ```
/// use searchez::Document;
/// let doc = Document::new()
///     .field("title", "The Rust Programming Language")
///     .field("year", 2018);
/// assert_eq!(doc.get("year").and_then(|v| v.as_i64()), Some(2018));
/// ```
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Document(Map<String, Value>);

impl Document {
    pub fn new() -> Self {
        Self(Map::new())
    }

    /// Add or replace a field. Values become searchable text when they are
    /// strings (or arrays/objects containing strings); every field is available
    /// for filtering and is returned on a hit.
    pub fn field(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
        self.0.insert(key.into(), value.into());
        self
    }

    /// Insert in place (the non-builder form of [`field`](Self::field)).
    pub fn set(&mut self, key: impl Into<String>, value: impl Into<Value>) {
        self.0.insert(key.into(), value.into());
    }

    pub fn get(&self, key: &str) -> Option<&Value> {
        self.0.get(key)
    }

    pub fn iter(&self) -> impl Iterator<Item = (&String, &Value)> {
        self.0.iter()
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    pub fn as_map(&self) -> &Map<String, Value> {
        &self.0
    }
}

impl From<Map<String, Value>> for Document {
    fn from(map: Map<String, Value>) -> Self {
        Self(map)
    }
}

/// Accepts a `json!({...})` object; a non-object value yields an empty document
/// rather than panicking (the caller's `to_document` should return an object).
impl From<Value> for Document {
    fn from(value: Value) -> Self {
        match value {
            Value::Object(map) => Self(map),
            _ => Self::new(),
        }
    }
}

impl serde::Serialize for Document {
    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
        self.0.serialize(s)
    }
}