From 7b7b20871a93812eff36b2e4b2a38b456d24712a Mon Sep 17 00:00:00 2001 From: Abu Masyail <20874779+sooluh@users.noreply.github.com> Date: Wed, 1 May 2024 12:59:33 +0700 Subject: [PATCH] feat: maximizing search algorithm --- .prettierignore | 1 - README.md | 32 ++++++++++++++++---------------- app/controllers/search.ts | 20 ++++++++++++++++---- app/helpers/spec.ts | 8 ++++++++ {start => bin}/app.ts | 2 +- nodemon.json | 11 ----------- package.json | 24 +++++++++++++++++++++--- start/core.ts | 23 +++++++++++++++++++++-- tsconfig.json | 2 +- types/index.ts | 3 ++- vercel.json | 4 ++-- 11 files changed, 88 insertions(+), 42 deletions(-) delete mode 100644 .prettierignore rename {start => bin}/app.ts (97%) delete mode 100644 nodemon.json diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 8fce603..0000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -data/ diff --git a/README.md b/README.md index 023f1d4..cc4d6b6 100644 --- a/README.md +++ b/README.md @@ -87,32 +87,32 @@ curl -XGET 'http://localhost:30 "code": "OK", "data": [ { - "province": "Jawa Tengah", - "regency": "Purbalingga", - "district": "Karangjambu", + "code": 46386, "village": "Danasari", - "code": "53357" + "district": "Cisaga", + "regency": "Ciamis", + "province": "Jawa Barat" }, { - "province": "Jawa Tengah", - "regency": "Tegal", - "district": "Bojong", + "code": 53357, "village": "Danasari", - "code": "52465" + "district": "Karangjambu", + "regency": "Purbalingga", + "province": "Jawa Tengah" }, { - "province": "Jawa Tengah", - "regency": "Pemalang", - "district": "Pemalang", + "code": 52314, "village": "Danasari", - "code": "52314" + "district": "Pemalang", + "regency": "Pemalang", + "province": "Jawa Tengah" }, { - "province": "Jawa Barat", - "regency": "Ciamis", - "district": "Cisaga", + "code": 52465, "village": "Danasari", - "code": "46386" + "district": "Bojong", + "regency": "Tegal", + "province": "Jawa Tengah" } ] } diff --git a/app/controllers/search.ts b/app/controllers/search.ts index 30227db..1aa97fe 100644 --- a/app/controllers/search.ts +++ b/app/controllers/search.ts @@ -1,18 +1,30 @@ import { KeywordOptions } from '../../types' -import { createSpecResponse } from '../helpers/spec' +import { createSpecResponse, sendBadRequest } from '../helpers/spec' import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' export const search = (app: FastifyInstance) => { return async (request: FastifyRequest<{ Querystring: KeywordOptions }>, reply: FastifyReply) => { const { q } = request.query // TODO: search by province, regency, or district - const data = app.fuse.search(q).sort((a, b) => (a.score || 0) - (b.score || 0)) - reply.header('Cache-Control', 's-maxage=86400, stale-while-revalidate=604800') + if (!q) { + return sendBadRequest(reply) + } + + const keywords = q + // remove duplicate spaces + .replace(/\s+/g, ' ') + .split(' ') + // add extended search per word + // https://www.fusejs.io/examples.html#extended-search + .map((i) => `'${i}`) + .join(' ') - const result = data.map(({ item }) => item) + const data = app.fuse.search(keywords) + const result = data.map(({ item: { fulltext, ...rest } }) => rest) const response = createSpecResponse(result) + reply.header('Cache-Control', 's-maxage=86400, stale-while-revalidate=604800') return reply.send(response) } } diff --git a/app/helpers/spec.ts b/app/helpers/spec.ts index 2781c75..234f073 100644 --- a/app/helpers/spec.ts +++ b/app/helpers/spec.ts @@ -21,3 +21,11 @@ export const sendNotFound = (reply: FastifyReply) => { message: 'This endpoint cannot be found.', }) } + +export const sendBadRequest = (reply: FastifyReply) => { + return reply.status(400).send({ + statusCode: 400, + code: 'BAD_REQUEST', + message: "The 'q' parameter must be filled.", + }) +} diff --git a/start/app.ts b/bin/app.ts similarity index 97% rename from start/app.ts rename to bin/app.ts index 7f1e423..0fa275f 100644 --- a/start/app.ts +++ b/bin/app.ts @@ -9,7 +9,7 @@ const app = async () => { await app.register(import('@fastify/cors')) await app.register(import('@fastify/compress')) await app.register(import('@fastify/etag')) - await app.register(import('./core')) + await app.register(import('../start/core')) if (process.env.ENABLE_RATE_LIMIT) { await app.register(import('@fastify/rate-limit'), { max: 2, timeWindow: '1 second' }) diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index eb8a5a4..0000000 --- a/nodemon.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "restartable": "rs", - "ignore": [".git", "node_modules/**/node_modules"], - "verbose": true, - "watch": ["app", "start"], - "env": { - "NODE_ENV": "development" - }, - "ext": "ts,json", - "exec": "ts-node ./start/app.ts" -} diff --git a/package.json b/package.json index 17c2d2a..4ff63ea 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "name": "@sooluh/kodepos", "version": "4.0.0", "description": "Indonesian postal code search API by place name, village or city", - "main": "dist/app.js", + "main": "dist/bin/app.js", "scripts": { "build": "npx tsc -p tsconfig.json", "postbuild": "copyfiles data/* dist/", - "start": "node ./dist/app.js", + "start": "node ./dist/bin/app.js", "dev": "nodemon", "postinstall": "npm run build", - "format": "prettier --write .", + "format": "prettier . \"!data/kodepos.json\" --write", "commit": "git-cz" }, "engines": { @@ -46,6 +46,24 @@ "prepare-commit-msg": "exec < /dev/tty && npx cz --hook || true" } }, + "nodemonConfig": { + "restartable": "rs", + "ignore": [ + ".git", + "node_modules/**/node_modules" + ], + "verbose": true, + "watch": [ + "app", + "bin", + "start" + ], + "env": { + "NODE_ENV": "development" + }, + "ext": "ts", + "exec": "ts-node ./bin/app.ts" + }, "repository": { "type": "git", "url": "git+https://github.com/sooluh/kodepos.git" diff --git a/start/core.ts b/start/core.ts index b2b9e3d..6a8c0f0 100644 --- a/start/core.ts +++ b/start/core.ts @@ -5,14 +5,33 @@ import * as fs from 'node:fs/promises' import type { DataResult } from '../types' import type { FastifyInstance, FastifyPluginOptions } from 'fastify' +const createFullText = (data: DataResult) => { + const keys = Object.keys(data) + const combinations: string[] = [] + + keys.forEach((key1, index1) => { + keys.forEach((key2, index2) => { + if (index1 !== index2) { + combinations.push(`${data[key1]} ${data[key2]}`) + } + }) + }) + + return combinations.join(' ') +} + const load = async (app: FastifyInstance, _: FastifyPluginOptions) => { const text = await fs.readFile(path.resolve('data/kodepos.json'), { encoding: 'utf-8' }) - const data: DataResult[] = JSON.parse(text) + const json: DataResult[] = JSON.parse(text) + const data = json.map((item) => ({ ...item, fulltext: createFullText(item) })) const fuse = new Fuse(data, { - keys: ['province', 'regency', 'district', 'village', 'code'], + keys: ['fulltext'], includeScore: true, threshold: 0.1, + shouldSort: true, + ignoreLocation: true, + useExtendedSearch: true, }) app.decorate('fuse', fuse) diff --git a/tsconfig.json b/tsconfig.json index 6cb7381..f467f52 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "declaration": true, "typeRoots": ["node_modules/@types", "types"] }, - "include": ["start/*.ts", "start/*.d.ts", "app/**/*.ts", "app/**/*.d.ts", "types/index.ts"], + "include": ["app/**/*.ts", "bin/app.ts", "start/*.ts", "types/index.ts"], "exclude": ["node_modules"] } diff --git a/types/index.ts b/types/index.ts index 32f4f86..12f2668 100644 --- a/types/index.ts +++ b/types/index.ts @@ -10,5 +10,6 @@ export type DataResult = { regency?: string district?: string village?: string - code?: string + code?: number + fulltext?: string } diff --git a/vercel.json b/vercel.json index c1cbaa6..ed71453 100644 --- a/vercel.json +++ b/vercel.json @@ -3,12 +3,12 @@ "routes": [ { "src": "/(.*)", - "dest": "start/app.ts" + "dest": "bin/app.ts" } ], "builds": [ { - "src": "start/app.ts", + "src": "bin/app.ts", "use": "@vercel/node" } ],