text: #  Licensed to Elasticsearch B.V. under one or more contributor
  #  license agreements. See the NOTICE file distributed with
  #  this work for additional information regarding copyright
  #  ownership. Elasticsearch B.V. licenses this file to you under
  #  the Apache License, Version 2.0 (the "License"); you may
  #  not use this file except in compliance with the License.
  #  You may obtain a copy of the License at
  #
  # 	http://www.apache.org/licenses/LICENSE-2.0
  #
  #  Unless required by applicable law or agreed to in writing,
  #  software distributed under the License is distributed on an
  #  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  #  KIND, either express or implied.  See the License for the
  #  specific language governing permissions and limitations
  #  under the License.
  
  """
  Complex data model example modeling stackoverflow-like data.
  
  It is used to showcase several key features of elasticsearch-dsl:
  
      * Object and Nested fields: see User and Comment classes and fields they
        are used in
  
          * method add_comment is used to add comments
  
      * Parent/Child relationship
  
          * See the Join field on Post creating the relationship between Question
            and Answer
  
          * Meta.matches allows the hits from same index to be wrapped in proper
            classes
  
          * to see how child objects are created see Question.add_answer
  
          * Question.search_answers shows how to query for children of a
            particular parent
  
  """
  from datetime import datetime
  
  from elasticsearch_dsl import (
      Boolean,
      Date,
      Document,
      InnerDoc,
      Join,
      Keyword,
      Long,
      Nested,
      Object,
      Text,
      connections,
  )
  
  
  class User(InnerDoc):
      """
      Class used to represent a denormalized user stored on other objects.
      """
  
      id = Long(required=True)
      signed_up = Date()
      username = Text(fields={"keyword": Keyword()}, required=True)
      email = Text(fields={"keyword": Keyword()})
      location = Text(fields={"keyword": Keyword()})
  
  
  class Comment(InnerDoc):
      """
      Class wrapper for nested comment objects.
      """
  
      author = Object(User, required=True)
      created = Date(required=True)
      content = Text(required=True)
  
  
  class Post(Document):
      """
      Base class for Question and Answer containing the common fields.
      """
  
      author = Object(User, required=True)
      created = Date(required=True)
      body = Text(required=True)
      comments = Nested(Comment)
      question_answer = Join(relations={"question": "answer"})
  
      @classmethod
      def _matches(cls, hit):
          # Post is an abstract class, make sure it never gets used for
          # deserialization
          return False
  
      class Index:
          name = "test-qa-site"
          settings = {
              "number_of_shards": 1,
              "number_of_replicas": 0,
          }
  
      def add_comment(self, user, content, created=None, commit=True):
          c = Comment(author=user, content=content, created=created or datetime.now())
          self.comments.append(c)
          if commit:
              self.save()
          return c
  
      def save(self, **kwargs):
          # if there is no date, use now
          if self.created is None:
              self.created = datetime.now()
          return super(Post, self).save(**kwargs)
  
  
  class Question(Post):
      # use multi True so that .tags will return empty list if not present
      tags = Keyword(multi=True)
      title = Text(fields={"keyword": Keyword()})
  
      @classmethod
      def _matches(cls, hit):
          """Use Question class for parent documents"""
          return hit["_source"]["question_answer"] == "question"
  
      @classmethod
      def search(cls, **kwargs):
          return cls._index.search(**kwargs).filter("term", question_answer="question")
  
      def add_answer(self, user, body, created=None, accepted=False, commit=True):
          answer = Answer(
              # required make sure the answer is stored in the same shard
              _routing=self.meta.id,
              # since we don't have explicit index, ensure same index as self
              _index=self.meta.index,
              # set up the parent/child mapping
              question_answer={"name": "answer", "parent": self.meta.id},
              # pass in the field values
              author=user,
              created=created,
              body=body,
              accepted=accepted,
          )
          if commit:
              answer.save()
          return answer
  
      def search_answers(self):
          # search only our index
          s = Answer.search()
          # filter for answers belonging to us
          s = s.filter("parent_id", type="answer", id=self.meta.id)
          # add routing to only go to specific shard
          s = s.params(routing=self.meta.id)
          return s
  
      def get_answers(self):
          """
          Get answers either from inner_hits already present or by searching
          elasticsearch.
          """
          if "inner_hits" in self.meta and "answer" in self.meta.inner_hits:
              return self.meta.inner_hits.answer.hits
          return list(self.search_answers())
  
      def save(self, **kwargs):
          self.question_answer = "question"
          return super(Question, self).save(**kwargs)
  
  
  class Answer(Post):
      is_accepted = Boolean()
  
      @classmethod
      def _matches(cls, hit):
          """Use Answer class for child documents with child name 'answer'"""
          return (
              isinstance(hit["_source"]["question_answer"], dict)
              and hit["_source"]["question_answer"].get("name") == "answer"
          )
  
      @classmethod
      def search(cls, **kwargs):
          return cls._index.search(**kwargs).exclude("term", question_answer="question")
  
      @property
      def question(self):
          # cache question in self.meta
          # any attributes set on self would be interpretted as fields
          if "question" not in self.meta:
              self.meta.question = Question.get(
                  id=self.question_answer.parent, index=self.meta.index
              )
          return self.meta.question
  
      def save(self, **kwargs):
          # set routing to parents id automatically
          self.meta.routing = self.question_answer.parent
          return super(Answer, self).save(**kwargs)
  
  
  def setup():
      """Create an IndexTemplate and save it into elasticsearch."""
      index_template = Post._index.as_template("base")
      index_template.save()
  
  
  if __name__ == "__main__":
      # initiate the default connection to elasticsearch
      connections.create_connection()
  
      # create index
      setup()
  
      # user objects to use
      nick = User(
          id=47,
          signed_up=datetime(2017, 4, 3),
          username="fxdgear",
          email="nick.lang@elastic.co",
          location="Colorado",
      )
      honza = User(
          id=42,
          signed_up=datetime(2013, 4, 3),
          username="honzakral",
          email="honza@elastic.co",
          location="Prague",
      )
  
      # create a question object
      question = Question(
          _id=1,
          author=nick,
          tags=["elasticsearch", "python"],
          title="How do I use elasticsearch from Python?",
          body="""
          I want to use elasticsearch, how do I do it from Python?
          """,
      )
      question.save()
      answer = question.add_answer(honza, "Just use `elasticsearch-py`!")
  