Elasticsearch:如何在 Django 中使用 Elasticsearch

1,419 阅读9分钟

你是否正在构建需要搜索海量数据集的 Django 应用程序? 你可能正在考虑使用标准关系数据库。 但是你很快就会发现,在处理高级需求时,此解决方案可能会很慢且有问题。 这就是 Elasticsearch 的用武之地。

这是 Django 的 Elasticsearch 教程,可帮助你在项目中充分利用这个方便的搜索引擎。

为什么要使用 Elasticsearch?

在海量数据库上运行搜索引擎的应用程序经常面临这个问题:检索产品信息的时间太长。这反过来又会导致糟糕的用户体验,从而对应用程序作为数字产品的潜力产生负面影响。

大多数情况下,搜索滞后源于开发团队用于构建应用程序的关系数据库,其中数据分散在许多不同的表中。为了检索有意义的信息,系统需要从这些表中获取数据——这可能比某些用户准备等待的时间更长。

在通过数据库查询检索数据和获取搜索结果时,关系数据库的工作速度可能非常缓慢。这就是为什么开发人员一直忙于寻找可以加速数据检索过程的替代方法。

这就是 Elasticsearch 的用武之地。它是一个 NoSQL 分布式数据存储,适用于灵活的面向文档的数据库,以匹配具有实时参与需求的苛刻工作负载应用程序。

如果你对 Elasticsearch 还不是很了解的话,请参阅我之前的文章:

更多关于 Elastic Stack 方面的知识,请阅读 “Elastic:菜鸟上手指南”。

在 Django 中使用 Elasticsearch

为了在 Django 中利用 Elasticsearch,我们将使用一些非常有用的包:

  • Elasticsearch DSL - 一个高级库,可帮助编写和运行针对 Elasticsearch 的查询。 它建立在官方低级客户端(elasticsearch-py)之上。
  • Django Elasticsearch DSL – 一个允许轻松集成和配置 Elasticsearch 与 Django 的包。 它是围绕 elasticsearch-dsl-py 构建的薄包装器,因此您可以使用由 elasticsearch-dsl-py 团队开发的所有功能。
  • Django Elasticsearch DSL DRF – 集成了 Elasticsearch DSL 和 Django REST 框架。 它为我们提供了API 来最有效地访问 Elasticsearch。

Haystack 与 Elasticsearch DSL

Haystack 是一个很棒的开源包,它为 Django 提供模块化搜索。 不幸的是,它不完全支持最新版本的 Elasticsearch 或更复杂的查询。 此外,配置是最小的并且受到高度限制。

为了方便大家对本教程的理解,我把最终的代码放入到 github 中供大家参考:github.com/liu-xiao-gu…

安装

在做下面的练习之前,我们需要安装一些必要的软件:

Elasticsearch

我们可以参考文章 “如何在 Linux,MacOS 及 Windows 上进行安装 Elasticsearch” 来安装自己的 Elasticsearch。我可以直接在 http://localhost:9200 上进行访问。

Kibana

我们可以参考文章 “Kibana:如何在 Linux,MacOS 及 Windows上安装 Elastic 栈中的 Kibana” 来安装自己的 Kibana。我们可以在 http://localhost:5601 上访问自己的 Kibana。

安装 Python

我们根据自己的操作系统安装相应的 Python 及最新的版本。我们接着创建一个自己的 django 项目。我们首先创建一个叫做 django_elastic 的目录:

$ mkdir django_elastic
$ cd django_elastic

我们进入到该目录,并使用如下的命令来创建一个 virtualenv:

virtualenv -p python3 .
$ virtualenv -p python3 .
created virtual environment CPython3.8.5.final.0-64 in 648ms
  creator CPython3Posix(dest=/Users/liuxg/python/django_elastic, clear=False, no_vcs_ignore=False, global=False)
  seeder FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/Users/liuxg/Library/Application Support/virtualenv)
    added seed packages: pip==21.2.4, setuptools==57.4.0, wheel==0.37.0
  activators BashActivator,CShellActivator,FishActivator,PowerShellActivator,PythonActivator
$ ls
bin        lib        pyvenv.cfg

我们使用如下的命令来进入到该环境中:

source ./bin/activate
$ source ./bin/activate
(django_elastic) $ 

在这个  virtualenv 我们使用如下的命令来安装 django:

pip3 install django

我们接着使用如下的命令来创建一个叫做 elastic 的项目:

django-admin startproject elastic .
(django_elastic) $ django-admin startproject elastic .
(django_elastic) $ ls
bin        elastic    lib        manage.py  pyvenv.cfg

这样我们就创建了一个叫做 elastic 的项目。接下来,我们创建一个叫做 news 的 app:

python manage.py startapp news
(django_elastic) $ python manage.py startapp news
(django_elastic) $ ls
bin        elastic    lib        manage.py  news       pyvenv.cfg

从上面,我们可以看出来,我们已经创建了一个叫做 news 的应用。所有的 app 的文件将被置于 news 这个目录中。

由于一些原因,我们需要在命令行中打入如下的命令才可以使得像 django-admin makemigrations 的命令得以顺利的执行:

(django_elastic) $ export PYTHONPATH=.
(django_elastic) $ export DJANGO_SETTINGS_MODULE='elastic.settings'

请注意上面的 elastic.settings 中的 "elastic" 是项目的名称。我们可以在 manage.py 中找到相应的字符串。经过上面的修改后,我们可以使得如下的命令能够顺利地执行:

(django_elastic) $ django-admin makemigrations 
No changes detected

我们可以使用我们喜欢的编辑器来修改我们的项目。针对我的情况,我可以使用 code 来编辑我们的项目:

code .

通过上面的步骤,我们创建了一个最为基本的 django 项目。我们会在以后的练习中来对这个项目进行相应的修改。

安装本地 JSON server

为了能够在 django 中写入一些文档到 Elasticsearch 中,我仿照文章 “json-server模拟后端接口” 来创建一个本地的 JSON 色server。它的目的是为了让 django 的代码读取这个接口并写入到 Elasticsearch 中。我们可以仿照文章中的步骤,并编辑相应的 db.json 文件如下:

db.json

{
  "news": [
    {
       "id":1,
       "title":"Tesla's Autopilot, other driver assists are in a regulatory grey zone",
       "content":"Filed under: Government/Legal,Tesla,Safety,Technology Continue reading Tesla's Autopilot, other driver assists are in a regulatory grey zone Tesla's Autopilot, other driver assists are in a regulatory grey zone originally appeared on Autoblog on Sat, 24 Apr"
    },
    {
       "id":2,
       "title":"‘A Bunch of People Will Probably Die’ at Onset of Mars Colonisation, Musk Cautions",
       "content":"The Silicone Valley mogul has candidly outlined the pitfalls of potential trips to the red planet, as his brainchild SpaceX has been going to great lengths to make commercial tourism on Mars possible one day."
    },
    {
       "id":3,
       "title":"IRS says to do this one thing to get the biggest possible tax refund",
       "content":"In a normal year, one in which the country wasn't dealing with the catastrophic public health and financial impacts of a global pandemic, tax season would..."
    },
   ...
  ]
}

等我们安装好后,我们运行 json server:

我们可以在浏览器中访问该本地服务器:

 在上面,我们可以看到一个 JSON 输出格式的相应。我们共有 40 个文档。

在 Django 中使用 Elasticsearch

在这节中,我将展示如何在 Django 中使用 Elasticsearch。我们接着上面已经创建的 django 项目继续修改。

安装应用及相应的包

我们首先打开项目的 settings.py 文件:

elastic/settings.py

INSTALLED_APPS = [    'django.contrib.admin',    'django.contrib.auth',    'django.contrib.contenttypes',    'django.contrib.sessions',    'django.contrib.messages',    'django.contrib.staticfiles',    'news',    'rest_framework',    'django_elasticsearch_dsl',    'django_elasticsearch_dsl_drf',    ]

ELASTICSEARCH_DSL = {
    'default': {
        'hosts': 'localhost:9200'
    },
}

在上面,我们添加了如下的应用:

    'news',
    'rest_framework',
    'django_elasticsearch_dsl',
    'django_elasticsearch_dsl_drf',    

 我们也同时定义了 ELASTICSEARCH_DSL。你的 Elasticsearch 的地址如有不同,你需要做相应的调整。当然我们也需要安装相应的 python 包。为了方便大家安装,我们可以在项目的根目录下创建如下的一个 requirements.txt 文件:

requirements.txt

asgiref==3.3.4
certifi==2020.12.5
chardet==4.0.0
Django==3.2
django-cors-headers==3.7.0
django-debug-toolbar==3.2.1
django-discover-runner==1.0
django-elasticsearch-dsl==7.2.0
django-elasticsearch-dsl-drf==0.22
django-froala-editor==3.2.2
django-ipware==3.0.2
django-nine==0.2.4
django-redis==4.12.1
django-rest-elasticsearch==0.4.2
django-role-permissions==3.1.1
django-webpack-loader==0.7.0
djangorestframework==3.12.4
elasticsearch==7.12.0
elasticsearch-dsl==7.3.0
Faker==8.1.0
idna==2.10
ipaddress==1.0.23
Pillow==8.2.0
python-dateutil==2.8.1
pytz==2021.1
redis==3.5.3
requests==2.25.1
six==1.15.0
sqlparse==0.4.1
text-unidecode==1.3
typing-extensions==3.7.4.3
urllib3==1.26.4

我们可以使用如下的命令来安装所需要的包:

pip3 install -r requirements.txt

创建 model

接下来,我们来修改在 news app 下的 models.py:

news/models.py

from django.db import models

# Create your models here.
class ElasticNews(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()

添加 model 到管理页面

我们在 news app 的 admin.py 中进行如下的修改:

news/admin.py

from django.contrib import admin

# Register your models here.
from news.models import *


admin.site.register(ElasticNews)

创建 documents.py

我们在 news app 下创建一个叫做 documents.py 的文件:

news/documents.py

from django_elasticsearch_dsl import (
    Document ,
    fields,
    Index,
)

from .models import ElasticNews
PUBLISHER_INDEX = Index('elastic_news')

PUBLISHER_INDEX.settings(
    number_of_shards=1,
    number_of_replicas=0
)

@PUBLISHER_INDEX.doc_type
class NewsDocument(Document):

    id = fields.IntegerField(attr='id')
    fielddata=True
    title = fields.TextField(
        fields={
            'keyword':{
                'type': 'keyword',
            }
            
        }
    )
    content = fields.TextField(
        fields={
            'keyword': {
                'type': 'keyword',
                
            }
        },
    )
   

    class Django(object):
        model = ElasticNews

在上面我们定义了文档的 mapping。

创建 serializers.py

我们在 News app 下创建一个叫做 serializsers.py 的文件:

news/serializers.py

import json
from .models import ElasticNews

from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
from .documents import *


class NewsDocumentSerializer(DocumentSerializer):

    class Meta(object):
        """Meta options."""
        model = ElasticNews
        document = NewsDocument
        fields = (
            'title',
            'content',
        )
        def get_location(self, obj):
            """Represent location value."""
            try:
                return obj.location.to_dict()
            except:
                return {}

创建 index view

接下来,我们来修改在 News app 下的 views.py 文件,以便能在访问页面时生产相应的 Elasticsearch 文档:

news/views.py

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here.
from django.http import JsonResponse
import requests
import json
from news.models import *

from .documents import *
from .serializers import *

from django_elasticsearch_dsl_drf.filter_backends import (
    FilteringFilterBackend,
    CompoundSearchFilterBackend
)
from django_elasticsearch_dsl_drf.viewsets import DocumentViewSet
from django_elasticsearch_dsl_drf.filter_backends import (
    FilteringFilterBackend,
    OrderingFilterBackend,
)

def generate_random_data():
    url = 'http://localhost:3000/news'
    r = requests.get(url)
    payload = json.loads(r.text)
    count = 1
    
    # print (payload)
    print ("type of payload is: ", type(payload))
    for data in payload:
        # print("title: ", data['title'])
        # print("content: ", data['content'])
        ElasticNews.objects.create(
            title = data['title'],
            content = data['content']
        )

def index(request):
    generate_random_data()
    return JsonResponse({'status' : 200})
    # return HttpResponse("Hello, the world")


class PublisherDocumentView(DocumentViewSet):
    document = NewsDocument
    serializer_class = NewsDocumentSerializer
    lookup_field = 'first_name'
    fielddata=True
    filter_backends = [
        FilteringFilterBackend,
        OrderingFilterBackend,
        CompoundSearchFilterBackend,
    ]
   
    search_fields = (
        'title',
        'content',
    )
    multi_match_search_fields = (
       'title',
        'content',
    )
    filter_fields = {
       'title' : 'title',
        'content' : 'content',
    }
    ordering_fields = {
        'id': None,
    }
    ordering = ( 'id'  ,)

在上面有两个 view。第一个 view 是在访问主页时自动向本地的服务器发送一个请求,并得到 JSON 文档。最终写入到 Elasticsearch 中。第二个 view 是用来读数据进行搜索并排序的一个 view。

修改 ulrpatterns

到目前为止我们的应用基本已经创建完毕。我们接下来的工作就是来把上面创建的 view 和 ulrpattern 对应起来。我们修改 elastic 项目下的 urls.py 如下:

elastic/urls.py

"""elastic URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  path('', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.urls import include, path
    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from home.views import *

urlpatterns = [
    path('' , index),
    path('search/' , PublisherDocumentView.as_view({'get': 'list'})),
    path('admin/', admin.site.urls),
]

运行应用

到目前为止,我们已经完成了整个应用的设计。接下来,我们使用如下的命令来创建 Elasticsearch 索引 mapping:

django-admin makemigrations
django-admin migrate
python manage.py search_index — rebuild

 运行我们的应用:

python manage.py runserver

我们在浏览器中方为 http://localhost:8000 :

按照我们的设计,它将为 elastic_news 索引创建一些文档。我们可以在 Kibana 中进行查看:

GET elastic_news/_search

 我们可以看到有 80 个文档。这是因为我们运行接口两次的缘故。每次都会写入 40个。这和我们之前在 jsonserver 中看到的是一样的。

我们使用 django 中的第二个 view 来进行搜索。我们在浏览器中打开 :http://localhost:8000/search/

我们可以看到 40 个文档。我们也可以对我们的索引进行如下的搜索:

http://localhost:8000/search/?search=Tesla

如上所示,这次我们只看到很少的几个文档。我们看到有重复的搜索结果,这是因为在 index 接口我们摄入了两组同样的文档的缘故。

我们可以在 Kibana 中查看我们的文档的 mapping:

GET elastic_news/_mapping

显然这个和我们之前在 documents.py 中定义的是一致的。 

总结

我希望本指南可以帮助您在 Django 中使用 Elasticsearch。如你所见,Elasticsearch 并不像看起来那么可怕。 借助 REST API,每个 Web 开发人员都可以快速熟悉此解决方案。 该方案的构建类似于数据库概念,便于理解什么是节点、索引、类型和文档。

参考

【1】 How to Use ElasticSearch With Django

【2】sunscrapers.com/blog/how-to…