arvel-search¶
Introduction¶
arvel-search provides full-text search, modeled after Laravel Scout. Add Searchable to a model, declare which fields to index, and records sync to the configured backend automatically. Query with Model.search("term").
Installation¶
Register the provider:
# bootstrap/providers.py
from arvel_search import SearchServiceProvider
providers = [SearchServiceProvider]
The provider registers SearchConfig and SearchManager, binds the Search facade, and registers the SearchIndexJob / SearchRemoveJob queue jobs. There are no migrations.
Making a Model Searchable¶
Mix in Searchable and list the indexed fields in __searchable__:
from arvel.database import Model, id_, string
from arvel_search import Searchable
class Article(Model, Searchable):
__tablename__ = "articles"
__searchable__ = ("title", "body")
id: int = id_()
title: str = string(200)
body: str = string(2000)
Create/update/delete now sync to the search engine automatically (when an engine is bound).
Optional class attributes: __search_index__ (index name; defaults from the table) and __search_key__ (the document key; defaults to id). Override to_searchable_array() to control the indexed document.
Searching¶
results = await Article.search("python").get() # list[Article]
first = await Article.search("python").where("body", "x").first()
ids = await Article.search("python").keys()
total = await Article.search("python").count()
page = await Article.search("python").paginate(per_page=15, page=1)
The builder supports where(column, value), limit, offset, and the terminal methods above.
Drivers¶
SEARCH_DRIVER picks the backend (default database):
| Driver | Behavior |
|---|---|
database | ILIKE against the model's own table columns. Writes are no-ops — no separate index. Best for small tables / admin search. |
collection | In-memory substring match. For tests and local dev. |
null | Swallows writes; search returns nothing. |
meilisearch | Talks to a Meilisearch server over HTTP. |
elasticsearch | Talks to Elasticsearch over HTTP. |
Configuration (env, all SEARCH_*):
| Env var | Default |
|---|---|
SEARCH_DRIVER | database |
SEARCH_SYNC_ON_SAVE | true |
SEARCH_QUEUE_SYNC | false |
SEARCH_MEILISEARCH_URL | http://localhost:7700 |
SEARCH_MEILISEARCH_KEY | "" |
SEARCH_ELASTICSEARCH_URL | http://localhost:9200 |
SEARCH_ELASTICSEARCH_KEY | "" |
Register a custom engine with SearchManager.register_driver(name, factory).
Bulk (Re)indexing¶
await Article.make_all_searchable() # index every row
await Article.remove_all_from_search() # clear the index
Testing¶
Search.fake() swaps in an assertable double:
from arvel_search import Search
fake = Search.fake()
article = await Article.create(title="Hello", body="world")
fake.assert_indexed(article)
Search.restore()
Gotchas¶
- Querying with no engine bound raises
SearchEngineNotConfigured; auto-sync is a no-op when no engine is bound. - Declaring a sensitive-looking column in
__searchable__triggers a runtime warning. - The
databasedriver searches live SQL — it doesn't build or maintain a separate index.