Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Elastic io refactor #19

Merged
merged 5 commits into from
Jul 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 8 additions & 18 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,27 +79,17 @@ components:
type: array
items:
type: object
additionalProperties: true
description: Dynamic keys/fields allowed
properties:
_index:
name:
type: string
_id:
status:
type: string
ID:
type: string
collection:
type: string
_score:
type: number
_source:
type: object
additionalProperties: true
description: Dynamic keys/fields allowed
properties:
name:
type: string
status:
type: string
ID:
type: string
collection:
type: string


Config:
type: object
Expand Down
90 changes: 90 additions & 0 deletions src/ElasticIO.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Client } from "@elastic/elasticsearch";
import ElasticIO from "./ElasticIO";
import env from "../util/validateEnv";
import Config from "./config";
import { SearchHit } from "@elastic/elasticsearch/lib/api/types";

//Mock the Client
jest.mock("@elastic/elasticsearch");
Expand Down Expand Up @@ -64,6 +65,95 @@ describe("ElasticIO", () =>
});
});

describe("processResults", () =>
{
it("should return an empty array when hits is undefined", () =>
{
const result = elasticIO.processResults(undefined);
expect(result).toEqual([]);
});
it("should return an empty array when hits is an empty array", () =>
{
const result = elasticIO.processResults([]);
expect(result).toEqual([]);
});
it("should sort the search results by _score in descending order and return their _source properties", () =>
{
const hits: SearchHit<unknown>[] = [
{ _score: 1, _source: { id: 1, name: "Item 1" } } as SearchHit<unknown>,
{ _score: 3, _source: { id: 3, name: "Item 3" } } as SearchHit<unknown>,
{ _score: 2, _source: { id: 2, name: "Item 2" } } as SearchHit<unknown>,
];

const result = elasticIO.processResults(hits);

expect(result).toEqual([
{ id: 3, name: "Item 3" },
{ id: 2, name: "Item 2" },
{ id: 1, name: "Item 1" },
]);
});
});

describe("search", () =>
{
it("should should call elasticsearchClient.search with the correct parameters", async () =>
{
const mockClient = {
search: jest.fn().mockResolvedValue({
hits: {
hits: [
{ _source: { name: "Junior Jobs", status: "Active", ID: { "$oid": "bb00" } } },
{ _source: { name: "Michael Smith", status: "Active", ID: { "$oid": "bb01" } } },
]
}
})
};

elasticIO["elasticsearchClient"] = mockClient as unknown as Client;

const queryString = 'active';
const searchRes = await elasticIO.search(queryString);

expect(mockClient.search).toHaveBeenCalledWith({
index: env.INDEX_NAME,
body: {
query: {
bool: {
should: [
{
multi_match: {
query: `${queryString}*`,
type: "best_fields",
fields: ["name", "status", "firstName", "lastName"]
}
},
{
query_string: {
query: `${queryString}*`,
fields: ["name", "status", "firstName", "lastName"]
}
}
]
}
}
}
});
if (searchRes) {
searchRes.hits.hits.forEach(result =>
{
expect(result._source).toEqual(
expect.objectContaining({
ID: expect.any(Object),
name: expect.any(String),
status: expect.any(String),
})
);
});
}
});
});

describe("disconnect", () =>
{
it("should close Elasticsearch client connection if client is defined", async () =>
Expand Down
42 changes: 40 additions & 2 deletions src/ElasticIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ export default class ElasticIO

constructor() { }



private connectionBody: {
node: string;
auth: {
Expand Down Expand Up @@ -48,6 +46,46 @@ export default class ElasticIO
}
}

processResults(hits: import("@elastic/elasticsearch/lib/api/types").SearchHit<unknown>[] | undefined)
{
if (!hits) return [];

hits.sort((a, b) => (b._score ?? 0) - (a._score ?? 0));

let processedHits = hits.map((hit) => hit._source);

return processedHits;
}

public async search(queryString: string)
{
const searchRes = await this.elasticsearchClient?.search({
index: env.INDEX_NAME,
body: {
query: {
bool: {
should: [
{
multi_match: {
query: `${queryString}*`,
type: "best_fields",
fields: ["name", "status", "firstName", "lastName"]
}
},
{
query_string: {
query: `${queryString}*`,
fields: ["name", "status", "firstName", "lastName"]
}
}
]
}
}
}
});
return searchRes;
}

public async disconnect(config: Config): Promise<void>
{
try {
Expand Down
35 changes: 8 additions & 27 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ app.get('/', (req, res) =>
res.send('Express + TypeScript Server');
});


app.get('/api/search', async (req, res, next) =>
{
//initialize client
Expand All @@ -32,43 +33,21 @@ app.get('/api/search', async (req, res, next) =>
throw createHttpError(400, 'Bad Request: Missing or invalid "query" header');
}
const queryString = req.headers.query as string;
const searchRes = await client?.search({
index: env.INDEX_NAME,
body: {
query: {
bool: {
should: [
{
multi_match: {
query: `${queryString}*`,
type: "best_fields",
fields: ["name", "status"]
}
},
{
query_string: {
query: `${queryString}*`,
fields: ["name", "status"]
}
}
]
}
}
}
}
);
const searchRes = await elasticIO.search(queryString);
if (searchRes?.timed_out) {
createHttpError(504, "Search database response has timed out. Please try again later.");
}
const processedResults = elasticIO.processResults(searchRes?.hits.hits);
//respond with elasticsearch response
res.status(200).json(searchRes?.hits.hits);
res.status(200).json(processedResults);
console.info("Search results returned successfully");
}
catch (error) {
console.error(error);
next(error);
}
finally {
await elasticIO?.disconnect(new Config());
await elasticIO.disconnect(new Config());
}
});

Expand All @@ -79,6 +58,7 @@ app.get('/api/health', async (req, res, next) =>
res.status(200);
res.set('Content-Type', register.contentType);
res.end(metrics);
console.info("GetHealth Completed");

} catch (error) {
console.error(error);
Expand All @@ -91,6 +71,7 @@ app.get('/api/config', async (req, res, next) =>
try {
const config = new Config();
res.status(200).json(config);
console.info("GetConfig Completed");
}
catch (error) {
console.error(error);
Expand Down
4 changes: 2 additions & 2 deletions src/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ RUN BRANCH=$(git rev-parse --abbrev-ref HEAD) && \
# Stage 2: Run the app in a lightweight image
FROM node:21-alpine as deploy

ENV ELASTICSEARCH_PROTOCOL=http
ENV ELASTICSEARCH_PROTOCOL=https
ENV ELASTICSEARCH_HOST=localhost
ENV ELASTICSEARCH_USERNAME=elastic
ENV ELASTICSEARCH_PASS="o0=eLmmQbsrdEW89a-Id"
ENV ELASTICSEARCH_PORT=9200
ENV API_PORT=8081
ENV INDEX_NAME=search-index
ENV CONNECTION_TIMEOUT=10
ENV CONNECTION_STRING=http://admin:admin@mentorhub-searchdb:9200
ENV CONNECTION_STRING=https://@mentorhub-searchdb:9200

WORKDIR /app

Expand Down
Loading