Skip to content

Commit 6bd6f34

Browse files
committed
Introduce strongly-typed MessageIndexDocument case class
Replace Map[String, AnyRef] with a strongly-typed MessageIndexDocument case class for OpenSearch document storage and retrieval. This improves type safety and makes the code more maintainable. Changes: - Add MessageIndexDocument case class with proper field types - Use Option[String] for optional title field - Use Seq[String] for tags field (replacing Java List) - Add @JsonProperty annotations for snake_case field mapping - Update OpenSearchIndexService, SearchService, and MoreLikeThisService to use the new case class
1 parent f0d4189 commit 6bd6f34

4 files changed

Lines changed: 88 additions & 58 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 1998-2026 Linux.org.ru
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package ru.org.linux.search
17+
18+
import com.fasterxml.jackson.annotation.JsonProperty
19+
20+
case class MessageIndexDocument(
21+
@JsonProperty("section") section: String,
22+
@JsonProperty("topic_author") topicAuthor: String,
23+
@JsonProperty("topic_id") topicId: Int,
24+
@JsonProperty("author") author: String,
25+
@JsonProperty("group") group: String,
26+
@JsonProperty("title") title: Option[String],
27+
@JsonProperty("topic_title") topicTitle: String,
28+
@JsonProperty("message") message: String,
29+
@JsonProperty("postdate") postdate: String,
30+
@JsonProperty("tag") tags: Seq[String],
31+
@JsonProperty("is_comment") isComment: Boolean,
32+
@JsonProperty("topic_awaits_commit") topicAwaitsCommit: Boolean
33+
)

src/main/scala/ru/org/linux/search/MoreLikeThisService.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ class MoreLikeThisService(
8080
breaker.withCircuitBreaker {
8181
val request = makeQuery(topic, tags)
8282

83-
val result: Future[Result] = client.search(request, classOf[java.util.Map[String, AnyRef]])
83+
val result: Future[Result] = client.search(request, classOf[MessageIndexDocument])
8484
.asScala
8585
.map { response =>
8686
val hits = response.hits.hits.asScala
@@ -153,17 +153,17 @@ class MoreLikeThisService(
153153
}
154154
}
155155

156-
private def processHit(hit: Hit[java.util.Map[String, AnyRef]]): MoreLikeThisTopic = {
156+
private def processHit(hit: Hit[MessageIndexDocument]): MoreLikeThisTopic = {
157157
val source = hit.source
158-
val section = source.get("section").asInstanceOf[String]
159-
val group = source.get("group").asInstanceOf[String]
158+
val section = source.section
159+
val group = source.group
160160

161161
val builder = UriComponentsBuilder.fromPath("/{section}/{group}/{msgid}")
162162
val link = builder.buildAndExpand(section, group, Integer.parseInt(hit.id)).toUriString
163163

164-
val postdate = Instant.parse(source.get("postdate").asInstanceOf[String])
164+
val postdate = Instant.parse(source.postdate)
165165

166-
val title = source.get("title").asInstanceOf[String]
166+
val title = source.title.getOrElse("")
167167

168168
MoreLikeThisTopic(
169169
title = StringUtil.processTitle(StringUtil.escapeHtml(title)),

src/main/scala/ru/org/linux/search/OpenSearchIndexService.scala

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ class OpenSearchIndexService(sectionService: SectionService, groupDao: GroupDao,
8383

8484
private def indexCommentToBulkOp(topic: Topic, comment: Comment, group: Group): BulkOperation = {
8585
val doc = indexOfComment(topic, comment, group)
86-
val indexOp = IndexOperation.of[java.util.Map[String, Any]](i => i
86+
val indexOp = IndexOperation.of[MessageIndexDocument](i => i
8787
.index(MessageIndex)
8888
.id(comment.id.toString)
89-
.document(doc.asJava))
89+
.document(doc))
9090
BulkOperation.of(op => op.index(indexOp))
9191
}
9292

@@ -97,10 +97,10 @@ class OpenSearchIndexService(sectionService: SectionService, groupDao: GroupDao,
9797
if (topicPermissionService.isTopicSearchable(topic, group)) {
9898
val topicDoc = indexOfTopic(topic, group)
9999

100-
val indexOp = IndexOperation.of[java.util.Map[String, Any]](i => i
100+
val indexOp = IndexOperation.of[MessageIndexDocument](i => i
101101
.index(MessageIndex)
102102
.id(topic.id.toString)
103-
.document(topicDoc.asJava))
103+
.document(topicDoc))
104104

105105
val operations = Seq(
106106
BulkOperation.of(op => op.index(indexOp))
@@ -155,7 +155,7 @@ class OpenSearchIndexService(sectionService: SectionService, groupDao: GroupDao,
155155
}
156156
}
157157

158-
private def indexOfComment(topic: Topic, comment: Comment, group: Group): Map[String, Any] = {
158+
private def indexOfComment(topic: Topic, comment: Comment, group: Group): MessageIndexDocument = {
159159
val section = sectionService.getSection(topic.sectionId)
160160
val author = userService.getUserCached(comment.userid)
161161
val topicAuthor = userService.getUserCached(topic.authorUserId)
@@ -174,23 +174,20 @@ class OpenSearchIndexService(sectionService: SectionService, groupDao: GroupDao,
174174
.filterNot(_.startsWith("Re:"))
175175
.map(StringEscapeUtils.unescapeHtml4)
176176

177-
val fields = scala.collection.mutable.Map[String, Any](
178-
"section" -> section.getUrlName,
179-
"topic_author" -> topicAuthor.nick,
180-
"topic_id" -> topic.id,
181-
"author" -> author.nick,
182-
"group" -> group.urlName,
183-
"topic_title" -> topicTitle,
184-
COLUMN_TOPIC_AWAITS_COMMIT -> topicAwaitsCommit(topic),
185-
"message" -> html,
186-
"postdate" -> comment.postdate.toInstant.toString,
187-
"tag" -> topicTagService.getTags(topic),
188-
"is_comment" -> true
177+
MessageIndexDocument(
178+
section = section.getUrlName,
179+
topicAuthor = topicAuthor.nick,
180+
topicId = topic.id,
181+
author = author.nick,
182+
group = group.urlName,
183+
title = title,
184+
topicTitle = topicTitle,
185+
message = html,
186+
postdate = comment.postdate.toInstant.toString,
187+
tags = topicTagService.getTags(topic),
188+
isComment = true,
189+
topicAwaitsCommit = topicAwaitsCommit(topic)
189190
)
190-
191-
title.foreach(t => fields("title") = t)
192-
193-
fields.toMap
194191
}
195192

196193
private def topicAwaitsCommit(msg: Topic) = {
@@ -199,7 +196,7 @@ class OpenSearchIndexService(sectionService: SectionService, groupDao: GroupDao,
199196
section.isPremoderated && !msg.commited
200197
}
201198

202-
private def indexOfTopic(topic: Topic, group: Group): Map[String, Any] = {
199+
private def indexOfTopic(topic: Topic, group: Group): MessageIndexDocument = {
203200
val section = sectionService.getSection(topic.sectionId)
204201
val author = userService.getUserCached(topic.authorUserId)
205202

@@ -211,19 +208,19 @@ class OpenSearchIndexService(sectionService: SectionService, groupDao: GroupDao,
211208
nofollow = !topicPermissionService.followInTopic(topic, author),
212209
canonicalUrl = url)
213210

214-
Map(
215-
"section" -> section.getUrlName,
216-
"topic_author" -> author.nick,
217-
"topic_id" -> topic.id,
218-
"author" -> author.nick,
219-
"group" -> group.urlName,
220-
"title" -> topic.getTitleUnescaped,
221-
"topic_title" -> topic.getTitleUnescaped,
222-
"message" -> html,
223-
"postdate" -> topic.postdate.toInstant.toString,
224-
"tag" -> topicTagService.getTags(topic),
225-
COLUMN_TOPIC_AWAITS_COMMIT -> topicAwaitsCommit(topic),
226-
"is_comment" -> false
211+
MessageIndexDocument(
212+
section = section.getUrlName,
213+
topicAuthor = author.nick,
214+
topicId = topic.id,
215+
author = author.nick,
216+
group = group.urlName,
217+
title = Some(topic.getTitleUnescaped),
218+
topicTitle = topic.getTitleUnescaped,
219+
message = html,
220+
postdate = topic.postdate.toInstant.toString,
221+
tags = topicTagService.getTags(topic),
222+
isComment = false,
223+
topicAwaitsCommit = topicAwaitsCommit(topic)
227224
)
228225
}
229226
}

src/main/scala/ru/org/linux/search/SearchService.scala

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ class SearchService(elastic: OpenSearchClient, userService: UserService, siteCon
171171
.trackTotalHits(t => t.enabled(true))
172172
.build()
173173

174-
val response = elastic.search(request, classOf[util.Map[String, AnyRef]])
174+
val response = elastic.search(request, classOf[MessageIndexDocument])
175175
buildResponse(query, response)
176176
}
177177

178-
private def buildResponse(query: SearchServiceRequest, response: SearchResponse[util.Map[String, AnyRef]]): SearchServiceResponse = {
178+
private def buildResponse(query: SearchServiceRequest, response: SearchResponse[MessageIndexDocument]): SearchServiceResponse = {
179179
var sectionFacetResponse: Option[Seq[FacetItem]] = None
180180
var groupFacetResponse: Option[Seq[FacetItem]] = None
181181
var foundTagsResponse: Option[Seq[TagRef]] = None
@@ -227,19 +227,19 @@ class SearchService(elastic: OpenSearchClient, userService: UserService, siteCon
227227
}
228228
}
229229

230-
private def prepare(doc: Hit[util.Map[String, AnyRef]]): SearchItem = {
230+
private def prepare(doc: Hit[MessageIndexDocument]): SearchItem = {
231231
val source = doc.source
232232

233-
val author = userService.getUserCached(source.get("author").asInstanceOf[String])
233+
val author = userService.getUserCached(source.author)
234234

235-
val postdate = Instant.parse(source.get("postdate").asInstanceOf[String])
235+
val postdate = Instant.parse(source.postdate)
236236

237-
val comment = source.get("is_comment").asInstanceOf[Boolean]
237+
val comment = source.isComment
238238

239239
val tags: Seq[TagRef] = if (comment) {
240240
Seq.empty
241241
} else {
242-
source.get("tag").asInstanceOf[Seq[String]].map(tag => TagService.tagRef(tag))
242+
source.tags.map(tag => TagService.tagRef(tag))
243243
}
244244

245245
val docScore = if (doc.score != null) doc.score.toFloat else 0f
@@ -256,37 +256,37 @@ class SearchService(elastic: OpenSearchClient, userService: UserService, siteCon
256256
)
257257
}
258258

259-
private def getTitle(doc: Hit[util.Map[String, AnyRef]]): String = {
259+
private def getTitle(doc: Hit[MessageIndexDocument]): String = {
260260
val highlight = Option(doc.highlight).map(_.asScala.toMap).getOrElse(Map.empty)
261261
val source = doc.source
262262

263263
val itemTitle = highlight.get("title").flatMap(_.asScala.headOption)
264-
.orElse(Option(source.get("title")).map { v => StringUtil.escapeHtml(v.asInstanceOf[String]) })
264+
.orElse(source.title.map { v => StringUtil.escapeHtml(v) })
265265

266266
itemTitle.filter(_.trim.nonEmpty).orElse(
267267
highlight.get("topic_title").flatMap(_.asScala.headOption))
268-
.getOrElse(StringUtil.escapeHtml(source.get("topic_title").asInstanceOf[String]))
268+
.getOrElse(StringUtil.escapeHtml(source.topicTitle))
269269
}
270270

271-
private def getMessage(doc: Hit[util.Map[String, AnyRef]]): String = {
271+
private def getMessage(doc: Hit[MessageIndexDocument]): String = {
272272
val highlight = Option(doc.highlight).map(_.asScala.toMap).getOrElse(Map.empty)
273273
val source = doc.source
274274

275275
val html = highlight.get("message").flatMap(_.asScala.headOption) getOrElse {
276-
source.get("message").asInstanceOf[String].take(MessageFragment)
276+
source.message.take(MessageFragment)
277277
}
278278

279279
Jsoup.clean(html, siteConfig.getSecureUrl, TextSafelist)
280280
}
281281

282-
private def getUrl(doc: Hit[util.Map[String, AnyRef]]): String = {
282+
private def getUrl(doc: Hit[MessageIndexDocument]): String = {
283283
val source = doc.source
284-
val section = source.get("section").asInstanceOf[String]
284+
val section = source.section
285285
val msgid = doc.id
286286

287-
val comment = source.get("is_comment").asInstanceOf[Boolean]
288-
val topic = source.get("topic_id").asInstanceOf[Int]
289-
val group = source.get("group").asInstanceOf[String]
287+
val comment = source.isComment
288+
val topic = source.topicId
289+
val group = source.group
290290

291291
if (comment) {
292292
val builder = UriComponentsBuilder.fromPath("/{section}/{group}/{msgid}?cid={cid}")

0 commit comments

Comments
 (0)