The what and why of Python and Elasticsearch

@honzakral

Elasticsearch

HTTP Rest

import requests

ES_URL = 'http://localhost:9200'

requests.put(ES_URL + '/i/t/42', json={"title": "PyCon Belarus"})

query = {"query": {"match": {"title": "pycon"}}}

data = requests.get(ES_URL + '/i/_search', json=query).json()

I know http!

well, actually...

Rich API

Distributed

Official Client(s)

  • maintained
  • full API support
  • load balancing
  • node failures
  • different transports
  • no opinions

No Opinions?!

"Nobody should have a reason not to use the official client."

1-to-1 mapping


response = client.search(
  index="my-index",
  body={
    "query": {
      "bool": {
        "must": {"match": {"title": "python"}},
        "must_not": {"match": {"description": "beta"}}
      }
    }
  }
)
curl -X GET localhost:9200/my-index/_search -d '{
  "query": {
    "bool": {
      "must": {"match": {"title": "python"}},
      "must_not": {"match": {"description": "beta"}}
    }
  }
}'

Component architecture

from elasticsearch import Elasticsearch
from elasticsearch_async import AsyncTransport

client = Elasticsearch(transport_class=AsyncTransport)

Unified test suite

elasticsearch-py


response = client.search(
    index="my-index",
    body={
      "query": {
        "bool": {
          "must": [{"match": {"title": "python"}}],
          "must_not": [{"match": {"description": "beta"}}]
          "filter": [{"term": {"category": "search"}}]
        }
      },
      "aggs" : {
        "per_tag": {
          "terms": {"field": "tags"},
          "aggs": {
            "max_lines": {"max": {"field": "lines"}}
          }
        }
      }
    }
)

for hit in response['hits']['hits']:
    print(hit['_score'], hit['_source']['title'])

elasticsearch-dsl

s = Search(using=client, index="my-index")
# filter only search
s = s.filter("term", category="search")
# we want python in title
s = s.query("match", title="python")
# and no beta releases
s = s.query(~Q("match", description="beta"))

# aggregate on tags
s.aggs.bucket('per_tag', 'terms', field='tags')
# max lines per tag
s.aggs['per_tag'].metric('max_lines', 'max', field='lines')

Hide mechanics,

not meaning!

Mechanics


response = client.search(
    index="my-index",
    body={
      "query": {
        "bool": {
          "must": [{"match": {"title": "python"}}],
          "must_not": [{"match": {"description": "beta"}}]
          "filter": [{"term": {"category": "search"}}]
        }
      },
      "aggs" : {
        "per_tag": {
          "terms": {"field": "tags"},
          "aggs": {
            "max_lines": {"max": {"field": "lines"}}
          }
        }
      }
    }
)

for hit in response['hits']['hits']:
    print(hit['_score'], hit['_source']['title'])

Meaning

s = Search(using=client, index="my-index")
s = s.filter("term", category="search")
s = s.query("match", title="python")
s = s.query(~Q("match", description="beta"))

s.aggs.bucket('per_tag', 'terms', field='tags') \
    .metric('max_lines', 'max', field='lines')

for hit in s:
    print(hit.meta.score, hit.title)

Integrations

DocType

from datetime import date
from elasticsearch_dsl import DocType, Text, Keyword, Date

class BlogPost(DocType):
    title = Text(analyzer="english")
    body = Text(analyzer="english")

    published_date = Date()

    tags = Keyword(multi=True)

BlogPost.search('terms', tags=['python', 'belarus'])

bp = BlogPost(title='PyCon Belarus', published_date=date.today())
bp.tags.append('python')
bp.save()
  

Django Integration?

from django import models
from .search import BlogPost as BlogPostDoc

class BlogPost(models.Model):
    title = models.CharField(max_length=200)
    ...
    def to_search(self):
        return BlogPostDoc(
            _id=self.id,
            title=self.title,
            tags=[t.name for t in self.tags.all()],
            ...
        )


def update_search(instance, **kwargs):
    instance.to_search().save()

def remove_from_search(instance, **kwargs):
    instance.to_search().delete()

post_save.connect(update_search, sender=BlogPost)
pre_delete.connect(remove_from_search, sender=BlogPost)

Faceted Search

Faceted Search

from elasticsearch_dsl import FacetedSearch, TermsFacet, DateHistogramFacet

class BlogSearch(FacetedSearch):
    doc_types = [BlogPost, ]

    fields = ['tags', 'title', 'body']

    facets = {
        'tags': TermsFacet(field='tags'),
        'months': DateHistogramFacet(field='published_date', interval='month')
    }

bs = BlogSearch('python web', {'months': date(2015, 6)})

Future?

Thanks!

@honzakral

The What and Why of Python and Elasticsearch

By Honza Král

The What and Why of Python and Elasticsearch

  • 2,347