» Node.js: 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.

Create src/domain/gateway/book_manager.ts:

import { Book } from "../model";

export interface BookManager {
  indexBook(b: Book): Promise<string>;
}

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

Add src/domain/gateway/index.ts:

export { BookManager } from "./book_manager";

Create src/infrastructure/search/es.ts:

import { Client } from "@elastic/elasticsearch";

import { Book } from "../../domain/model";
import { BookManager } from "../../domain/gateway";

const INDEX_BOOK = "book_idx";

export class ElasticSearchEngine implements BookManager {
  private client: Client;
  private page_size: number;

  constructor(address: string, page_size: number) {
    this.page_size = page_size;
    this.client = new Client({ node: address });
  }

  async indexBook(b: Book): Promise<string> {
    const result = await this.client.index({
      index: INDEX_BOOK,
      document: 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.

Create src/infrastructure/config/config.ts:

import { readFileSync } from "fs";

interface SearchConfig {
  address: string;
}

interface ApplicationConfig {
  port: number;
  page_size: number;
}

export interface Config {
  app: ApplicationConfig;
  search: SearchConfig;
}

export function parseConfig(filename: string): Config {
  return JSON.parse(readFileSync(filename, "utf-8"));
}

Add src/infrastructure/config/index.ts:

export { Config, parseConfig } from "./config";

Create config.json:

{
  "app": {
    "port": 3000,
    "page_size": 10
  },
  "search": {
    "address": "http://localhost:9200"
  }
}

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

{
 "app": {
   "port": 3000,
   "page_size": 10
 },
 "search": {
   "address": ""
 }
}

Create src/application/executor/book_operator.ts:

import { BookManager } from "../../domain/gateway";
import { Book } from "../../domain/model";

export class BookOperator {
  private bookManager: BookManager;

  constructor(b: BookManager) {
    this.bookManager = b;
  }

  async createBook(b: Book): Promise<string> {
    return await this.bookManager.indexBook(b);
  }
}

Remember to create index.ts files for all the sub-folders.

Create src/application/wire_helper.ts:

import { Config } from "../infrastructure/config";
import { BookManager } from "../domain/gateway";
import { ElasticSearchEngine } from "../infrastructure/search";

// WireHelper is the helper for dependency injection
export class WireHelper {
  private engine: ElasticSearchEngine;

  constructor(c: Config) {
    this.engine = new ElasticSearchEngine(c.search.address, c.app.page_size);
  }

  bookManager(): BookManager {
    return this.engine;
  }
}

Create src/adapter/router.ts:

import express, { Request, Response } from "express";

import { Book } from "../domain/model";
import { BookOperator } from "../application/executor";
import { WireHelper } from "../application";

class RestHandler {
  private bookOperator: BookOperator;

  constructor(bookOperator: BookOperator) {
    this.bookOperator = bookOperator;
  }

  // Create a new book
  public async createBook(req: Request, res: Response): Promise<void> {
    try {
      const bookID = await this.bookOperator.createBook(req.body as Book);
      res.status(201).json({ id: bookID });
    } catch (err) {
      console.error(`Failed to create: ${err}`);
      res.status(404).json({ error: "Failed to create" });
    }
  }
}

// Create router
function MakeRouter(wireHelper: WireHelper): express.Router {
  const restHandler = new RestHandler(
    new BookOperator(wireHelper.bookManager())
  );

  const router = express.Router();
  router.get("/", (req: Request, res: Response) => {
    res.json({ status: "ok" });
  });
  router.post("/books", restHandler.createBook.bind(restHandler));
  return router;
}

export function InitApp(wireHelper: WireHelper): express.Express {
  const app = express();

  // Middleware to parse JSON bodies
  app.use(express.json());

  const r = MakeRouter(wireHelper);
  app.use("", r);
  return app;
}

Replace main.ts with the following code:

import { WireHelper } from "./application";
import { InitApp } from "./adapter";
import { parseConfig } from "./infrastructure/config";

const CONFIG_FILENAME = "config.json";

const c = parseConfig(CONFIG_FILENAME);
const wireHelper = new WireHelper(c);
const app = InitApp(wireHelper);

app.listen(c.app.port, () => {
  console.log(`Running on port ${c.app.port}`);
});

Run the server again and try it with curl:

npm run dev

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."}' \
  http://localhost:3000/books

Example response:

{"id":"1jxTDo8BWkofwNB1TRnC"}

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books

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."}' \
  http://localhost:3000/books