# `AshScylla.Search`
[🔗](https://github.com/ohhi-vn/ash_scylla/blob/main/lib/ash_scylla/search.ex#L1)

A scalable multi-word search engine built on ScyllaDB using an inverted index.

Provides Lucene/OpenSearch-style search capabilities without `LIKE`,
`ALLOW FILTERING`, or secondary indexes.

## Architecture

The search engine uses an **inverted index** approach:

    Document → Analyzer → Tokenizer → Normalizer → Stemmer
      → Stop Words → Indexer → search_post_terms table

    Query → Analyzer → Planner → Boolean Engine → Ranking → Results

## Quick Start

    # 1. Create search tables
    Search.create_tables(MyApp.Repo, "my_keyspace")

    # 2. Index a document
    Search.index(MyApp.Repo, "my_keyspace", "post-uuid-here", %{
      title: "Learning Elixir Phoenix Framework",
      body: "Phoenix is a distributed web framework built on Elixir."
    })

    # 3. Search
    {:ok, results} = Search.search(MyApp.Repo, "my_keyspace", "learning phoenix")
    #=> %{entries: [{"post-uuid-here", 2.0}], ...}

## Features

  * Single-word and multi-word AND/OR search
  * Pagination with page metadata
  * Relevance ranking (TF, TF-IDF, BM25)
  * Document updates and deletes
  * Sharded partitions to prevent hotspot term partitions
  * Unicode-aware tokenization
  * Porter stemming
  * Stop word filtering

# `field_map`

```elixir
@type field_map() :: %{optional(atom()) =&gt; String.t()}
```

# `search_result`

```elixir
@type search_result() ::
  {:ok, AshScylla.Search.Query.Paginator.page()} | {:error, term()}
```

# `create_tables`

```elixir
@spec create_tables(module(), String.t()) :: :ok | {:error, term()}
```

Creates the search engine tables in the given keyspace.

This must be called once before indexing or searching.

Returns `:ok` or `{:error, reason}`.

# `delete`

```elixir
@spec delete(module(), String.t(), String.t()) :: :ok | {:error, term()}
```

Removes a document from the search index.

Deletes all term entries for the given post ID from both
`search_post_terms` and `search_post_fields`.

Returns `:ok` or `{:error, reason}`.

# `drop_tables`

```elixir
@spec drop_tables(module(), String.t()) :: :ok | {:error, term()}
```

Drops the search engine tables from the given keyspace.

# `index`

```elixir
@spec index(module(), String.t(), String.t(), field_map(), keyword()) ::
  :ok | {:error, term()}
```

Indexes a document into the search engine.

Accepts a map of field names to text values. Each field is analyzed
and its terms are written to the inverted index.

Returns `:ok` or `{:error, reason}`.

## Examples

    Search.index(MyApp.Repo, "my_keyspace", "abc-123", %{
      title: "Learning Elixir",
      body: "Elixir is a functional language"
    })

# `search`

```elixir
@spec search(module(), String.t(), String.t(), keyword()) :: search_result()
```

Searches the inverted index for documents matching the query.

The full search pipeline:
  1. Parse query string into AST
  2. Analyze query terms
  3. Look up posting lists from ScyllaDB
  4. Apply boolean operations (AND/OR/NOT)
  5. Rank results by relevance
  6. Paginate results

## Options

  * `:page` — page number, starting at 1 (default: `1`)
  * `:page_size` — results per page (default: `20`)
  * `:strategy` — ranking strategy: `:tf` (default), `:tfidf`, `:bm25`
  * `:num_shards` — shard count per term partition (default: `16`)
  * `:analyzer_opts` — options passed to the analyzer (e.g. `:stem`)

## Examples

    # Basic search
    {:ok, page} = Search.search(repo, keyspace, "elixir phoenix")

    # With AND/OR operators
    {:ok, page} = Search.search(repo, keyspace, "elixir OR phoenix")

    # With BM25 ranking and pagination
    {:ok, page} = Search.search(repo, keyspace, "distributed web",
      strategy: :bm25, page: 1, page_size: 10)

Returns `{:ok, page}` where `page` is a map with `:entries`, `:page_number`,
`:total_count`, etc., or `{:error, reason}`.

# `search!`

```elixir
@spec search!(module(), String.t(), String.t(), keyword()) ::
  AshScylla.Search.Query.Paginator.page() | no_return()
```

Same as `search/4` but raises on error.

# `update`

```elixir
@spec update(module(), String.t(), String.t(), field_map(), keyword()) ::
  :ok | {:error, term()}
```

Updates a document's indexed terms.

Computes the diff between old and new terms for each field, applying
only the necessary inserts and deletes. Fields omitted from the map
are left unchanged.

Returns `:ok` or `{:error, reason}`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
