Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/saadmk11/redis-search-django

Django package that provides auto indexing and searching capabilities for Django model instances using RediSearch.
https://github.com/saadmk11/redis-search-django

django django-application django-packages django-redis django-search django-search-engine python python-redis python3 redis redis-django redis-python redis-search redis-search-django redisearch redisjson

Last synced: about 1 month ago
JSON representation

Django package that provides auto indexing and searching capabilities for Django model instances using RediSearch.

Awesome Lists containing this project

README

        

# redis-search-django

[![Pypi Version](https://img.shields.io/pypi/v/redis-search-django.svg?style=flat-square)](https://pypi.org/project/redis-search-django/)
[![Supported Python Versions](https://img.shields.io/pypi/pyversions/redis-search-django?style=flat-square)](https://pypi.org/project/redis-search-django/)
[![Supported Django Versions](https://img.shields.io/pypi/frameworkversions/django/redis-search-django?color=darkgreen&style=flat-square)](https://pypi.org/project/redis-search-django/)
[![License](https://img.shields.io/github/license/saadmk11/redis-search-django?style=flat-square)](https://github.com/saadmk11/redis-search-django/blob/main/LICENSE)

![Django Tests](https://img.shields.io/github/actions/workflow/status/saadmk11/redis-search-django/test.yml??label=Test&style=flat-square&branch=main)
![Codecov](https://img.shields.io/codecov/c/github/saadmk11/redis-search-django?style=flat-square&token=ugjHXbEKib)
![pre-commit.ci](https://img.shields.io/badge/pre--commit.ci-enabled-brightgreen?logo=pre-commit&logoColor=white&style=flat-square)
![Changelog-CI](https://img.shields.io/github/actions/workflow/status/saadmk11/redis-search-django/changelog-ci.yaml??label=Changelog-CI&style=flat-square&branch=main)
![Code Style](https://img.shields.io/badge/Code%20Style-Black-black?style=flat-square)

# About

A Django package that provides **auto indexing** and **searching** capabilities for Django model instances using **[RediSearch](https://redis.io/docs/stack/search/)**.

# Features

- Management Command to create, update and populate the RediSearch Index.
- Auto Index on Model object Create, Update and Delete.
- Auto Index on Related Model object Add, Update, Remove and Delete.
- Easy to create Document classes (Uses Django Model Form Class like structure).
- Index nested models (e.g: `OneToOneField`, `ForeignKey` and `ManyToManyField`).
- Search documents using `redis-om`.
- Search Result Pagination.
- Search Result Sorting.
- RediSearch Result to Django QuerySet.
- Faceted Search.

# Requirements

- Python: 3.7, 3.8, 3.9, 3.10
- Django: 3.2, 4.0, 4.1
- redis-om: >= 0.0.27

# Redis

## Downloading Redis

The latest version of Redis is available from [Redis.io](https://redis.io/). You can also install Redis with your operating system's package manager.

## RediSearch and RedisJSON

`redis-search-django` relies on the [RediSearch](https://redis.io/docs/stack/search/) and [RedisJSON](https://redis.io/docs/stack/json/) Redis modules to support rich queries and embedded models.
You need these Redis modules to use `redis-search-django`.

The easiest way to run these Redis modules during local development is to use the [redis-stack](https://hub.docker.com/r/redis/redis-stack) Docker image.

## Docker Compose

There is a `docker-compose.yaml` file provided in the project's root directory.
This file will run Redis with RedisJSON and RediSearch modules during development.

Run the following command to start the Redis container:

```bash
docker compose up -d
```

# Example Project

There is an example project available at [Example Project](https://github.com/saadmk11/redis-search-django/tree/main/example).

# Documentation

## Installation

```bash
pip install redis-search-django
```

Then add `redis_search_django` to your `INSTALLED_APPS`:

```bash
INSTALLED_APPS = [
...
'redis_search_django',
]
```

## Usage

### Document Types

There are **3 types** of documents class available:

- **JsonDocument:** This uses `RedisJSON` to store the document. If you want to use Embedded Documents (Required For `OneToOneField`, `ForeignKey` and `ManyToManyField`) then use `JsonDocument`.
- **EmbeddedJsonDocument:** If the document will be embedded inside another document class then use this. Embedded Json Documents are used for `OneToOneField`, `ForeignKey` and `ManyToManyField` or any types of nested documents.
- **HashDocument:** This uses `RedisHash` to store the documents. It can not be used for nested documents.

### Creating Document Classes

You need to inherit from The Base Document Classes mentioned above to build a document class.

#### Simple Example

**1. For Django Model:**

```python
# models.py

from django.db import models

class Category(models.Model):
name = models.CharField(max_length=30)
slug = models.SlugField(max_length=30)

def __str__(self) -> str:
return self.name
```

**2. You can create a document class like this:**

**Note:** Document classes must be stored in `documents.py` file.

```python
# documents.py

from redis_search_django.documents import JsonDocument

from .models import Category

class CategoryDocument(JsonDocument):
class Django:
model = Category
fields = ["name", "slug"]
```

**3. Run Index Django Management Command to create the index on Redis:**

```bash
python manage.py index
```

**Note:** This will also populate the index with existing data from the database

Now category objects will be indexed on create/update/delete.

#### More Complex Example

**1. For Django Models:**

```python
# models.py

from django.db import models

class Tag(models.Model):
name = models.CharField(max_length=30)

def __str__(self) -> str:
return self.name

class Vendor(models.Model):
name = models.CharField(max_length=30)
email = models.EmailField()
establishment_date = models.DateField()

def __str__(self) -> str:
return self.name

class Product(models.Model):
name = models.CharField(max_length=256)
description = models.TextField(blank=True)
vendor = models.OneToOneField(Vendor, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag, blank=True)
price = models.DecimalField(max_digits=6, decimal_places=2)

def __str__(self) -> str:
return self.name
```

**2. You can create a document classes like this:**

**Note:** Document classes must be stored in `documents.py` file.

```python
# documents.py

from typing import List

from django.db import models
from redis_om import Field

from redis_search_django.documents import EmbeddedJsonDocument, JsonDocument

from .models import Product, Tag, Vendor

class TagDocument(EmbeddedJsonDocument):
custom_field: str = Field(index=True, full_text_search=True)

class Django:
model = Tag
# Model Fields
fields = ["name"]

@classmethod
def prepare_custom_field(cls, obj):
return "CUSTOM FIELD VALUE"

class VendorDocument(EmbeddedJsonDocument):
class Django:
model = Vendor
# Model Fields
fields = ["name", "establishment_date"]

class ProductDocument(JsonDocument):
# OnetoOneField, with null=False
vendor: VendorDocument
# ManyToManyField
tags: List[TagDocument]

class Django:
model = Product
# Model Fields
fields = ["name", "description", "price"]
# Related Model Options
related_models = {
Vendor: {
"related_name": "product",
"many": False,
},
Tag: {
"related_name": "product_set",
"many": True,
},
}

@classmethod
def get_queryset(cls) -> models.QuerySet:
"""Override Queryset to filter out available products."""
return super().get_queryset().filter(available=True)

@classmethod
def prepare_name(cls, obj):
"""Use this to update field value."""
return obj.name.upper()
```

**Note:**

- You can not inherit from `HashDocument` for documents that include nested fields.
- You need to inherit from `EmbeddedJsonDocument` for document classes that will be embedded inside another document class.
- You need to explicitly add `OneToOneField`, `ForeignKey` or `ManyToManyField` (e.g: `tags: List[TagDocument]`) with an embedded document class if you want to index them.
you can not add it in the `Django.fields` option.
- For `related_models` option, you need to specify the fields `related_name` and if it is a `ManyToManyField` or a `ForeignKey` Field then specify `"many": True`.
- `related_models` will be used when a related object is saved that contributes to the document.
- You can define `prepare_{field_name}` method to update the value of a field before indexing.
- If it is a custom field (not a model field) you must define a `prepare_{field_name}` method that returns the value of the field.
- You can override `get_queryset` method to provide more filtering. This will be used while indexing a queryset.
- Field names must match model field names or define a `prepare_{field_name}` method.

**3. Run Index Django Management Command to create the index on Redis:**

```bash
python manage.py index
```

**Note:** This will also populate the index with existing data from the database

### Management Command

This package comes with `index` management command that can be used to index all the model instances to Redis index if it has a Document class defined.

**Note:** Make sure that Redis is running before running the command.

Run the following command to index **all** models that have Document classes defined:

```bash
python manage.py index
```

You can use `--migrate-only` option to **only** update the **index schema**.

```bash
python manage.py index --migrate-only
```

You can use `--models` to **specify** which models to index (models must have a Document class defined to be indexed).

```bash
python manage.py index --models app_name.ModelName app_name2.ModelName2
```

### Views

You can use the `redis_search_django.mixin.RediSearchListViewMixin` with a Django Generic View to search for documents.
`RediSearchPaginator` which helps paginate `ReadiSearch` results is also added to this mixin.

#### Example

```python
# views.py

from django.utils.functional import cached_property
from django.views.generic import ListView
from redis.commands.search import reducers

from redis_search_django.mixins import RediSearchListViewMixin

from .documents import ProductDocument
from .models import Product

class SearchView(RediSearchListViewMixin, ListView):
paginate_by = 20
model = Product
template_name = "core/search.html"
document_class = ProductDocument

@cached_property
def search_query_expression(self):
query = self.request.GET.get("query")
query_expression = None

if query:
query_expression = (
self.document_class.name % query
| self.document_class.description % query
)

return query_expression

@cached_property
def sort_by(self):
return self.request.GET.get("sort")

def facets(self):
if self.search_query_expression:
request = self.document_class.build_aggregate_request(
self.search_query_expression
)
else:
request = self.document_class.build_aggregate_request()

result = self.document_class.aggregate(
request.group_by(
["@tags_name"],
reducers.count().alias("count"),
)
)
return result
```

### Search

This package uses `redis-om` to search for documents.

#### Example

```python
from .documents import ProductDocument

categories = ["category1", "category2"]
tags = ["tag1", "tag2"]

# Search For Products That Match The Search Query (name or description)
query_expression = (
ProductDocument.name % "Some search query"
| ProductDocument.description % "Some search query"
)

# Search For Products That Match The Price Range
query_expression = (
ProductDocument.price >= float(10) & ProductDocument.price <= float(100)
)

# Search for Products that include following Categories
query_expression = ProductDocument.category.name << ["category1", "category2"]

# Search for Products that include following Tags
query_expression = ProductDocument.tags.name << ["tag1", "tag2"]

# Query expression can be passed on the `find` method
result = ProductDocument.find(query_expression).sort_by("-price").execute()
```

For more details checkout [redis-om docs](https://github.com/redis/redis-om-python/blob/main/docs/getting_started.md)

### RediSearch Aggregation / Faceted Search

`redis-om` does not support faceted search (RediSearch Aggregation). So this package uses `redis-py` to do faceted search.

#### Example

```python
from redis.commands.search import reducers

from .documents import ProductDocument

query_expression = (
ProductDocument.name % "Some search query"
| ProductDocument.description % "Some search query"
)

# First we need to build the aggregation request
request1 = ProductDocument.build_aggregate_request(query_expression)
request2 = ProductDocument.build_aggregate_request(query_expression)

# Get the number of products for each category
ProductDocument.aggregate(
request1.group_by(
["@category_name"],
reducers.count().alias("count"),
)
)
# >> [{"category_name": "Shoes", "count": "112"}, {"category_name": "Cloths", "count": "200"}]

# Get the number of products for each tag
ProductDocument.aggregate(
request2.group_by(
["@tags_name"],
reducers.count().alias("count"),
)
)
# >> [{"tags_name": "Blue", "count": "14"}, {"tags_name": "Small", "count": "57"}]
```

For more details checkout [redis-py docs](https://redis.readthedocs.io/en/stable/examples/search_json_examples.html?highlight=aggregate#Aggregation) and
[RediSearch Aggregation docs](https://redis.io/docs/stack/search/reference/aggregations/)

### Settings

#### Environment Variables

- **`REDIS_OM_URL`** (Default: `redis://localhost:6379`): This environment variable follows the `redis-py` URL format. If you are using external redis server
You need to set this variable with the URL of the redis server following this pattern: `redis://[[username]:[password]]@[host]:[post]/[database number]`

**Example:** `redis://redis_user:[email protected]:6379/0`

For more details checkout [redis-om docs](https://github.com/redis/redis-om-python/blob/main/docs/getting_started.md#setting-the-redis-url-environment-variable)

#### Django Document Options

You can add these options on the `Django` class of each Document class:

```python
# documents.py

from redis_search_django.documents import JsonDocument

from .models import Category, Product, Tag, Vendor

class ProductDocument(JsonDocument):
class Django:
model = Product
fields = ["name", "description", "price", "created_at"]
select_related_fields = ["vendor", "category"]
prefetch_related_fields = ["tags"]
auto_index = True
related_models = {
Vendor: {
"related_name": "product",
"many": False,
},
Category: {
"related_name": "product_set",
"many": True,
},
Tag: {
"related_name": "product_set",
"many": True,
},
}
```

- **`model`** (Required): Django Model class to index.
- **`auto_index`** (Default: `True`, Optional): If True, the model instances will be indexed on create/update/delete.
- **`fields`** (Default: `[]`, Optional): List of model fields to index. (Do not add `OneToOneField`, `ForeignKey` or `ManyToManyField` here. These need to be explicitly added to the Document class using `EmbeddedJsonDocument`.)
- **`select_related_fields`** (Default: `[]`, Optional): List of fields to use on `queryset.select_related()`.
- **`prefetch_related_fields`** (Default: `[]`, Optional): List of fields to use on `queryset.prefetch_related()`.
- **`related_models`** (Default: `{}`, Optional): Dictionary of related models.
You need to specify the fields `related_name` and if it is a `ManyToManyField` or a `ForeignKey` Field then specify `"many": True`.
These are used to update the document data if any of the related model instances are updated.
`related_models` will be used when a related object is saved/added/removed/deleted that contributes to the document.

For `redis-om` specific options checkout [redis-om docs](https://github.com/redis/redis-om-python/blob/main/docs/models.md)

#### Global Options

You can add these options to your Django `settings.py` File:

- **`REDIS_SEARCH_AUTO_INDEX`** (Default: `True`): Enable or Disable Auto Index when model instance is created/updated/deleted for all document classes.

# Example Application Screenshot

![RediSearch Django](https://user-images.githubusercontent.com/24854406/185760315-4e12d02b-68a2-499a-a6d6-88d8162b5447.png)

# License

The code in this project is released under the [MIT License](LICENSE).