» Python: Building Full-Text Search API with ElasticSearch » 2. Index Documents » 2.3 Send Index Requests

Send Index Requests

Let's add the API for indexing books, then we can put in some test data for later use.

Move domain/model to books/domain/model.

Create books/domain/gateway/book_manager.py:

from abc import ABC, abstractmethod

from ..model import Book

class BookManager(ABC):
    def index_book(self, b: Book) -> str:

Again, we‘re using 4 Layers Architecture Pattern. It will pay off when your project grows bigger and bigger. Read more here.

Add books/domain/gateway/__init__.py:

from .book_manager import BookManager

Create books/infrastructure/search/es.py:

from dataclasses import asdict

from elasticsearch import Elasticsearch

from ...domain.gateway import BookManager
from ...domain.model import Book

INDEX_BOOK = "book_idx"

class ElasticSearchEngine(BookManager):
    def __init__(self, address: str, page_size: int):
        self.page_size = page_size
        self.client = Elasticsearch(address)

    def index_book(self, b: Book) -> str:
        result = self.client.index(index=INDEX_BOOK, document=asdict(b))
        return result['_id']

By default, Elasticsearch allows you to index documents into an index that doesn‘t exist yet. When you index a document into a non-existent index, Elasticsearch will dynamically create the index for you with default settings. This can be convenient for certain use cases where you don‘t want to manage index creation explicitly.

Install yaml dependencies:

pip3 install PyYAML

Create books/infrastructure/config/config.py:

from dataclasses import dataclass
import yaml

class SearchConfig:
    address: str

class ApplicationConfig:
    page_size: int

class Config:
    app: ApplicationConfig
    search: SearchConfig

def parseConfig(filename: str) -> Config:
    with open(filename, 'r') as f:
        data = yaml.safe_load(f)
        return Config(

Add books/infrastructure/config/__init__.py:

from .config import Config, parseConfig

Create config.yml:

  page_size: 10
  address: "http://localhost:9200"

Caution: Do not directly track your config.yml file with Git, as this could potentially leak sensitive data. If necessary, only track the template of the configuration file.

  page_size: 10
  address: ""

Create books/application/executor/book_operator.py:

from ...domain.model import Book
from ...domain.gateway import BookManager

class BookOperator():

    def __init__(self, book_manager: BookManager):
        self.book_manager = book_manager

    def create_book(self, b: Book) -> str:
        return self.book_manager.index_book(b)

Remember to create __init__.py files for all the sub-folders.

Create books/application/wire_helper.py:

from ..domain.gateway import BookManager
from ..infrastructure.config import Config
from ..infrastructure.search import ElasticSearchEngine

class WireHelper:
    def __init__(self, engine: ElasticSearchEngine):
        self.engine = engine

    def new(cls, c: Config):
        es = ElasticSearchEngine(c.search.address, c.app.page_size)
        return cls(es)

    def book_manager(self) -> BookManager:
        return self.engine

Create books/adapter/router.py:

import logging

from fastapi import FastAPI, HTTPException

from ..application.executor import BookOperator
from ..application import WireHelper
from ..domain.model import Book

class RestHandler:
    def __init__(self, logger: logging.Logger, book_operator: BookOperator):
        self._logger = logger
        self.book_operator = book_operator

    def create_book(self, b: Book):
            return self.book_operator.create_book(b)
        except Exception as e:
            self._logger.error(f"Failed to create: {e}")
            raise HTTPException(status_code=400, detail="Failed to create")

def make_router(app: FastAPI, wire_helper: WireHelper):
    rest_handler = RestHandler(

    async def welcome():
        return {"status": "ok"}

    async def create_book(b: Book):
        book_id = rest_handler.create_book(b)
        return {"id": book_id}

Replace main.py with the following code:

from fastapi import FastAPI

from books.adapter import make_router
from books.application import WireHelper
from books.infrastructure.config import parseConfig

CONFIG_FILENAME = "config.yml"

c = parseConfig(CONFIG_FILENAME)
wire_helper = WireHelper.new(c)
app = FastAPI()
make_router(app, wire_helper)

Run the server again and try it with curl:

uvicorn web.main:app --reload

Example request:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Da Vinci Code","author":"Dan Brown","published_at":"2003-03-18","content":"In the Louvre, a curator is found dead. Next to his body, an enigmatic message. It is the beginning of a race to discover the truth about the Holy Grail."}' \

Example response:


Put in more test data

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"Harry Potter and the Philosopher\u0027s Stone","author":"J.K. Rowling","published_at":"1997-06-26","content":"A young boy discovers he is a wizard and begins his education at Hogwarts School of Witchcraft and Wizardry, where he uncovers the mystery of the Philosopher‘s Stone."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"To Kill a Mockingbird","author":"Harper Lee","published_at":"1960-07-11","content":"Set in the American South during the Great Depression, the novel explores themes of racial injustice and moral growth through the eyes of young Scout Finch."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Lord of the Rings","author":"J.R.R. Tolkien","published_at":"1954-07-29","content":"A hobbit named Frodo Baggins embarks on a perilous journey to destroy a powerful ring and save Middle-earth from the Dark Lord Sauron."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Catcher in the Rye","author":"J.D. Salinger","published_at":"1951-07-16","content":"Holden Caulfield narrates his experiences in New York City after being expelled from prep school, grappling with themes of alienation, identity, and innocence."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Alchemist","author":"Paulo Coelho","published_at":"1988-01-01","content":"Santiago, a shepherd boy, travels from Spain to Egypt in search of a treasure buried near the Pyramids. Along the way, he learns about the importance of following one‘s dreams."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Hunger Games","author":"Suzanne Collins","published_at":"2008-09-14","content":"In a dystopian future, teenagers are forced to participate in a televised death match called the Hunger Games. Katniss Everdeen volunteers to take her sister‘s place and becomes a symbol of rebellion."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"1984","author":"George Orwell","published_at":"1949-06-08","content":"Winston Smith lives in a totalitarian society ruled by the Party led by Big Brother. He rebels against the oppressive regime but ultimately succumbs to its control."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"The Girl with the Dragon Tattoo","author":"Stieg Larsson","published_at":"2005-08-01","content":"Journalist Mikael Blomkvist and hacker Lisbeth Salander investigate the disappearance of a young woman from a wealthy family, uncovering dark secrets and corruption."}' \

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"title":"Gone Girl","author":"Gillian Flynn","published_at":"2012-06-05","content":"On their fifth wedding anniversary, Nick Dunne‘s wife, Amy, disappears. As the media circus ensues and suspicions mount, Nick finds himself in a whirlwind of deception and betrayal."}' \