🎉

DjangoとAWS OpenSearchを接続する

2023/06/19に公開

概要

DjangoとAWS OpenSearchを接続する方法に関するメモです。以下の記事が参考になりました。

https://testdriven.io/blog/django-drf-elasticsearch/

ただし、上記の記事はElasticsearchを対象にした設定のため、OpenSearchに応じた変更が必要です。

変更点

以下のElasticsearch Setupの部分から、OpenSearchに応じた変更が必要でした。

https://testdriven.io/blog/django-drf-elasticsearch/#elasticsearch-setup

具体的には、以下の2つのライブラリが必要でした。

(env)$ pip install opensearch-py
(env)$ pip install django-opensearch-dsl

その後は、django_elasticsearch_dslとなっている箇所をdjango-opensearch-dslに、elasticsearch_dslopensearchpyに書き換えることで、記事の通りに進めることができました。

例えば、以下のような形です。

blog/documents.py
# blog/documents.py

from django.contrib.auth.models import User
from django_opensearch_dsl import Document, fields # opensearchに変更
from django_opensearch_dsl.registries import registry # opensearchに変更

from blog.models import Category, Article


@registry.register_document
class UserDocument(Document):
    class Index:
        name = 'users'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
        }

    class Django:
        model = User
        fields = [
            'id',
            'first_name',
            'last_name',
            'username',
        ]


@registry.register_document
class CategoryDocument(Document):
    id = fields.IntegerField()

    class Index:
        name = 'categories'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
        }

    class Django:
        model = Category
        fields = [
            'name',
            'description',
        ]


@registry.register_document
class ArticleDocument(Document):
    author = fields.ObjectField(properties={
        'id': fields.IntegerField(),
        'first_name': fields.TextField(),
        'last_name': fields.TextField(),
        'username': fields.TextField(),
    })
    categories = fields.ObjectField(properties={
        'id': fields.IntegerField(),
        'name': fields.TextField(),
        'description': fields.TextField(),
    })
    type = fields.TextField(attr='type_to_string')

    class Index:
        name = 'articles'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
        }

    class Django:
        model = Article
        fields = [
            'title',
            'content',
            'created_datetime',
            'updated_datetime',
        ]

Populate Elasticsearch

Elasticsearchを対象にした上記の記事では、以下のコマンドが紹介されています。

python manage.py search_index --rebuild

一方、OpenSearchの場合は、以下のコマンドが必要でした。

インデックスの作成

python manage.py opensearch index create
The following indices will be created:
        - users.
        - categories.
        - articles.

Continue ? [y]es [n]o : y

Creating index 'users'... OK
Creating index 'categories'... OK
Creating index 'articles'... OK

ドキュメントの登録

python3 manage.py opensearch document index
The following documents will be indexed:
        - 5 User.
        - 3 Category.
        - 5 Article.

Continue ? [y]es [n]o : y

Indexing 5 User: OK          
Indexing 3 Category: OK          
Indexing 5 Article: OK          

5 User successfully indexed, 0 errors:
3 Category successfully indexed, 0 errors:
5 Article successfully indexed, 0 errors:

インデックスのリビルド

python manage.py opensearch index rebuild

その他:analyzerとfieldsの追加

以下の部分に記載されているField Classesを試します。

https://django-opensearch-dsl.readthedocs.io/en/latest/fields/#field-classes

以下の例では、usernameについて、html_stripというanalyzerと、Keywordフィールドを設定しています。

blog/documents.py
# blog/documents.py

from django.contrib.auth.models import User
from django_opensearch_dsl import Document, fields
from django_opensearch_dsl.registries import registry

from blog.models import Category, Article

from opensearchpy import analyzer, tokenizer

html_strip = analyzer(
    'html_strip',
    tokenizer="standard",
    filter=["lowercase", "stop", "snowball"],
    char_filter=["html_strip"]
)

@registry.register_document
class UserDocument(Document):

    username = fields.TextField(
        analyzer=html_strip,
        fields={'raw': fields.KeywordField()}
    )

    class Index:
        name = 'users'
        settings = {
            'number_of_shards': 1,
            'number_of_replicas': 0,
        }

    class Django:
        model = User
        fields = [
            'id',
            'first_name',
            'last_name',
            # 'username',
        ]

上記の結果、以下のようなマッピングがOpenSearchに登録されました。

{
  "users" : {
    "mappings" : {
      "properties" : {
        "first_name" : {
          "type" : "text"
        },
        "id" : {
          "type" : "integer"
        },
        "last_name" : {
          "type" : "text"
        },
        "username" : {
          "type" : "text",
          "fields" : {
            "raw" : {
              "type" : "keyword"
            }
          },
          "analyzer" : "html_strip"
        }
      }
    }
  }

username.rawを使用することで、ソートやアグリゲーションが可能となります。以下がviewsの例です。-を与えることで、降順になるようです。

search/views.py
# search/views.py

import abc

from django.http import HttpResponse
from opensearchpy import Q

from rest_framework.pagination import LimitOffsetPagination
from rest_framework.views import APIView

from blog.documents import ArticleDocument, UserDocument, CategoryDocument
from blog.serializers import ArticleSerializer, UserSerializer, CategorySerializer

class PaginatedOpenSearchAPIView(APIView, LimitOffsetPagination):
    serializer_class = None
    document_class = None

    @abc.abstractmethod
    def generate_q_expression(self, query):
        """This method should be overridden
        and return a Q() expression."""

    def get(self, request, query):
        try:
            q = self.generate_q_expression(query)
            search = self.document_class.search().query(q).sort(
                "-username.raw"
            )
            response = search.execute()

            print(
                f'*** Found {response.hits.total.value} hit(s) for query: "{query}"')

            results = self.paginate_queryset(response, request, view=self)
            serializer = self.serializer_class(results, many=True)
            return self.get_paginated_response(serializer.data)
        except Exception as e:
            print(e)
            return HttpResponse(e, status=500)

まとめ

DjangoとAWS OpenSearchの接続にあたり、参考になりましたら幸いです。

Discussion