feat: add MongoDB search method, endpoints, and settings module
deploy-chabokan / deploy (push) Canceled after 0s

This commit is contained in:
2026-08-19 05:50:14 +03:30
parent 3829851566
commit acb2e5d623
18 changed files with 649 additions and 21 deletions
+129 -5
View File
@@ -90,15 +90,136 @@ const ensureCollectionExists = async (forceRecreate = false) => {
}
}
const settingService = require('../setting/settingService')
const mongoose = require('mongoose')
const searchPropertiesMongo = async (inputQuery = {}) => {
const page = parseInt(inputQuery.page) || 1
const per_page = parseInt(inputQuery.per_page) || parseInt(inputQuery.limit) || 20
const mongoQuery = { isActive: { $ne: false } }
const q = (inputQuery.query || inputQuery.search || inputQuery.q || '').trim()
if (q && q !== '*') {
const searchRegex = { $regex: q, $options: 'i' }
const orConditions = [
{ name: searchRegex },
{ nationalGeoCode: searchRegex },
{ uniqueId: searchRegex },
{ serialNumber: searchRegex },
{ ownerName: searchRegex },
{ registrationPlate: searchRegex },
{ description: searchRegex },
{ address: searchRegex },
{ tags: searchRegex },
{ relatedProjects: searchRegex },
]
if (!isNaN(q) && q !== '') {
orConditions.push({ propertyCode: Number(q) })
}
if (mongoose.Types.ObjectId.isValid(q)) {
orConditions.push({ _id: q })
}
mongoQuery.$or = orConditions
}
if (inputQuery.category && mongoose.Types.ObjectId.isValid(inputQuery.category)) {
mongoQuery.category = inputQuery.category
}
if (inputQuery.status && mongoose.Types.ObjectId.isValid(inputQuery.status)) {
mongoQuery.status = inputQuery.status
}
let sortObj = { createdAt: -1 }
if (inputQuery.sort) {
if (typeof inputQuery.sort === 'string') {
const parts = inputQuery.sort.split(':')
const field = parts[0]
const order = parts[1] === 'asc' ? 1 : -1
sortObj = { [field]: order }
} else if (typeof inputQuery.sort === 'object') {
sortObj = inputQuery.sort
}
}
const totalCount = await Property.countDocuments(mongoQuery)
const properties = await Property.find(mongoQuery)
.populate('category')
.populate('status')
.sort(sortObj)
.skip((page - 1) * per_page)
.limit(per_page)
const items = properties.map((prop) => {
const combinedTags = Array.isArray(prop.tags) ? prop.tags.join(' ') : (prop.tags || '')
return {
_id: prop._id.toString(),
id: prop.propertyCode ? prop.propertyCode.toString() : prop._id.toString(),
propertyCode: prop.propertyCode,
name: prop.name || '',
category: prop.category?.englishName || (typeof prop.category === 'string' ? prop.category : ''),
nativeCategory: prop.category?.nativeName || '',
categoryObj: prop.category,
status: prop.status,
imageUrl: prop.mainImageUrls?.[0] || 'https://cloud.mbmanager.ir/images/item-no-image.png',
mainImageUrls: prop.mainImageUrls || [],
documents: prop.documentUrls || [],
documentUrls: prop.documentUrls || [],
description: prop.description || '',
nationalGeoCode: prop.nationalGeoCode || '',
tags: combinedTags,
dong: prop.dong,
ownerName: prop.ownerName || '',
registrationPlate: prop.registrationPlate || '',
uniqueId: prop.uniqueId || '',
serialNumber: prop.serialNumber || '',
coordinates: prop.location?.coordinates || [0, 0],
location: prop.location,
createdAt: Math.floor(new Date(prop.createdAt || Date.now()).getTime() / 1000),
updatedAt: prop.updatedAt,
isActive: prop.isActive !== false,
features: prop.features || [],
}
})
const pagesCount = Math.ceil(totalCount / per_page) || 1
return {
facets: [],
belongings: items,
properties: items,
totalCount,
pagesCount,
searchEngine: 'mongo',
}
}
const searchProperties = async (inputQuery) => {
await ensureCollectionExists(false)
inputQuery = updateInputQuery(inputQuery, {})
try {
const setting = await settingService.getSearchSettingByPage('properties')
if (setting && setting.searchMethod === 'mongo') {
return searchPropertiesMongo(inputQuery)
}
} catch (e) {
console.warn('Failed to fetch search setting for properties, attempting typesense:', e.message)
}
const tsFeatures = new TsApiFeatures(client, 'properties', inputQuery).filter().facet().sort().limitFields().paginate().search()
try {
await ensureCollectionExists(false)
const updatedQuery = updateInputQuery({ ...inputQuery }, {})
let { facets, items, totalCount, pagesCount } = await tsFeatures.execute()
const tsFeatures = new TsApiFeatures(client, 'properties', updatedQuery).filter().facet().sort().limitFields().paginate().search()
return { facets, belongings: items, totalCount, pagesCount }
let { facets, items, totalCount, pagesCount } = await tsFeatures.execute()
return { facets, belongings: items, properties: items, totalCount, pagesCount, searchEngine: 'typesense' }
} catch (err) {
console.warn('Typesense search failed for properties, falling back to MongoDB:', err.message)
return searchPropertiesMongo(inputQuery)
}
}
const updateInputQuery = (inputQuery, extraQueries) => {
@@ -111,6 +232,7 @@ const updateInputQuery = (inputQuery, extraQueries) => {
return inputQuery
}
const indexAllProperties = async () => {
const batchSize = 20
const count = await Property.countDocuments({ isActive: true }, null)
@@ -213,5 +335,7 @@ module.exports = {
createProperty,
deleteProperty,
searchProperties,
searchPropertiesMongo,
indexOneProperty,
}