README.md

# moss

Elixir SDK for moss — local-first indexing and querying for real-time agent workflows, with built-in embedding flows for `"moss-minilm"` and `"moss-mediumlm"`.

## Features

- **Local Session Index** — Index and query entirely in memory during a live session
- **Built-In Embeddings** — `add_docs/3` and `query/3` embed automatically for built-in models
- **Custom Embeddings** — Set `.embedding` on each `DocumentInfo` and pass `embedding:` to `query/3`
- **Index Loading** — Load indexes into memory and query them with `Moss.Client.query/4`
- **Cloud Sync** — Push a session index to cloud, or manage cloud indexes directly
- **High-Performance Rust Core** — Indexing, search, and embedding execution run through Rust NIFs

## Installation

```elixir
# mix.exs
defp deps do
  [{:moss, "~> 1.0"}]
end
```

## Quick Start

```elixir
alias Moss.{Client, DocumentInfo, Session}

# 1. Create a client
{:ok, client} = Client.new("your-project-id", "your-project-key")

# 2. Open a session (auto-loads from cloud if the index exists)
{:ok, session} = Client.session(client, "session-abc")

# 3. Index documents locally
docs = [
  %DocumentInfo{id: "1", text: "Customer asked about their invoice from March"},
  %DocumentInfo{id: "2", text: "Discussed upgrading to the business plan"},
  %DocumentInfo{id: "3", text: "Customer mentioned difficulty logging in"}
]

{:ok, {3, 0}} = Session.add_docs(session, docs)

# 4. Query (local, no cloud round trip)
{:ok, result} = Session.query(session, "billing question", top_k: 2)

for doc <- result.docs do
  IO.puts("#{Float.round(doc.score, 3)}  #{String.slice(doc.text, 0, 60)}")
end

# 5. Push to cloud
{:ok, push_result} = Session.push_index(session)
IO.puts("Pushed #{push_result.doc_count} docs as '#{push_result.index_name}'")
```

## Custom Embeddings

```elixir
{:ok, client} = Moss.Client.new("your-project-id", "your-project-key")
{:ok, session} = Moss.Client.session(client, "session-abc", model_id: "custom")

docs = [
  %Moss.DocumentInfo{id: "1", text: "Invoice from March", embedding: embedding_1},
  %Moss.DocumentInfo{id: "2", text: "Plan upgrade", embedding: embedding_2}
]

{:ok, {2, 0}} = Moss.Session.add_docs(session, docs)
{:ok, result} = Moss.Session.query(session, "billing", embedding: query_embedding, top_k: 2)
```

## Load and Query Cloud Indexes Locally

```elixir
{:ok, client} = Moss.Client.new("project-id", "project-key")

# Load a cloud index into memory
{:ok, _} = Moss.Client.load_index(client, "faq-index")

# Query it locally
{:ok, results} = Moss.Client.query(client, "faq-index", "cancel subscription", top_k: 3)
```

## Session + Loaded Cloud Index

```elixir
{:ok, client} = Moss.Client.new("project-id", "project-key")
{:ok, _} = Moss.Client.load_index(client, "faq-index")
{:ok, session} = Moss.Client.session(client, "session-xyz")

# Index session-specific documents
{:ok, _} =
  Moss.Session.add_docs(session, [
    %Moss.DocumentInfo{id: "turn-1", text: "Customer: I need to cancel my subscription"},
    %Moss.DocumentInfo{id: "turn-2", text: "Agent: I can help with that. Can I ask why?"}
  ])

# Query session and cloud indexes independently
{:ok, session_results} = Moss.Session.query(session, "subscription cancellation", top_k: 3)
{:ok, faq_results} = Moss.Client.query(client, "faq-index", "subscription cancellation", top_k: 3)

# Push session to cloud when done
{:ok, _} = Moss.Session.push_index(session)
```

For loaded `"custom"` indexes, pass `embedding: [...]` to `Moss.Client.query/4`.

## Metadata Filters

All local query functions accept the same `:filter` keyword argument.

```elixir
filter = %{"field" => "type", "condition" => %{"$eq" => "billing"}}
{:ok, result} = Moss.Session.query(session, "invoice", filter: filter)

filter = %{
  "$and" => [
    %{"field" => "type", "condition" => %{"$eq" => "billing"}},
    %{"field" => "priority", "condition" => %{"$gte" => "5"}}
  ]
}

{:ok, result} = Moss.Client.query(client, "faq-index", "urgent billing", filter: filter)
```

Supported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$near`

Logical combinators: `$and`, `$or`

## Available Models

| `model_id` | Behavior |
|---|---|
| `"moss-minilm"` | Built-in local embeddings, optimized for speed |
| `"moss-mediumlm"` | Built-in local embeddings, higher quality |
| `"custom"` | Caller supplies embeddings via `DocumentInfo.embedding` and `embedding:` opt |

## Hybrid Search

The `:alpha` option blends semantic and keyword search.

```elixir
Moss.Session.query(session, "billing", alpha: 0.0)  # keyword-only
Moss.Session.query(session, "billing")               # balanced (default 0.8)
Moss.Session.query(session, "billing", alpha: 1.0)  # semantic-only
```

## Cloud CRUD

```elixir
{:ok, client} = Moss.Client.new("project-id", "project-key")
{:ok, result} = Moss.Client.create_index(client, "my-index", docs)
{:ok, info}   = Moss.Client.get_index(client, "my-index")
{:ok, status} = Moss.Client.get_job_status(client, result.job_id)
```

## Module Reference

| Module | Description |
|---|---|
| `Moss.Client` | Entry point — cloud CRUD, local index ops, and sessions |
| `Moss.Session` | GenServer for local session indexing |
| `Moss.DocumentInfo` | `%{id, text, metadata, embedding}` |
| `Moss.SearchResult` | `%{docs, query, index_name, time_taken_ms}` |
| `Moss.QueryResultDoc` | `%{id, text, metadata, score}` |
| `Moss.IndexInfo` | `%{id, name, version, status, doc_count, created_at, updated_at, model}` |
| `Moss.PushIndexResult` | `%{job_id, index_name, doc_count, status}` |

## License

This package is licensed under the [PolyForm Shield License 1.0.0](./LICENSE).

- Free for testing, evaluation, internal use, and modifications.
- Not permitted for production or competing commercial use.
- For commercial licenses, contact: <contact@moss.dev>

`moss` reports aggregated usage counts to Moss servers for billing. No document content is sent.

## Contact

For support, commercial licensing, or partnership inquiries: [contact@moss.dev](mailto:contact@moss.dev)