Skip to content

Commit 7b7b208

Browse files
committed
feat: maximizing search algorithm
1 parent 99f6733 commit 7b7b208

File tree

11 files changed

+88
-42
lines changed

11 files changed

+88
-42
lines changed

.prettierignore

-1
This file was deleted.

README.md

+16-16
Original file line numberDiff line numberDiff line change
@@ -87,32 +87,32 @@ curl -XGET '<a href="https://kodepos.vercel.app/?q=danasari">http://localhost:30
8787
"code": "OK",
8888
"data": [
8989
{
90-
"province": "Jawa Tengah",
91-
"regency": "Purbalingga",
92-
"district": "Karangjambu",
90+
"code": 46386,
9391
"village": "Danasari",
94-
"code": "53357"
92+
"district": "Cisaga",
93+
"regency": "Ciamis",
94+
"province": "Jawa Barat"
9595
},
9696
{
97-
"province": "Jawa Tengah",
98-
"regency": "Tegal",
99-
"district": "Bojong",
97+
"code": 53357,
10098
"village": "Danasari",
101-
"code": "52465"
99+
"district": "Karangjambu",
100+
"regency": "Purbalingga",
101+
"province": "Jawa Tengah"
102102
},
103103
{
104-
"province": "Jawa Tengah",
105-
"regency": "Pemalang",
106-
"district": "Pemalang",
104+
"code": 52314,
107105
"village": "Danasari",
108-
"code": "52314"
106+
"district": "Pemalang",
107+
"regency": "Pemalang",
108+
"province": "Jawa Tengah"
109109
},
110110
{
111-
"province": "Jawa Barat",
112-
"regency": "Ciamis",
113-
"district": "Cisaga",
111+
"code": 52465,
114112
"village": "Danasari",
115-
"code": "46386"
113+
"district": "Bojong",
114+
"regency": "Tegal",
115+
"province": "Jawa Tengah"
116116
}
117117
]
118118
}

app/controllers/search.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
import { KeywordOptions } from '../../types'
2-
import { createSpecResponse } from '../helpers/spec'
2+
import { createSpecResponse, sendBadRequest } from '../helpers/spec'
33
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
44

55
export const search = (app: FastifyInstance) => {
66
return async (request: FastifyRequest<{ Querystring: KeywordOptions }>, reply: FastifyReply) => {
77
const { q } = request.query
88
// TODO: search by province, regency, or district
9-
const data = app.fuse.search(q).sort((a, b) => (a.score || 0) - (b.score || 0))
109

11-
reply.header('Cache-Control', 's-maxage=86400, stale-while-revalidate=604800')
10+
if (!q) {
11+
return sendBadRequest(reply)
12+
}
13+
14+
const keywords = q
15+
// remove duplicate spaces
16+
.replace(/\s+/g, ' ')
17+
.split(' ')
18+
// add extended search per word
19+
// https://www.fusejs.io/examples.html#extended-search
20+
.map((i) => `'${i}`)
21+
.join(' ')
1222

13-
const result = data.map(({ item }) => item)
23+
const data = app.fuse.search(keywords)
24+
const result = data.map(({ item: { fulltext, ...rest } }) => rest)
1425
const response = createSpecResponse(result)
1526

27+
reply.header('Cache-Control', 's-maxage=86400, stale-while-revalidate=604800')
1628
return reply.send(response)
1729
}
1830
}

app/helpers/spec.ts

+8
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,11 @@ export const sendNotFound = (reply: FastifyReply) => {
2121
message: 'This endpoint cannot be found.',
2222
})
2323
}
24+
25+
export const sendBadRequest = (reply: FastifyReply) => {
26+
return reply.status(400).send({
27+
statusCode: 400,
28+
code: 'BAD_REQUEST',
29+
message: "The 'q' parameter must be filled.",
30+
})
31+
}

start/app.ts bin/app.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const app = async () => {
99
await app.register(import('@fastify/cors'))
1010
await app.register(import('@fastify/compress'))
1111
await app.register(import('@fastify/etag'))
12-
await app.register(import('./core'))
12+
await app.register(import('../start/core'))
1313

1414
if (process.env.ENABLE_RATE_LIMIT) {
1515
await app.register(import('@fastify/rate-limit'), { max: 2, timeWindow: '1 second' })

nodemon.json

-11
This file was deleted.

package.json

+21-3
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
"name": "@sooluh/kodepos",
33
"version": "4.0.0",
44
"description": "Indonesian postal code search API by place name, village or city",
5-
"main": "dist/app.js",
5+
"main": "dist/bin/app.js",
66
"scripts": {
77
"build": "npx tsc -p tsconfig.json",
88
"postbuild": "copyfiles data/* dist/",
9-
"start": "node ./dist/app.js",
9+
"start": "node ./dist/bin/app.js",
1010
"dev": "nodemon",
1111
"postinstall": "npm run build",
12-
"format": "prettier --write .",
12+
"format": "prettier . \"!data/kodepos.json\" --write",
1313
"commit": "git-cz"
1414
},
1515
"engines": {
@@ -46,6 +46,24 @@
4646
"prepare-commit-msg": "exec < /dev/tty && npx cz --hook || true"
4747
}
4848
},
49+
"nodemonConfig": {
50+
"restartable": "rs",
51+
"ignore": [
52+
".git",
53+
"node_modules/**/node_modules"
54+
],
55+
"verbose": true,
56+
"watch": [
57+
"app",
58+
"bin",
59+
"start"
60+
],
61+
"env": {
62+
"NODE_ENV": "development"
63+
},
64+
"ext": "ts",
65+
"exec": "ts-node ./bin/app.ts"
66+
},
4967
"repository": {
5068
"type": "git",
5169
"url": "git+https://github.com/sooluh/kodepos.git"

start/core.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,33 @@ import * as fs from 'node:fs/promises'
55
import type { DataResult } from '../types'
66
import type { FastifyInstance, FastifyPluginOptions } from 'fastify'
77

8+
const createFullText = (data: DataResult) => {
9+
const keys = Object.keys(data)
10+
const combinations: string[] = []
11+
12+
keys.forEach((key1, index1) => {
13+
keys.forEach((key2, index2) => {
14+
if (index1 !== index2) {
15+
combinations.push(`${data[key1]} ${data[key2]}`)
16+
}
17+
})
18+
})
19+
20+
return combinations.join(' ')
21+
}
22+
823
const load = async (app: FastifyInstance, _: FastifyPluginOptions) => {
924
const text = await fs.readFile(path.resolve('data/kodepos.json'), { encoding: 'utf-8' })
10-
const data: DataResult[] = JSON.parse(text)
25+
const json: DataResult[] = JSON.parse(text)
26+
const data = json.map((item) => ({ ...item, fulltext: createFullText(item) }))
1127

1228
const fuse = new Fuse(data, {
13-
keys: ['province', 'regency', 'district', 'village', 'code'],
29+
keys: ['fulltext'],
1430
includeScore: true,
1531
threshold: 0.1,
32+
shouldSort: true,
33+
ignoreLocation: true,
34+
useExtendedSearch: true,
1635
})
1736

1837
app.decorate('fuse', fuse)

tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
"declaration": true,
1414
"typeRoots": ["node_modules/@types", "types"]
1515
},
16-
"include": ["start/*.ts", "start/*.d.ts", "app/**/*.ts", "app/**/*.d.ts", "types/index.ts"],
16+
"include": ["app/**/*.ts", "bin/app.ts", "start/*.ts", "types/index.ts"],
1717
"exclude": ["node_modules"]
1818
}

types/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export type DataResult = {
1010
regency?: string
1111
district?: string
1212
village?: string
13-
code?: string
13+
code?: number
14+
fulltext?: string
1415
}

vercel.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
"routes": [
44
{
55
"src": "/(.*)",
6-
"dest": "start/app.ts"
6+
"dest": "bin/app.ts"
77
}
88
],
99
"builds": [
1010
{
11-
"src": "start/app.ts",
11+
"src": "bin/app.ts",
1212
"use": "@vercel/node"
1313
}
1414
],

0 commit comments

Comments
 (0)