Merge pull request #2 from kavehhn174/typesense

Typesense
This commit is contained in:
Kaveh HajiNajaf
2025-01-02 16:45:38 +03:30
committed by GitHub
10 changed files with 471 additions and 26 deletions
@@ -1,6 +1,25 @@
const catchAsync = require('../../globalErrorHandler/catchAsync') const catchAsync = require('../../globalErrorHandler/catchAsync')
const belongingService = require('./belongingService') const belongingService = require('./belongingService')
exports.searchBelongings = catchAsync(async (req, res, next) => {
const {facets, belongings, totalCount, pagesCount} = await belongingService.searchBelongings()
res.status(200).json({
status: 'success',
facets, belongings, totalCount, pagesCount
})
})
exports.indexAllBelongings = catchAsync(async (req, res, next) => {
const { indexedCount } = await belongingService.indexAllBelongings()
res.status(200).json({
status: 'success',
indexedCount
})
})
exports.getAllBelongings = catchAsync(async (req, res, next) => { exports.getAllBelongings = catchAsync(async (req, res, next) => {
const belongings = await belongingService.getAllBelongings() const belongings = await belongingService.getAllBelongings()
+2 -6
View File
@@ -9,15 +9,11 @@ const belongingSchema = new mongoose.Schema(
required: true, required: true,
unique: true, unique: true,
}, },
belongingCategory: { category: {
type: ObjectID, type: ObjectID,
ref: 'Category', ref: 'Category',
}, },
projectCode: { description: String,
type: String,
required: true,
default: false,
},
relatedProjects: [String], relatedProjects: [String],
location: { location: {
type: { type: {
+2
View File
@@ -11,6 +11,8 @@ router.get('/admin/get-all', perm.requires('getallbelongings'), belongingControl
router.post('/admin/create', perm.requires('createabelonging'), belongingController.createBelonging) router.post('/admin/create', perm.requires('createabelonging'), belongingController.createBelonging)
router.post('/admin/index-all', perm.requires('indexbelongings', belongingController.indexAllBelongings))
router.get('/admin/get-one/:belongingId', perm.requires('getabelonging'), belongingController.getBelonging) router.get('/admin/get-one/:belongingId', perm.requires('getabelonging'), belongingController.getBelonging)
router.patch('/admin/update/:belongingId', perm.requires('updateabelonging'), belongingController.updateBelonging) router.patch('/admin/update/:belongingId', perm.requires('updateabelonging'), belongingController.updateBelonging)
+88 -5
View File
@@ -1,15 +1,17 @@
const Belonging = require('./belongingModel') const Belonging = require('./belongingModel')
const AppError = require('../../globalErrorHandler/AppError') const AppError = require('../../globalErrorHandler/AppError')
const { client } = require('../../setup/typesense')
const TsApiFeatures = require("../../utils/TsApiFeatures");
module.exports.getAllBelongings = async () => { const getAllBelongings = async () => {
return Belonging.find({}, null, null) return Belonging.find({}, null, null)
} }
module.exports.createBelonging = async (data) => { const createBelonging = async (data) => {
return Belonging.create(data, null) return Belonging.create(data, null)
} }
module.exports.getBelonging = async (belongingId) => { const getBelonging = async (belongingId) => {
const belonging = await Belonging.findById(belongingId, null, null) const belonging = await Belonging.findById(belongingId, null, null)
if (!belonging) { if (!belonging) {
@@ -19,7 +21,7 @@ module.exports.getBelonging = async (belongingId) => {
return belonging return belonging
} }
module.exports.updateBelonging = async (belongingId, data) => { const updateBelonging = async (belongingId, data) => {
const belonging = await Belonging.findOneAndUpdate({ _id: belongingId }, data, { new: true }) const belonging = await Belonging.findOneAndUpdate({ _id: belongingId }, data, { new: true })
if (!belonging) { if (!belonging) {
@@ -29,7 +31,7 @@ module.exports.updateBelonging = async (belongingId, data) => {
return belonging return belonging
} }
module.exports.deleteBelonging = async (belongingId) => { const deleteBelonging = async (belongingId) => {
const belonging = await Belonging.findByIdAndUpdate(belongingId, { isActive: false }, { new: true }) const belonging = await Belonging.findByIdAndUpdate(belongingId, { isActive: false }, { new: true })
if (!belonging) { if (!belonging) {
@@ -38,3 +40,84 @@ module.exports.deleteBelonging = async (belongingId) => {
return null return null
} }
const ensureCollectionExists = async () => {
const collections = await client.collections().retrieve()
const collectionExists = collections.some((collection) => collection.name === 'belongings')
if (collectionExists) {
await client.collections('belongings').delete()
}
const schema = getTypeSenseSchema()
await client.collections().create(schema)
console.log('Collection "belongings" created successfully')
}
const searchBelongings = async (inputQuery) => {
const tsFeatures = new TsApiFeatures(client, 'belongings', inputQuery).filter().facet().sort().limitFields().paginate().search()
let {facets, items, totalCount, pagesCount} = await tsFeatures.execute()
return {facets, belongings: items, totalCount, pagesCount}
}
const indexAllBelongings = async () => {
const batchSize = 20
const count = await Belonging.countDocuments({}, null)
const rounds = Math.ceil(count / batchSize)
await ensureCollectionExists()
for (let i = 1; i <= rounds; i++) {
console.log(`Indexing ${batchSize} belongings, Round ${i}`)
const belongings = await Belonging.find({}, null, null)
.populate('category')
.skip((i - 1) * batchSize)
.limit(batchSize)
let documents = []
for (const belonging of belongings) {
const data = {
_id: belonging._id,
isActive: belonging.isActive,
belongingCode: belonging.belongingCode,
category: belonging.category.map( x=> x.englishName),
nativeCategory: belonging.category.map( x=> x.nativeName),
documents: belonging.documents,
description: belonging.description,
coordinates: belonging.location.coordinates
}
documents.push(data)
}
await client.collections('belonging').documents().import(documents, {action: 'emplace'})
}
return {totalCount: count}
}
const getTypeSenseSchema = () => {
return {
name: 'belongings',
enable_nested_fields: true,
fields: [
{ name: '_id', type: 'string', facet: false, infix: false },
{ name: 'isActive', type: 'bool', facet: false, infix: true },
{ name: 'belongingCode', type: 'string', facet: true },
{ name: 'category', type: 'string[]', facet: true },
{ name: 'nativeCategory', type: 'string[]', facet: false },
{ name: 'documents', type: 'string[]', facet: false },
{ name: 'description', type: 'string', facet: false },
{ name: 'coordinates', type: 'geopoint' },
],
default_sorting_field: 'createdAt',
}
}
module.exports = {indexAllBelongings, getBelonging, getAllBelongings, createBelonging, deleteBelonging, searchBelongings}
+5 -3
View File
@@ -1,7 +1,9 @@
MONGODB_URL=mongodb://admin:password@mongodb:27018/marbm?authSource=admin MONGODB_URL=mongodb://admin:*135792468*@localhost:27018/marbm?authSource=admin
NODE_ENV=development NODE_ENV=development
PORT=3000 PORT=3000
JWT_SECRET=Y@RJ$WT!@K0NS@MPL! JWT_SECRET=Y@RJ$WT!@K0NS@MPL!
REDIS_URL= redis://:135792468@redis:6379/0 REDIS_URL= redis://:135792468@localhost:6379/0
LOGIN_ATTEMPT_LIMIT=5 LOGIN_ATTEMPT_LIMIT=5
SUPER_ADMIN_ROLE_ID=676afd6d076b286f9395b05f SUPER_ADMIN_ROLE_ID=676afd6d076b286f9395b05f
TYPE_SENSE_URL=localhost:8108
TYPE_SENSE_API_KEY=vYQq7pTgl6ACoRy
+24 -12
View File
@@ -1,14 +1,14 @@
services: services:
api-server: # api-server:
build: ./ # build: ./
ports: # ports:
- 3000:3000 # - 3000:3000
environment: # environment:
- MONGODB_URL=mongodb://admin:*135792468*@mongo:27017/marbm?authSource=admin # - MONGODB_URL=mongodb://admin:*135792468*@mongo:27017/marbm?authSource=admin
- NODE_ENV=development # - NODE_ENV=development
- PORT=3000 # - PORT=3000
- REDIS_URL=redis://:135792468@redis:6379/0 # - REDIS_URL=redis://:135792468@redis:6379/0
minio-server: bm_minio:
image: minio/minio image: minio/minio
ports: ports:
- 9000:9000 - 9000:9000
@@ -19,7 +19,7 @@ services:
volumes: volumes:
- bm_minio:/upload/ - bm_minio:/upload/
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
mongo: bm_mongo:
image: mongo:8.0 image: mongo:8.0
ports: ports:
- 27018:27017 - 27018:27017
@@ -30,7 +30,7 @@ services:
volumes: volumes:
- bm_mongo:/data/db - bm_mongo:/data/db
redis: bm_redis:
image: redis image: redis
command: redis-server --save 20 1 --loglevel warning --requirepass 135792468 command: redis-server --save 20 1 --loglevel warning --requirepass 135792468
volumes: volumes:
@@ -38,7 +38,19 @@ services:
ports: ports:
- 6379:6379 - 6379:6379
bm_typesense:
image: typesense/typesense:27.1
command: ./typesense-server
ports:
- "8108:8108"
environment:
TYPESENSE_API_KEY: vYQq7pTgl6ACoRy
TYPESENSE_DATA_DIR: /data
volumes:
- bm_typesense:/data
volumes: volumes:
bm_mongo: bm_mongo:
bm_redis: bm_redis:
bm_minio: bm_minio:
bm_typesense:
+121
View File
@@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^1.7.9",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"chalk": "^5.3.0", "chalk": "^5.3.0",
@@ -21,12 +22,25 @@
"mongoose": "^8.9.0", "mongoose": "^8.9.0",
"mongoose-sequence": "^6.0.1", "mongoose-sequence": "^6.0.1",
"redis": "^4.7.0", "redis": "^4.7.0",
"typesense": "^1.8.2",
"validator": "^13.12.0" "validator": "^13.12.0"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.9" "nodemon": "^3.1.9"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz",
"integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==",
"peer": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@mongodb-js/saslprep": { "node_modules/@mongodb-js/saslprep": {
"version": "1.1.9", "version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
@@ -144,6 +158,21 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -303,6 +332,17 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/compressible": { "node_modules/compressible": {
"version": "2.0.18", "version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -401,6 +441,14 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -585,6 +633,38 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -888,6 +968,18 @@
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
}, },
"node_modules/loglevel": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz",
"integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==",
"engines": {
"node": ">= 0.6.0"
},
"funding": {
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/loglevel"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
@@ -1255,6 +1347,11 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pstree.remy": { "node_modules/pstree.remy": {
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -1321,6 +1418,9 @@
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz",
"integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==",
"workspaces": [
"./packages/*"
],
"dependencies": { "dependencies": {
"@redis/bloom": "1.2.0", "@redis/bloom": "1.2.0",
"@redis/client": "1.6.0", "@redis/client": "1.6.0",
@@ -1330,6 +1430,12 @@
"@redis/time-series": "1.1.0" "@redis/time-series": "1.1.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"peer": true
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1585,6 +1691,21 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/typesense": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/typesense/-/typesense-1.8.2.tgz",
"integrity": "sha512-aBpePjA99Qvo+OP2pJwMpvga4Jrm1Y2oV5NsrWXBxlqUDNEUCPZBIksPv2Hq0jxQxHhLLyJVbjXjByXsvpCDVA==",
"dependencies": {
"axios": "^1.6.0",
"loglevel": "^1.8.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/undefsafe": { "node_modules/undefsafe": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+2
View File
@@ -11,6 +11,7 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"axios": "^1.7.9",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"body-parser": "^1.20.3", "body-parser": "^1.20.3",
"chalk": "^5.3.0", "chalk": "^5.3.0",
@@ -23,6 +24,7 @@
"mongoose": "^8.9.0", "mongoose": "^8.9.0",
"mongoose-sequence": "^6.0.1", "mongoose-sequence": "^6.0.1",
"redis": "^4.7.0", "redis": "^4.7.0",
"typesense": "^1.8.2",
"validator": "^13.12.0" "validator": "^13.12.0"
}, },
"devDependencies": { "devDependencies": {
+15
View File
@@ -0,0 +1,15 @@
const TypeSense = require('typesense')
let client = new TypeSense.Client({
nodes: [
{
host: process.env.TYPE_SENSE_URL,
port: '443',
protocol: 'httpS',
},
],
apiKey: process.env.TYPE_SENSE_API_KEY,
connectionTimeoutSeconds: 2,
})
module.exports = { client }
+193
View File
@@ -0,0 +1,193 @@
const axios = require('axios')
class TsApiFeatures {
constructor(client, collectionName, queryString) {
this.client = client
this.collectionName = collectionName
this.queryString = queryString
this.queryOptions = {}
}
search() {
// Set `q` to provided search term or default to `*` for all documents
this.queryOptions.q = this.queryString.query || '*'
// Set `query_by` to specified fields or default to `*`
this.queryOptions.query_by = this.queryString.query_by || 'name'
return this
}
filter() {
let filters = []
let rawKeys = []
// Add any additional filter conditions from queryString, if needed
Object.keys(this.queryString).forEach((key) => {
let operatorConditions = []
let normalValues = []
if (
[
'page',
'sort',
'limit',
'fields',
'per_page',
'filter',
'query',
'facet_by',
'query_by',
'highlight_full_fields',
].includes(key)
)
return
let value = this.queryString[key]
rawKeys.push(key)
if (!Array.isArray(value)) {
value = [value]
}
if (/\b(gte|gt|lte|lt|ne|eq)\b/.test(value)) {
const operatorMap = { gte: '>=', gt: '>', lte: '<=', lt: '<', ne: '!=', eq: '=' }
const match = value.match(/\b(gte|gt|lte|lt|ne|eq)\b/)
if (match) {
const operator = operatorMap[match[0]]
const cleanValue = value.replace(match[0], '').trim()
operatorConditions.push(`${key}${operator}${cleanValue}`)
}
} else {
normalValues.push(value)
}
if (operatorConditions.length > 0 && normalValues.length > 0) {
// Add all operator conditions
operatorConditions.forEach((cond) => filters.push(cond))
// Add the normal values condition
if (normalValues.length === 1) {
filters.push(`${key}:=${normalValues[0]}`)
} else {
filters.push(`${key}:=[${normalValues.join(',')}]`)
}
} else if (operatorConditions.length > 0) {
// Only operators
operatorConditions.forEach((cond) => filters.push(cond))
} else {
// Only normal values
if (normalValues.length === 1) {
filters.push(`${key}:=${normalValues[0]}`)
} else if (normalValues.length > 1) {
filters.push(`${key}:=[${normalValues.join(',')}]`)
}
}
})
// Join all conditions for TypeSense filter
if (filters.length) {
this.queryOptions.filters = filters.join(' && ')
}
const searchFilters = []
rawKeys.forEach((value, index) => {
const tempArray = JSON.parse(JSON.stringify(filters))
tempArray.splice(index, 1)
searchFilters.push(tempArray.join(' && '))
})
this.filterKeys = rawKeys
this.searchFilters = searchFilters
return this
}
sort() {
if (this.queryString.sort) {
this.queryOptions.sort_by = this.queryString.sort.split(',').join(',')
} else {
this.queryOptions.sort_by = 'createdAt:desc'
}
return this
}
limitFields() {
if (this.queryString.fields) {
this.queryOptions.include_fields = this.queryString.fields.split(',').join(',')
} else {
this.queryOptions.exclude_fields = '__v'
}
return this
}
facet() {
// Set `facet_by` based on the queryString, if available
if (this.queryString.facet_by) {
this.queryOptions.facet_by = this.queryString.facet_by
this.queryOptions.highlight_full_fields = this.queryString.highlight_full_fields
this.queryOptions.exhaustive_search = true
}
return this
}
paginate() {
const page = parseInt(this.queryString.page) || 1
const per_page = parseInt(this.queryString.per_page) || 20
this.queryOptions.page = page
this.queryOptions.per_page = per_page
return this
}
async execute() {
const searches = [
{
collection: this.collectionName,
exhaustive_search: this.queryOptions.exhaustive_search,
query_by: this.queryOptions.query_by,
filter_by: this.queryOptions.filters,
highlight_full_fields: this.queryOptions.highlight_full_fields,
q: this.queryOptions.q,
facet_by: this.queryOptions.facet_by,
max_facet_values: 100,
page: this.queryOptions.page,
per_page: this.queryOptions.per_page,
},
]
this.filterKeys.forEach((value, index) => {
searches.push({
collection: this.collectionName,
exhaustive_search: this.queryOptions.exhaustive_search,
query_by: this.queryOptions.query_by,
filter_by: this.searchFilters[index],
highlight_full_fields: this.queryOptions.highlight_full_fields,
q: this.queryOptions.q,
facet_by: value,
max_facet_values: 100,
page: this.queryOptions.page,
per_page: this.queryOptions.per_page,
})
})
const config = {
url: 'https://' + process.env.TYPE_SENSE_URL + '/multi_search?x-typesense-api-key=' + process.env.TYPE_SENSE_API_KEY,
method: 'POST',
data: {
searches,
},
}
const request = await axios(config)
const results = request.data.results[0]
let facet_counts = results.facet_counts
this.filterKeys.forEach((value, index) => {
const objIndex = facet_counts.findIndex((item) => item.field_name === value)
facet_counts[objIndex] = request.data.results[index + 1].facet_counts[0]
})
return {
facets: facet_counts,
items: results.hits.map((hit) => hit.document),
totalCount: results.found,
pagesCount: Math.ceil(results.found / this.queryOptions.per_page),
}
}
}
module.exports = TsApiFeatures