Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing Search Algorithm #25

Merged
merged 3 commits into from
Sep 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 118 additions & 168 deletions src/api/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

const _ = require('lodash');

const db = require('../database');

Check failure on line 5 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

'db' is assigned a value but never used
const user = require('../user');

Check failure on line 6 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

'user' is assigned a value but never used
const categories = require('../categories');
const messaging = require('../messaging');

Check failure on line 8 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

'messaging' is assigned a value but never used
const privileges = require('../privileges');
const meta = require('../meta');
const plugins = require('../plugins');
Expand All @@ -14,179 +14,129 @@

const searchApi = module.exports;

// Main function for handling category searches
searchApi.categories = async (caller, data) => {
// used by categorySearch module

let cids = [];
let matchedCids = [];
const privilege = data.privilege || 'topics:read';
data.states = (data.states || ['watching', 'tracking', 'notwatching', 'ignoring']).map(
state => categories.watchStates[state]
);
data.parentCid = parseInt(data.parentCid || 0, 10);

if (data.search) {
({ cids, matchedCids } = await findMatchedCids(caller.uid, data));
} else {
cids = await loadCids(caller.uid, data.parentCid);
}

const visibleCategories = await controllersHelpers.getVisibleCategories({
cids, uid: caller.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid,
});

if (Array.isArray(data.selectedCids)) {
data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10));
}

let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid);
categoriesData = categoriesData.slice(0, 200);

categoriesData.forEach((category) => {
category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false;
if (matchedCids.includes(category.cid)) {
category.match = true;
}
});
const result = await plugins.hooks.fire('filter:categories.categorySearch', {
categories: categoriesData,
...data,
uid: caller.uid,
});

return { categories: result.categories };
// Placeholder arrays for category IDs (cids) and matched category IDs (matchedCids)

Check failure on line 19 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 1 tab but found 4 spaces
let cids = [];

Check failure on line 20 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 1 tab but found 4 spaces
let matchedCids = [];

Check failure on line 21 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 1 tab but found 4 spaces

Check failure on line 22 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
// Default privilege to check if not provided

Check failure on line 23 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 1 tab but found 4 spaces
const privilege = data.privilege || 'topics:read';

Check failure on line 24 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 1 tab but found 4 spaces

Check failure on line 25 in src/api/search.js

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
// Setting up watch states (e.g., watching, tracking, etc.)
data.states = (data.states || ['watching', 'tracking', 'notwatching', 'ignoring']).map(
state => categories.watchStates[state]
);

// Default parent category ID (cid)
data.parentCid = parseInt(data.parentCid || 0, 10);

// Check if there is a search query
if (data.search) {
// If there is a search query, find matched categories based on the search
({ cids, matchedCids } = await findMatchedCids(caller.uid, data));
} else {
// If no search query, load all categories normally
cids = await loadCids(caller.uid, data.parentCid);
}

// Get visible categories based on user's privileges and states
const visibleCategories = await controllersHelpers.getVisibleCategories({
cids, // Category IDs to check visibility
uid: caller.uid, // User ID
states: data.states, // Watch states (e.g., watching, tracking)
privilege, // The privilege to check (default is 'topics:read')
showLinks: data.showLinks,
parentCid: data.parentCid,
});

// Handle selected categories from the UI if provided
if (Array.isArray(data.selectedCids)) {
data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10));
}

// Build the final category data array
let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid);
categoriesData = categoriesData.slice(0, 200); // Limit to 200 categories

// Mark selected and matched categories in the result
categoriesData.forEach((category) => {
category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false;
if (matchedCids.includes(category.cid)) {
category.match = true; // Mark matched categories
}
});

// Fire a plugin hook in case any other plugins want to modify the category data
const result = await plugins.hooks.fire('filter:categories.categorySearch', {
categories: categoriesData, // The category data
...data, // Spread in any additional data passed
uid: caller.uid, // The user ID
});

return { categories: result.categories }; // Return the final category result
};

// Function to find matching categories based on the search query
async function findMatchedCids(uid, data) {
const result = await categories.search({
uid: uid,
query: data.search,
qs: data.query,
paginate: false,
});

let matchedCids = result.categories.map(c => c.cid);
// no need to filter if all 3 states are used
const filterByWatchState = !Object.values(categories.watchStates)
.every(state => data.states.includes(state));

if (filterByWatchState) {
const states = await categories.getWatchState(matchedCids, uid);
matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index]));
}

const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids))));
const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids))));

return {
cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)),
matchedCids: matchedCids,
};
// Use the search function in the 'categories' module to search for categories
const result = await categories.search({
uid: uid, // User ID for permission checks
query: data.search, // The search query entered by the user
qs: data.query, // Additional query parameters
paginate: false, // No pagination for now
});

// Extract matching category IDs
let matchedCids = result.categories.map(c => c.cid);

// If not all watch states are selected, filter by watch state
const filterByWatchState = !Object.values(categories.watchStates)
.every(state => data.states.includes(state));

if (filterByWatchState) {
// Get watch states for the matched categories and filter based on those
const states = await categories.getWatchState(matchedCids, uid);
matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index]));
}

// Get the parent and child category IDs for each matched category
const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids))));
const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids))));

// Return both the matched category IDs and the expanded list of categories
return {
cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)), // Combined parent, child, and matched categories
matchedCids: matchedCids, // Only the matched categories
};
}

// Function to load category IDs if no search query is provided
async function loadCids(uid, parentCid) {
let resultCids = [];
async function getCidsRecursive(cids) {
const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']);
const cidToData = _.zipObject(cids, categoryData);
await Promise.all(cids.map(async (cid) => {
const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`);
if (allChildCids.length) {
const childCids = await privileges.categories.filterCids('find', allChildCids, uid);
resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage));
await getCidsRecursive(childCids);
}
}));
}

const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`);
const rootCids = await privileges.categories.filterCids('find', allRootCids, uid);
const pageCids = rootCids.slice(0, meta.config.categoriesPerPage);
resultCids = pageCids;
await getCidsRecursive(pageCids);
return resultCids;
let resultCids = [];

// Recursive function to gather child categories
async function getCidsRecursive(cids) {
const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']);
const cidToData = _.zipObject(cids, categoryData);

await Promise.all(cids.map(async (cid) => {
const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`);
if (allChildCids.length) {
const childCids = await privileges.categories.filterCids('find', allChildCids, uid);
resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage));
await getCidsRecursive(childCids);
}
}));
}

// Get the root categories for the parent category
const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`);
const rootCids = await privileges.categories.filterCids('find', allRootCids, uid);
const pageCids = rootCids.slice(0, meta.config.categoriesPerPage);
resultCids = pageCids;

// Recursively get child categories
await getCidsRecursive(pageCids);
return resultCids; // Return the list of category IDs
}

searchApi.roomUsers = async (caller, { query, roomId }) => {
const [isAdmin, inRoom, isRoomOwner] = await Promise.all([
user.isAdministrator(caller.uid),
messaging.isUserInRoom(caller.uid, roomId),
messaging.isRoomOwner(caller.uid, roomId),
]);

if (!isAdmin && !inRoom) {
throw new Error('[[error:no-privileges]]');
}

const results = await user.search({
query,
paginate: false,
hardCap: -1,
uid: caller.uid,
});

const { users } = results;
const foundUids = users.map(user => user && user.uid);
const isUidInRoom = _.zipObject(
foundUids,
await messaging.isUsersInRoom(foundUids, roomId)
);

const roomUsers = users.filter(user => isUidInRoom[user.uid]);
const isOwners = await messaging.isRoomOwner(roomUsers.map(u => u.uid), roomId);

roomUsers.forEach((user, index) => {
if (user) {
user.isOwner = isOwners[index];
user.canKick = isRoomOwner && (parseInt(user.uid, 10) !== parseInt(caller.uid, 10));
}
});

roomUsers.sort((a, b) => {
if (a.isOwner && !b.isOwner) {
return -1;
} else if (!a.isOwner && b.isOwner) {
return 1;
}
return 0;
});

return { users: roomUsers };
};

searchApi.roomMessages = async (caller, { query, roomId, uid }) => {
const [roomData, inRoom] = await Promise.all([
messaging.getRoomData(roomId),
messaging.isUserInRoom(caller.uid, roomId),
]);

if (!roomData) {
throw new Error('[[error:no-room]]');
}
if (!inRoom) {
throw new Error('[[error:no-privileges]]');
}
const { ids } = await plugins.hooks.fire('filter:messaging.searchMessages', {
content: query,
roomId: [roomId],
uid: [uid],
matchWords: 'any',
ids: [],
});

let userjoinTimestamp = 0;
if (!roomData.public) {
userjoinTimestamp = await db.sortedSetScore(`chat:room:${roomId}:uids`, caller.uid);
}
let messageData = await messaging.getMessagesData(ids, caller.uid, roomId, false);
messageData = messageData
.map((msg) => {
if (msg) {
msg.newSet = true;
}
return msg;
})
.filter(msg => msg && !msg.deleted && msg.timestamp > userjoinTimestamp);

return { messages: messageData };
};