AshScylla.Search (AshScylla v1.6.1)

Copy Markdown View Source

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

Summary

Functions

Creates the search engine tables in the given keyspace.

Removes a document from the search index.

Drops the search engine tables from the given keyspace.

Indexes a document into the search engine.

Searches the inverted index for documents matching the query.

Updates a document's indexed terms.

Types

field_map()

@type field_map() :: %{optional(atom()) => String.t()}

search_result()

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

Functions

create_tables(repo, keyspace)

@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(repo, keyspace, post_id)

@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(repo, keyspace)

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

Drops the search engine tables from the given keyspace.

index(repo, keyspace, post_id, fields, opts \\ [])

@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(repo, keyspace, query, opts \\ [])

@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!(repo, keyspace, query, opts \\ [])

Same as search/4 but raises on error.

update(repo, keyspace, post_id, fields, opts \\ [])

@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}.