Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
41c7e43
Search Bar Enhancement: add message content search backend
Kaveh-Vakili Jul 17, 2026
2bceca7
Merge branch 'dev' into Search-Bar-Enhancement
tlokeshrao Jul 20, 2026
e36cb34
Address PR review: fix limit/offset parsing, simplify LIKE filter, ad…
Kaveh-Vakili Jul 21, 2026
dc7d30f
Merge branch 'dev' into Search-Bar-Enhancement
tlokeshrao Jul 21, 2026
14d5da8
Default includeMessageText to true for backward compatibility
Kaveh-Vakili Jul 22, 2026
6cd1032
Merge branch 'dev' into Search-Bar-Enhancement
tlokeshrao Jul 23, 2026
c09fc7e
Restore project ID fallback to insight context; restore limit/offset/…
Kaveh-Vakili Jul 23, 2026
f609bed
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Jul 23, 2026
723ef3a
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Jul 23, 2026
cbcd693
Use MESSAGE date_created; use getLong/getBoolean helpers; fix OFFSET-…
Kaveh-Vakili Jul 23, 2026
de5d53e
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Jul 24, 2026
f954bc5
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 3, 2026
8fc036e
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 3, 2026
08d0645
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 4, 2026
abda202
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 4, 2026
0104c71
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 4, 2026
082a76a
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 4, 2026
9a4710f
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 6, 2026
d9c63e7
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 6, 2026
8852a87
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 7, 2026
3acef8a
fix: dedupe search results to one row per room before pagination
Kaveh-Vakili Aug 7, 2026
8200b55
Merge branch 'Search-Bar-Enhancement' of https://github.com/SEMOSS/Se…
Kaveh-Vakili Aug 7, 2026
d3f7e27
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 10, 2026
143dff7
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 10, 2026
ca0c3cc
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 11, 2026
3c79ca0
fix: dedupe logic and room filtering
kunal0137 Aug 11, 2026
243185d
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 11, 2026
7faf3c1
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 12, 2026
004e9f6
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 12, 2026
9516f36
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 12, 2026
1b255d2
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 13, 2026
8682e51
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 14, 2026
acf99df
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 14, 2026
1f500e2
fix: use ROOM__DATE_CREATED in SearchRoomMessages to match GetPlaygro…
Kaveh-Vakili Aug 17, 2026
f94f424
Merge branch 'dev' of https://github.com/SEMOSS/Semoss into Search-Ba…
tevanburen Aug 17, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -1495,42 +1495,61 @@ public static boolean doSetRoomToPinned(String userId, String roomId, boolean pi
/**
* Searches messages for a user and project by keyword. Handles message_data as
* a binary field (bytea/blob/varbinary). Converts/casts as necessary for each
* DB so text search via LIKE is possible.
* DB so text search via LIKE is possible. Results are grouped to one row per
* room in SQL before limit/offset are applied, so pagination operates on rooms
* rather than raw message rows.
*
* @param userId the user to search for
* @param projectId the project to search within
* @param projectId the project to search within, or null/blank to search all
* projects for the user
* @param keyword the text keyword to find in message bodies
* @return a list of matching messages (room_id, message_text, message_id)
* @return a list of matching rooms, one row per room (room_id, room_name, and
* date_created from that room's most recent matching message)
*/
public static List<Map<String, Object>> searchMessages(String userId, String projectId, String keyword) {
return searchMessages(userId, projectId, keyword, -1, 0, false, false);
}

public static List<Map<String, Object>> searchMessages(String userId, String projectId, String keyword,
long limit, long offset, boolean includeUnnamedRooms, boolean includeChildRooms) {
IRDBMSEngine modelInferenceLogsDb = SystemEngineRegistry.getModelInferenceLogsDb();
SelectQueryStruct qs = new SelectQueryStruct();

// Always select room_id and message_id
qs.addSelector(new QueryColumnSelector("ROOM__ROOM_ID", "room_id"));
qs.addSelector(new QueryColumnSelector("MESSAGE__MESSAGE_ID", "message_id"));
qs.addSelector(new QueryColumnSelector("ROOM__ROOM_NAME", "room_name"));
qs.addSelector(new QueryColumnSelector("ROOM__DATE_CREATED", "date_created"));

// Build a selector for message_text out of message_data, adapted to DB type
// Use the search-specific conversion so malformed searchable content cannot
// abort an otherwise unrelated room/project search.
QueryFunctionSelector messageTextSelector = modelInferenceLogsDb.getQueryUtil()
.getBlobToStringFunctionSelector(new QueryColumnSelector("MESSAGE__MESSAGE_DATA"), "message_text");
qs.addSelector(messageTextSelector);

// JOIN, filters, and ordering
qs.addRelation("MESSAGE__ROOM_ID", "ROOM__ROOM_ID", "left.join");
// JOIN, filters, grouping, and ordering
qs.addRelation("MESSAGE__ROOM_ID", "ROOM__ROOM_ID", "inner.join");
qs.addExplicitFilter(
SimpleQueryFilter.makeColToValFilter("ROOM__IS_ACTIVE", "==", true, PixelDataType.BOOLEAN));
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PROJECT_ID", "==", projectId));
if (projectId != null && !projectId.trim().isEmpty()) {
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PROJECT_ID", "==", projectId));
}
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__USER_ID", "==", userId));
if (!includeUnnamedRooms) {
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", null));
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", ""));
}
if (!includeChildRooms) {
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PARENT_ROOM_ID", "==", null));
}
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter(messageTextSelector,
"?like", keyword, PixelDataType.CONST_STRING));

// Add filter on decoded message text
qs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter(messageTextSelector, // use the computed selector (the
// decoded/casted field)
"?like", keyword.toLowerCase(), // (may want '?ilike' if framework supports, for case-insensitive)
PixelDataType.CONST_STRING));

qs.addOrderBy("ROOM__DATE_CREATED", "DESC");
qs.addOrderBy("MESSAGE__DATE_CREATED", "DESC");

qs.addOrderBy(new QueryColumnOrderBySelector("date_created", "DESC"));
qs.addOrderBy(new QueryColumnOrderBySelector("room_id", "DESC"));
if (limit > 0) {
qs.setLimit(limit);
}
if (offset > 0) {
qs.setOffSet(offset);
}
return QueryExecutionUtility.flushRsToMap(modelInferenceLogsDb, qs);
}

Expand Down Expand Up @@ -1788,11 +1807,18 @@ public static List<Map<String, Object>> doVerifyConversation(String userId, Stri
*/
public static List<Map<String, Object>> getUserConversations(String userId, String projectId, long limit,
long offset, String sortDir, String search, Boolean pinned) {
return getUserConversations(userId, projectId, limit, offset, sortDir, search, pinned, null);
return getUserConversations(userId, projectId, limit, offset, sortDir, search, pinned, null, false, false);
}

public static List<Map<String, Object>> getUserConversations(String userId, String projectId, long limit,
long offset, String sortDir, String search, Boolean pinned, String roomOptionsSearch) {
return getUserConversations(userId, projectId, limit, offset, sortDir, search, pinned, roomOptionsSearch,
false, false);
}

public static List<Map<String, Object>> getUserConversations(String userId, String projectId, long limit,
long offset, String sortDir, String search, Boolean pinned, String roomOptionsSearch,
boolean includeUnnamedRooms, boolean includeChildRooms) {
IRDBMSEngine modelInferenceLogsDb = SystemEngineRegistry.getModelInferenceLogsDb();
SelectQueryStruct qs = new SelectQueryStruct();
qs.addSelector(new QueryColumnSelector("ROOM__ROOM_ID"));
Expand All @@ -1812,6 +1838,13 @@ public static List<Map<String, Object>> getUserConversations(String userId, Stri
if (projectId != null) {
subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PROJECT_ID", "==", projectId));
}
if (!includeUnnamedRooms) {
subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", null));
subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__ROOM_NAME", "!=", ""));
}
if (!includeChildRooms) {
subQs.addExplicitFilter(SimpleQueryFilter.makeColToValFilter("ROOM__PARENT_ROOM_ID", "==", null));
}
qs.addExplicitFilter(SimpleQueryFilter.makeColToSubQuery("ROOM__ROOM_ID", "==", subQs));

// SEARCH
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@
import prerna.sablecc2.om.ReactorKeysEnum;
import prerna.sablecc2.om.nounmeta.NounMetadata;

public class GetUserConversationRoomsReactor extends AbstractReactor {

public GetUserConversationRoomsReactor() {
this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), ReactorKeysEnum.LIMIT.getKey(),
ReactorKeysEnum.OFFSET.getKey(), ReactorKeysEnum.SEARCH.getKey(), ReactorKeysEnum.SORT.getKey(),
ReactorKeysEnum.PINNED.getKey(), "roomOptionsSearch" };
this.keyRequired = new int[] { 0, 0, 0, 0, 0, 0, 0 };
public class GetUserConversationRoomsReactor extends AbstractReactor {
private static final String INCLUDE_UNNAMED_ROOMS = "includeUnnamedRooms";
private static final String INCLUDE_CHILD_ROOMS = "includeChildRooms";

public GetUserConversationRoomsReactor() {
this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), ReactorKeysEnum.LIMIT.getKey(),
ReactorKeysEnum.OFFSET.getKey(), ReactorKeysEnum.SEARCH.getKey(), ReactorKeysEnum.SORT.getKey(),
ReactorKeysEnum.PINNED.getKey(), "roomOptionsSearch", INCLUDE_UNNAMED_ROOMS, INCLUDE_CHILD_ROOMS };
this.keyRequired = new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
}

@Override
Expand All @@ -53,10 +55,13 @@ public NounMetadata execute() {
if (user == null) {
throw new IllegalArgumentException("You are not properly logged in");
}
String projectId = this.keyValue.get(this.keysToGet[0]);
if (projectId == null) {
projectId = this.insight.getContextProjectId();
}
String projectId = this.keyValue.get(this.keysToGet[0]);
if (projectId == null) {
projectId = this.insight.getContextProjectId();
}
if (projectId == null) {
projectId = this.insight.getProjectId();
}

long limit = getLong(ReactorKeysEnum.LIMIT.getKey(), -1L);
long offset = getLong(ReactorKeysEnum.OFFSET.getKey(), -1L);
Expand All @@ -68,7 +73,11 @@ public NounMetadata execute() {
sortDir = "DESC";
}

String search = this.keyValue.get(ReactorKeysEnum.SEARCH.getKey());
String search = this.keyValue.get(ReactorKeysEnum.SEARCH.getKey());
if (search != null && !search.trim().isEmpty()
&& (projectId == null || projectId.trim().isEmpty())) {
throw new IllegalArgumentException("A project must be provided or available from the current insight");
}

// Optional pinned filter: true/false to filter, null/absent to ignore
Boolean pinned = null;
Expand All @@ -79,17 +88,19 @@ public NounMetadata execute() {

// Optional free-text search against the OPTIONS JSON column.
String roomOptionsSearch = this.keyValue.get("roomOptionsSearch");
if (roomOptionsSearch != null) {
if (roomOptionsSearch != null) {
roomOptionsSearch = roomOptionsSearch.trim();
if (roomOptionsSearch.isEmpty()) {
roomOptionsSearch = null;
}
}

// Call new overload of getUserConversations
List<Map<String, Object>> output = ModelInferenceLogsUtils.getUserConversations(
user.getPrimaryLoginToken().getId(), projectId, limit, offset, sortDir, search, pinned,
roomOptionsSearch);
}
boolean includeUnnamedRooms = getBoolean(INCLUDE_UNNAMED_ROOMS, false);
boolean includeChildRooms = getBoolean(INCLUDE_CHILD_ROOMS, false);

// Call new overload of getUserConversations
List<Map<String, Object>> output = ModelInferenceLogsUtils.getUserConversations(
user.getPrimaryLoginToken().getId(), projectId, limit, offset, sortDir, search, pinned,
roomOptionsSearch, includeUnnamedRooms, includeChildRooms);

return new NounMetadata(output, PixelDataType.VECTOR);
}
Expand All @@ -113,8 +124,12 @@ protected String getDescriptionForKey(String key) {
return "Sort direction by room creation date. Accepts ASC or DESC (default is DESC).";
} else if (ReactorKeysEnum.PINNED.getKey().equals(key)) {
return "Optional pinned filter: true for pinned rooms only, false for unpinned rooms only, omit for no pinned filter.";
} else if ("roomOptionsSearch".equals(key)) {
return "Optional free-text search term applied against the room's options JSON. Any room whose options contain this substring is included.";
} else if ("roomOptionsSearch".equals(key)) {
return "Optional free-text search term applied against the room's options JSON. Any room whose options contain this substring is included.";
} else if (INCLUDE_UNNAMED_ROOMS.equals(key)) {
return "Whether to include rooms with a null or empty name. Defaults to false.";
} else if (INCLUDE_CHILD_ROOMS.equals(key)) {
return "Whether to include rooms that have a parent room. Defaults to false.";
}
return super.getDescriptionForKey(key);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,61 +43,107 @@
public class SearchRoomMessagesReactor extends AbstractReactor {
private static final Logger classLogger = LogManager.getLogger(SearchRoomMessagesReactor.class);

private static final String INCLUDE_UNNAMED_ROOMS = "includeUnnamedRooms";
private static final String INCLUDE_CHILD_ROOMS = "includeChildRooms";

public SearchRoomMessagesReactor() {
// this expects projectId and search term
this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), ReactorKeysEnum.SEARCH.getKey(), };
this.keyRequired = new int[] { 0, 1 };
this.keysToGet = new String[] {
ReactorKeysEnum.PROJECT.getKey(),
ReactorKeysEnum.SEARCH.getKey(),
ReactorKeysEnum.LIMIT.getKey(),
ReactorKeysEnum.OFFSET.getKey(),
INCLUDE_UNNAMED_ROOMS,
INCLUDE_CHILD_ROOMS
};
this.keyRequired = new int[] { 0, 1, 0, 0, 0, 0 };
}

@Override
public NounMetadata execute() {
organizeKeys();

// Get user
User user = this.insight.getUser();
if (user == null) {
throw new IllegalArgumentException("You are not properly logged in");
}
String userId = user.getPrimaryLoginToken().getId();

// Get projectId
String projectId = this.keyValue.get(this.keysToGet[0]);
if (projectId == null) {
if (projectId == null || projectId.trim().isEmpty()) {
projectId = this.insight.getContextProjectId();
}
if (projectId == null || projectId.trim().isEmpty()) {
projectId = this.insight.getProjectId();
}
if (projectId == null || projectId.trim().isEmpty()) {
projectId = null;
}

// Get keyword
String keyword = this.keyValue.get(this.keysToGet[1]);
if (keyword == null || keyword.trim().isEmpty()) {
throw new IllegalArgumentException("Search keyword must be provided");
}

// Query messages
Long requestedLimit;
Long requestedOffset;
try {
requestedLimit = getLong(ReactorKeysEnum.LIMIT.getKey());
requestedOffset = getLong(ReactorKeysEnum.OFFSET.getKey());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Limit and offset must be whole numbers", e);
}
if (requestedLimit != null && requestedLimit <= 0) {
throw new IllegalArgumentException("Limit must be greater than zero");
}
if (requestedOffset != null && requestedOffset < 0) {
throw new IllegalArgumentException("Offset must be zero or greater");
}
if (requestedOffset != null && requestedOffset > 0 && requestedLimit == null) {
throw new IllegalArgumentException("A positive limit is required when offset is provided");
}
long limit = requestedLimit == null ? -1L : requestedLimit;
long offset = requestedOffset == null ? 0L : requestedOffset;
boolean includeUnnamedRooms = getBoolean(INCLUDE_UNNAMED_ROOMS, false);
boolean includeChildRooms = getBoolean(INCLUDE_CHILD_ROOMS, false);

List<Map<String, Object>> results;
try {
results = ModelInferenceLogsUtils.searchMessages(userId, projectId, keyword);
results = ModelInferenceLogsUtils.searchMessages(userId, projectId, keyword, limit, offset,
includeUnnamedRooms, includeChildRooms);
} catch (Exception e) {
classLogger.error("Error searching room messages", e);
throw new RuntimeException("Could not search room messages: " + e.getMessage(), e);
}

// Return results as VECTOR
return new NounMetadata(results, PixelDataType.VECTOR);
}

@Override
public String getReactorDescription() {
return "This reactor searches through the messages in the user's conversation rooms for a given keyword within a specified project. "
+ "It returns the matching messages, including relevant details such as room ID, message text, and message ID, for rooms the user has access to.";
return "Searches through the messages in the user's conversation rooms for a given keyword. "
+ "Returns one row per matching room: room_id, room_name, and the latest matching date_created. "
+ "Case-insensitive matching is handled by the query framework's ?like comparator. "
+ "Unnamed and child rooms are excluded unless explicitly included. "
+ "Falls back to the current insight's context/project when projectId is omitted; "
+ "if none is available, searches all projects for the user. "
+ "Supports limit and offset for pagination over matching rooms.";
}

@Override
protected String getDescriptionForKey(String key) {
if (key.equals(ReactorKeysEnum.PROJECT.getKey())) {
return "The project ID for which to search room messages. If no project ID is passed, then all rooms for the user will be searched.";
return "Optional project ID to scope the search. Falls back to the current insight's project, then searches all projects for the user if no project is available.";
} else if (key.equals(ReactorKeysEnum.SEARCH.getKey())) {
return "The search term to use to search for within the messages. All messages containing this text (case-insensitive) will be returned.";
return "The keyword to search for within message content (case-insensitive).";
} else if (key.equals(ReactorKeysEnum.LIMIT.getKey())) {
return "Maximum number of results to return. Defaults to no cap when omitted.";
} else if (key.equals(ReactorKeysEnum.OFFSET.getKey())) {
return "Number of results to skip for pagination.";
} else if (key.equals(INCLUDE_UNNAMED_ROOMS)) {
return "Whether to include rooms with a null or empty name. Defaults to false.";
} else if (key.equals(INCLUDE_CHILD_ROOMS)) {
return "Whether to include rooms that have a parent room. Defaults to false.";
}
return super.getDescriptionForKey(key);
}
}
}
Loading