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/query.rs
//
// What you ask for, and what comes back.

use crate::document::Document;
use serde_json::Value;

/// An exact-match constraint on a single field, ANDed with the others.
#[derive(Debug, Clone)]
pub struct Filter {
    pub field: String,
    pub value: Value,
}

/// A search request: optional full-text, zero or more exact filters, and a page.
///
/// ```
/// use searchez::Query;
/// let q = Query::text("dark roast")
///     .filter("in_stock", true)
///     .limit(10);
/// ```
#[derive(Debug, Clone, Default)]
pub struct Query {
    /// Free-text matched against the document's text; `None`/empty means
    /// "everything that passes the filters" (a pure filter query).
    pub text: Option<String>,
    pub filters: Vec<Filter>,
    pub limit: usize,
    pub offset: usize,
}

/// The default page size when a query doesn't set one — enough for a first page,
/// bounded so an unqualified search can't return an entire index.
pub const DEFAULT_LIMIT: usize = 20;

impl Query {
    /// A full-text query.
    pub fn text(text: impl Into<String>) -> Self {
        Self {
            text: Some(text.into()),
            limit: DEFAULT_LIMIT,
            ..Default::default()
        }
    }

    /// A query with no text — returns everything matching the filters, ranked by
    /// id. Add filters to narrow it.
    pub fn all() -> Self {
        Self {
            limit: DEFAULT_LIMIT,
            ..Default::default()
        }
    }

    pub fn filter(mut self, field: impl Into<String>, value: impl Into<Value>) -> Self {
        self.filters.push(Filter {
            field: field.into(),
            value: value.into(),
        });
        self
    }

    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    pub fn offset(mut self, offset: usize) -> Self {
        self.offset = offset;
        self
    }

    /// The effective page size (falls back to [`DEFAULT_LIMIT`] when unset).
    pub fn effective_limit(&self) -> usize {
        if self.limit == 0 {
            DEFAULT_LIMIT
        } else {
            self.limit
        }
    }
}

/// A plain string is the common case — a full-text query with defaults.
impl From<&str> for Query {
    fn from(s: &str) -> Self {
        Query::text(s)
    }
}

impl From<String> for Query {
    fn from(s: String) -> Self {
        Query::text(s)
    }
}

/// One matching record: its id, relevance score, and the stored document (so the
/// common case needs no second lookup; see the crate docs on hydrating full DB
/// records when you need more than was indexed).
#[derive(Debug, Clone)]
pub struct Hit {
    pub id: String,
    /// BM25 relevance for a text query; `0.0` for a pure filter query.
    pub score: f32,
    pub document: Document,
}