diff --git a/bun.lockb b/bun.lockb index 07d65b227b112081b8a5c9b3bf38892687972d65..c6fab67ceda4fd6f25551f4c2a416322ac7ac6ae 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 2cd1559215768cbf6c8d6f3664f09971050d9854..d60c4672c7ba451b8f83fa727010e423b0b8b4ee 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,13 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.750.0", + "@elastic/elasticsearch": "^8.17.0", "@hono/swagger-ui": "^0.5.0", "@hono/zod-openapi": "^0.18.3", - "@elastic/elasticsearch": "^8.17.0", "@hono/zod-validator": "^0.2.2", "@scalar/hono-api-reference": "^0.8.0", "archiver": "^7.0.1", + "bun": "1.2.10", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "drizzle-orm": "^0.31.2", diff --git a/src/routes/search.route.ts b/src/routes/search.route.ts index a30fe330f02d96f45c945232fe5d32cb3d67fca0..56aad13cbbeb760f7cb747f898aed49ea8c73e8e 100644 --- a/src/routes/search.route.ts +++ b/src/routes/search.route.ts @@ -1,4 +1,4 @@ -import { SearchService } from "@/services/search.service"; +import { SearchService, type ExactFilterFields } from "@/services/search.service"; import Container from "typedi"; // import { honoWithJwt } from ".."; import { Hono } from "hono"; @@ -11,18 +11,58 @@ const searchService = Container.get(SearchService); export const searchRouter = new Hono() .get( '/search', + zValidator( 'query', z.object({ - query: z.string().min(1, 'Query is required'), + query: z.string().optional(), + sortBy: z.enum(['created_at']).optional(), + sortOrder: z.enum(['asc', 'desc']).optional(), + offset: z.coerce.number().int().nonnegative().optional(), + page_size: z.coerce.number().int().positive().optional(), + + // filters + language: z.string().optional(), + objectType: z.string().optional(), + institution: z.string().optional(), }) ), + async (c) => { - try { - const { query } = c.req.valid('query'); + const validated = c.req.valid('query'); + console.log('VALIDATED:', validated); - const results = await searchService.searchIndex(query, {}); + try { + const { + query: rawQuery, + sortBy, + sortOrder = 'desc', + offset = 0, + page_size = 1000, + language, + objectType, + institution, + } = c.req.valid('query'); + + const query = rawQuery ?? ""; + + const filters: Partial<ExactFilterFields> = {}; + if (language) filters.language = language; + if (objectType) filters.objectType = objectType; + if (institution) filters.institution = institution; + + const results = await searchService.searchIndex( + query, + filters, + sortBy, + sortOrder, + undefined, + offset, + page_size + ); + + console.log("results", results) return c.json({ results }); } catch (e) { console.log(e) @@ -41,13 +81,13 @@ export const searchRouter = new Hono() ) .post( - '/index/resource/:id', + '/index/resource/:id', async (c) => { try { const id: number = +c.req.param('id') - + await searchService.indexResource(id); - return c.json({ message: `resource indexed successfully`}); + return c.json({ message: `resource indexed successfully` }); } catch (e) { return c.json( createApexError({ @@ -64,13 +104,13 @@ export const searchRouter = new Hono() ) .post( - '/index/collection/:id', + '/index/collection/:id', async (c) => { try { const id: number = +c.req.param('id') - + await searchService.indexCollection(id); - return c.json({ message: `collection indexed successfully`}); + return c.json({ message: `collection indexed successfully` }); } catch (e) { return c.json( createApexError({ @@ -87,14 +127,14 @@ export const searchRouter = new Hono() ) .post( - '/index/user/:id', + '/index/user/:id', async (c) => { try { const id: number = +c.req.param('id') - + await searchService.indexUser(id); - return c.json({ message: `user indexed successfully`}); + return c.json({ message: `user indexed successfully` }); } catch (e) { return c.json( createApexError({ diff --git a/src/routes/subjects.route.ts b/src/routes/subjects.route.ts index 97ff367ba142bab80f1b79d52c9dc9966d4c0287..503d76dd3f620a9763d2750a8ae77e0a80f043ac 100644 --- a/src/routes/subjects.route.ts +++ b/src/routes/subjects.route.ts @@ -94,7 +94,7 @@ export const publicSubjectsRouter = new Hono() async (c) => { try { const subjects = await service.findMany() - return c.json({ subjects }) + return c.json( subjects ) } catch (e) { return c.json( createApexError({ diff --git a/src/search/index.ts b/src/search/index.ts index 3269b06dd5f6f0d5be213f3c87eefc2eef590fb1..9f4f054216f7df88e5fdcf87fa0bba19a721cde5 100644 --- a/src/search/index.ts +++ b/src/search/index.ts @@ -6,6 +6,9 @@ import { Container } from 'typedi' export const es = new Client({ node: env.ELASTICSEARCH_URL, Connection: HttpConnection, + tls: { + rejectUnauthorized: false + } }) // Register the Elasticsearch client in the DI container @@ -26,4 +29,4 @@ checkElasticsearchConnection() export type es = typeof es -export default es \ No newline at end of file +export default es diff --git a/src/services/search.service.ts b/src/services/search.service.ts index 5991e1fc8a32b8ff446097a45d78d7d95d2ea417..c1cfa733c57bea99c0acef6bf747207b04d10faf 100644 --- a/src/services/search.service.ts +++ b/src/services/search.service.ts @@ -3,7 +3,7 @@ import es from '@/search' import env from '@/env' import { SearchRepo } from "@/db/repo/search.repo"; -type ExactFilterFields = { +export type ExactFilterFields = { language?: string; objectType?: string; institution?: string; @@ -158,6 +158,41 @@ export class SearchService { ]; } + const searchQuery = query + ? { + multi_match: { + query, + fuzziness: "AUTO", + fields: [ + 'name', + 'author', + 'description', + 'link', + 'fileFormats', + 'educational_stages', + 'languages', + 'subjects', + 'license', + 'object_type', + 'state', + 'user', + 'roles', + 'institution', + 'city', + 'views', + 'downloads', + 'likes', + 'shares', + 'score', + 'comments', + 'created_at' + ], + }, + } + : { + match_all: {}, + } + const result = await this.esClient.search({ index: index, body: { @@ -165,36 +200,7 @@ export class SearchService { size: page_size, query: { bool: { - must: [ - { - multi_match: { - query, - fuzziness: "AUTO", - fields: ['name', - 'author', - 'description', - 'link', - 'fileFormats', - 'educational_stages', - 'languages', - 'subjects', - 'license', - 'object_type', - 'state', - 'user', - 'roles', - 'institution', - 'city', - 'views', - 'downloads', - 'likes', - 'shares', - 'score', - 'comments', - 'created_at'], - }, - }, - ], + must: [searchQuery], filter: filterClauses, }, },