Skip to content

SAK-52258 Samigo Refactor question search functionality to use QuestionSearchService#14354

Draft
kunaljaykam wants to merge 9 commits intosakaiproject:masterfrom
kunaljaykam:SAK-52258
Draft

SAK-52258 Samigo Refactor question search functionality to use QuestionSearchService#14354
kunaljaykam wants to merge 9 commits intosakaiproject:masterfrom
kunaljaykam:SAK-52258

Conversation

@kunaljaykam
Copy link
Member

@kunaljaykam kunaljaykam commented Jan 30, 2026

Summary by CodeRabbit

Release Notes

  • Improvements
    • Refactored question search functionality for improved usability and performance
    • Updated search results display with enhanced metadata presentation
    • Improved visual formatting of question origins and tags in search results

✏️ Tip: You can customize this high-level summary in your review settings.

kunaljaykam and others added 9 commits December 28, 2025 16:19
The removal of searchResponse() from SearchService API will be done
in a separate PR (stmp-1) after this PR is merged.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These modules need OpenSearch dependency to mock SearchService until
PR2 (stmp-1) removes searchResponse() from the SearchService interface.

After PR2 is merged, these dependencies can be removed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 30, 2026

Walkthrough

This pull request introduces a new QuestionSearchService abstraction layer backed by Elasticsearch/OpenSearch, replacing the previous search implementation. The SearchQuestionBean is refactored to delegate to this service and use a new QuestionSearchResult data model. Dependencies are reorganized across modules, and the JSP search results view is updated.

Changes

Cohort / File(s) Summary
Build Configuration
samigo/samigo-app/pom.xml, samigo/samigo-services/pom.xml, search/elasticsearch/impl/pom.xml
Removed search-api and opensearch dependencies from samigo-app; added them to samigo-services. Added maven-jar-plugin configuration to elasticsearch/impl to produce classifier artifact.
Question Search Service
samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/question/QuestionSearchService.java, samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/question/QuestionSearchServiceImpl.java
New search interface with four methods: searchByTags, searchByText, userOwnsQuestion, getQuestionOrigins. Implementation uses Elasticsearch/OpenSearch with comprehensive error handling and result processing, including origin extraction and caching support.
Search Result Model
samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/question/QuestionSearchResult.java
New Lombok-annotated value object with fields: id, typeId, questionText, tags, questionPoolId, assessmentId, siteId. Includes predicates isFromQuestionPool() and isFromAssessment().
Search Bean Refactoring
samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/bean/author/SearchQuestionBean.java
Major refactor replacing ItemSearchResult with QuestionSearchResult. Added service dependencies and constants (EDIT_POOL, EDIT_ASSESSMENT). Introduced caching mechanisms (titleCache, questionsIOwn) for origins/ownership. Updated search logic to delegate to QuestionSearchService and simplified result handling.
Application Configuration
samigo/samigo-app/src/webapp/WEB-INF/applicationContext.xml
Added QuestionSearchService bean definition with wired dependencies: elasticSearchService, siteService, questionPoolService, assessmentService.
UI Layer
samigo/samigo-app/src/webapp/jsf/author/searchResults.jsp
Updated to use QuestionSearchResult properties: idString → id, qText → questionText, tagSet.toArray() → tags.toArray(). Replaced origin dataList with searchQuestionBean.getOriginDisplay() method call.

Suggested reviewers

  • ottenhoff
  • jesusmmp
  • ern
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main refactoring work: replacing legacy search functionality with a new QuestionSearchService across the Samigo assessment tool.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

🧪 Unit Test Generation v2 is now available!

We have significantly improved our unit test generation capabilities.

To enable: Add this to your .coderabbit.yaml configuration:

reviews:
  finishing_touches:
    unit_tests:
      enabled: true

Try it out by using the @coderabbitai generate unit tests command on your code files or under ✨ Finishing Touches on the walkthrough!

Have feedback? Share your thoughts on our Discord thread!


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/question/QuestionSearchServiceImpl.java`:
- Around line 158-189: The method processSearchResponse may NPE when
hit.field("tags"), hit.field("typeId"), or hit.field("qText") return null;
before calling getValues() or getValue() check the Field/DocumentField is
non-null and only then extract values (e.g., verify hit.field("tags") != null
before calling getValues(), and similarly guard hit.field("typeId") and
hit.field("qText")), falling back to empty set or null/empty string as
appropriate to match how questionPoolId/assessmentId/site are handled later in
processSearchResponse; update the code paths that build tags, typeId, and qText
to follow the same null-check pattern used for questionPoolId/assessmentId/site
to avoid dropping valid hits.
🧹 Nitpick comments (3)
samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/question/QuestionSearchServiceImpl.java (1)

58-84: Optional: extract shared search-parameter setup.
searchByTags and searchByText duplicate the same base params (group, scope, subtype, logic). A small helper could reduce repetition.

samigo/samigo-app/src/java/org/sakaiproject/tool/assessment/ui/bean/author/SearchQuestionBean.java (2)

104-118: Avoid double Optional lookup for tag retrieval.

tagService.getTags().getForId(tagId) is called twice per iteration—once for isPresent() and again for get(). This is inefficient and risks a race condition if the tag is removed between calls.

♻️ Proposed fix using ifPresent or direct Optional handling
         if (tagList != null) {
             boolean first = true;
             for (String tagId : tagList) {
-                if (tagService.getTags().getForId(tagId).isPresent()) {
-                    Tag tag = tagService.getTags().getForId(tagId).get();
+                Optional<Tag> optionalTag = tagService.getTags().getForId(tagId);
+                if (optionalTag.isPresent()) {
+                    Tag tag = optionalTag.get();
                     String fullLabel = tag.getTagLabel() + "(" + tag.getCollectionName() + ")";
                     tagLabelsForSearch.add(fullLabel);
                     if (!first) {

You'll need to add import java.util.Optional; at the top of the file.


215-225: Consider caching ItemService instead of instantiating on each call.

Both getItem and getData create a new ItemService() instance per invocation. If these methods are called repeatedly (e.g., in JSP iteration over search results), this could be inefficient.

♻️ Proposed refactor to cache ItemService

Add as an instance field alongside the other services:

     private QuestionPoolService questionPoolService = new QuestionPoolService();
     private AssessmentService assessmentService = new AssessmentService();
+    private ItemService itemService = new ItemService();

Then update the methods:

     public ItemFacade getItem(String itemId) {
-        return new ItemService().getItem(itemId);
+        return itemService.getItem(itemId);
     }

     public ItemDataIfc getData(String itemId) {
-        ItemFacade item = new ItemService().getItem(itemId);
+        ItemFacade item = itemService.getItem(itemId);
         if (item == null) {
             return null;
         }
         return item.getData();
     }

Comment on lines +158 to +189
private List<QuestionSearchResult> processSearchResponse(SearchResponse sr) {
List<QuestionSearchResult> results = new ArrayList<>();

Terms dedup = sr.getAggregations().get("dedup");
if (dedup == null) {
return results;
}

for (Terms.Bucket entry : dedup.getBuckets()) {
TopHits topHits = entry.getAggregations().get("dedup_docs");
if (topHits == null) {
continue;
}

for (SearchHit hit : topHits.getHits().getHits()) {
try {
String id = hit.getId();
if (!id.startsWith("/sam_item/")) {
continue;
}

String itemId = id.substring(10);
String typeId = hit.field("typeId").getValue();
String qText = hit.field("qText").getValue();

Set<String> tags = new HashSet<>();
List<Object> tagValues = hit.field("tags").getValues();
if (tagValues != null) {
for (Object tagValue : tagValues) {
tags.add(tagValue.toString());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the actual file
fd -t f "QuestionSearchServiceImpl.java" --exec cat -n {} \;

Repository: sakaiproject/sakai

Length of output: 13187


🏁 Script executed:

# Check the pom.xml or dependencies to understand Elasticsearch version
fd -t f "pom.xml" | head -5 | xargs -I {} grep -A 5 -B 5 "elasticsearch" {} 2>/dev/null | head -50

Repository: sakaiproject/sakai

Length of output: 44


🏁 Script executed:

# Search for usage patterns of hit.field() in the codebase
rg "hit\.field\(" -A 2 -B 2 samiago

Repository: sakaiproject/sakai

Length of output: 109


🏁 Script executed:

# Search for other usages of hit.field() in samiago directory
rg "hit\.field\(" samigo -A 1 -B 1 -c

Repository: sakaiproject/sakai

Length of output: 178


🌐 Web query:

OpenSearch SearchHit field() method null behavior documentation

💡 Result:

In OpenSearch server (High Level REST client / server-side classes), org.opensearch.search.SearchHit#field(String fieldName) returns the DocumentField for that name if it was actually returned on the hit (i.e., you requested stored fields / fields / script fields, etc.). The Javadoc describes it only as “The hit field matching the given field name” and does not explicitly document nullability. In practice, if the field isn’t present in the hit’s fields map, the result is null (same behavior you’d expect from a map lookup). [1]

Related: SearchHit#getFields() is “a map of hit fields … if additional fields were required to be loaded” (so if you didn’t request any, don’t assume the map contains your field). [1]

In the opensearch-java (new Java client), the per-hit fields() accessor returns a Map<String, JsonData>; if you didn’t request fields (or a specific field wasn’t returned), it will simply be absent from the map (so looking it up yields null). [2]

Sources
[1] OpenSearch server Javadoc: org.opensearch.search.SearchHit (includes field(String) and getFields() docs) https://javadoc.io/static/org.opensearch/opensearch/1.2.4/org/opensearch/search/SearchHit.html
[2] OpenSearch Java client Javadoc: org.opensearch.client.opensearch.core.search.Hit#fields() https://www.javadoc.io/static/org.opensearch.client/opensearch-java/2.17.0/org/opensearch/client/opensearch/core/search/Hit.html


Add null check before calling getValues() on the tags field.

The hit.field("tags") may return null if the field is missing from the hit, and calling .getValues() on null throws a NullPointerException. This causes valid search results to be dropped silently. The same pattern should also be applied to typeId and qText (lines 180–181) for consistency.

Fix for tags field
-                    List<Object> tagValues = hit.field("tags").getValues();
-                    if (tagValues != null) {
+                    List<Object> tagValues = null;
+                    if (hit.field("tags") != null) {
+                        tagValues = hit.field("tags").getValues();
+                    }
+                    if (tagValues != null) {
                         for (Object tagValue : tagValues) {
                             tags.add(tagValue.toString());
                         }
                     }

This pattern already exists in the method for questionPoolId, assessmentId, and site (lines 191–204) and should be applied consistently to all optional fields.

🤖 Prompt for AI Agents
In
`@samigo/samigo-services/src/java/org/sakaiproject/tool/assessment/services/question/QuestionSearchServiceImpl.java`
around lines 158 - 189, The method processSearchResponse may NPE when
hit.field("tags"), hit.field("typeId"), or hit.field("qText") return null;
before calling getValues() or getValue() check the Field/DocumentField is
non-null and only then extract values (e.g., verify hit.field("tags") != null
before calling getValues(), and similarly guard hit.field("typeId") and
hit.field("qText")), falling back to empty set or null/empty string as
appropriate to match how questionPoolId/assessmentId/site are handled later in
processSearchResponse; update the code paths that build tags, typeId, and qText
to follow the same null-check pattern used for questionPoolId/assessmentId/site
to avoid dropping valid hits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant