Skip to content

Commit d82f7ba

Browse files
committed
bookmark highlighting frontend
1 parent 8cda14f commit d82f7ba

10 files changed

Lines changed: 130 additions & 78 deletions

File tree

frontend/components/Bookmark/BookmarkCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export default function BookmarkCard({ bookmark }: Readonly<BookmarkProp>) {
218218
return (
219219
<CardBody
220220
bookmark={currentBookmark.current}
221+
highlight={bookmark.textHighlight}
221222
inEditMode={inEditMode}
222223
edit={edit}
223224
changeEditMode={changeEditMode}

frontend/components/Bookmark/CardBody.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ import { ScrapableBookmarkToggle } from "./ScrapableToggle";
66

77
interface CardBodyProp {
88
bookmark: Readonly<Bookmark>;
9+
highlight: String | null;
910
inEditMode: Readonly<boolean>;
1011
edit: Readonly<RefObject<Bookmark>>;
1112
changeEditMode: Readonly<Function>;
1213
}
1314

1415
export default function CardBody({
1516
bookmark,
17+
highlight,
1618
inEditMode,
1719
edit,
1820
changeEditMode,
1921
}: Readonly<CardBodyProp>) {
2022
const [isScrapable, setIsScrapable] = useState(bookmark.scrapable);
23+
2124
return (
2225
<Card.Body>
2326
<Card.Title>
@@ -62,6 +65,12 @@ export default function CardBody({
6265
{bookmark.url}
6366
</Card.Link>
6467
)}
68+
{highlight ? (
69+
<div>
70+
<hr />
71+
<p dangerouslySetInnerHTML={{ __html: highlight }}></p>
72+
</div>
73+
) : null}
6574
</Card.Body>
6675
);
6776
}

frontend/types/Bookmarks/Bookmark.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export default interface Bookmark {
77
screenshotUrl: string;
88
tags: Tag[];
99
scrapable: boolean;
10+
textHighlight: string | null;
1011
}

server/src/main/java/dev/findfirst/core/dto/BookmarkDTO.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,33 @@
33
import java.util.Date;
44
import java.util.List;
55

6-
public record BookmarkDTO(long id, String title, String url, String screenshotUrl, boolean scrapable, Date createdDate, Date lastModifiedOn, List<TagOnly> tags) {
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
import lombok.AllArgsConstructor;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
import lombok.Setter;
11+
import lombok.experimental.Accessors;
12+
13+
@Accessors(fluent = true, chain = false)
14+
@Getter(onMethod = @__(@JsonProperty))
15+
@Setter
16+
@AllArgsConstructor
17+
@NoArgsConstructor
18+
public class BookmarkDTO {
19+
20+
private long id;
21+
private String title;
22+
private String url;
23+
private String screenshotUrl;
24+
private boolean scrapable;
25+
private Date createdDate;
26+
private Date lastModifiedOn;
27+
private List<TagOnly> tags;
28+
private String textHighlight;
29+
30+
public BookmarkDTO(long id, String title, String url, String screenshotUrl, boolean scrapable,
31+
Date createdDate, Date lastModifiedOn, List<TagOnly> tags) {
32+
// we don't have any highlighted text.
33+
this(id, title, url, screenshotUrl, scrapable, createdDate, lastModifiedOn, tags, null);
34+
}
735
}

server/src/main/java/dev/findfirst/core/service/BookmarkService.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ public Optional<BookmarkDTO> getBookmarkDTOById(Long id) {
112112
}
113113
}
114114

115+
public List<BookmarkDTO> convertBookmarkJDBCToDTO(List<BookmarkJDBC> bookmarkEntities,
116+
List<String> highlights, int userId) {
117+
var bks = convertBookmarkJDBCToDTO(bookmarkEntities, userId);
118+
119+
for (int i = 0; i < highlights.size() && i < bookmarkEntities.size(); i++) {
120+
bks.get(i).textHighlight(highlights.get(i));
121+
}
122+
return bks;
123+
}
124+
115125
public List<BookmarkDTO> convertBookmarkJDBCToDTO(List<BookmarkJDBC> bookmarkEntities,
116126
int userId) {
117127

server/src/main/java/dev/findfirst/core/service/SearchService.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import java.util.List;
44
import java.util.StringJoiner;
5+
import java.util.stream.Collectors;
56

67
import dev.findfirst.core.dto.BookmarkDTO;
78
import dev.findfirst.core.repository.jdbc.BookmarkJDBCRepository;
9+
import dev.findfirst.core.service.TypesenseService.SearchHighlightResult;
810
import dev.findfirst.security.userauth.context.UserContext;
911

1012
import lombok.RequiredArgsConstructor;
@@ -42,11 +44,18 @@ public List<BookmarkDTO> bookmarksByTags(List<String> tags) {
4244

4345
public List<BookmarkDTO> bookmarksByText(String text) {
4446
var userID = userContext.getUserId();
45-
var ids = typesense.search(text).stream()
46-
.map(TypesenseService.SearchHighlightResult::id)
47-
.toList();
48-
var bookmarks = bookmarkRepo.findAllById(ids);
49-
return bookmarkService.convertBookmarkJDBCToDTO(bookmarks, userID);
47+
List<SearchHighlightResult> searchHighlightResults = typesense.search(text);
48+
49+
record IdHighlight(List<Long> ids, List<String> hightlights) {
50+
}
51+
52+
IdHighlight ih = searchHighlightResults.stream()
53+
.collect(Collectors.teeing(Collectors.mapping(hit -> hit.id(), Collectors.toList()),
54+
Collectors.mapping(hit -> hit.highlight(), Collectors.toList()),
55+
(ids, highlights) -> new IdHighlight(ids, highlights)));
56+
57+
var bookmarks = bookmarkRepo.findAllById(ih.ids());
58+
return bookmarkService.convertBookmarkJDBCToDTO(bookmarks, ih.hightlights(), userID);
5059

5160
}
5261

server/src/main/java/dev/findfirst/core/service/TypesenseService.java

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
@Slf4j
2828
public class TypesenseService {
2929

30-
public record SearchHighlightResult(Long id, String highlight) {};
30+
public record SearchHighlightResult(Long id, String highlight) {
31+
};
32+
3133
private final TypsenseInitializationRepository initRepo;
3234

3335
private final Client client;
@@ -56,8 +58,7 @@ private CollectionSchema createCollectionSchemaSchema() {
5658

5759
private String saveSchema(TypesenseInitRecord initRecord) {
5860
try {
59-
CollectionResponse collectionResponse =
60-
client.collections().create(createCollectionSchemaSchema());
61+
CollectionResponse collectionResponse = client.collections().create(createCollectionSchemaSchema());
6162
log.debug(collectionResponse.toString());
6263
initRecord.setInitialized(true);
6364
initRepo.save(initRecord);
@@ -88,28 +89,20 @@ public void addText(BookmarkJDBC bookmark, Document retDoc) {
8889
}
8990

9091
public List<SearchHighlightResult> search(String text) {
91-
SearchParameters searchParameters = new SearchParameters()
92-
.q(text)
93-
.queryBy("text")
94-
.highlightFields("text")
95-
.highlightStartTag("<mark>")
96-
.highlightEndTag("<mark>");
92+
SearchParameters searchParameters = new SearchParameters().q(text).queryBy("text")
93+
.highlightFields("text").highlightStartTag("<mark>").highlightEndTag("</mark>");
9794
try {
9895
log.debug("searching");
99-
SearchResult searchResult =
100-
client.collections(schemaName).documents().search(searchParameters);
96+
SearchResult searchResult = client.collections(schemaName).documents().search(searchParameters);
10197
log.debug(searchResult.toString());
10298

103-
return searchResult.getHits().stream()
104-
.map(hit -> {
105-
Long id = Long.parseLong(hit.getDocument().get("id").toString());
106-
String highlight = hit.getHighlights().stream()
107-
.filter(h -> "text".equals(h.getField()))
108-
.map(h-> h.getSnippet())
109-
.findFirst()
110-
.orElse("");
111-
return new SearchHighlightResult(id, highlight);
112-
}).toList();
99+
return searchResult.getHits().stream().map(hit -> {
100+
Long id = Long.parseLong(hit.getDocument().get("id").toString());
101+
102+
String highlight = hit.getHighlights().stream().filter(h -> "text".equals(h.getField()))
103+
.map(h -> h.getSnippet()).findFirst().orElse("");
104+
return new SearchHighlightResult(id, highlight);
105+
}).toList();
113106
} catch (Exception e) {
114107
log.error(e.toString());
115108
}

server/src/test/java/dev/findfirst/core/model/RobotsTxtResponseTest.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,18 @@
99
import org.junit.jupiter.api.Test;
1010

1111
class RobotsTxtResponseTest {
12-
RobotsTxtResponse response200 = new RobotsFetcher.RobotsTxtResponse(200, "response from server".getBytes(), "json");
13-
RobotsTxtResponse response200Same = new RobotsFetcher.RobotsTxtResponse(200, "response from server".getBytes(),
14-
"json");
15-
RobotsTxtResponse response400 = new RobotsFetcher.RobotsTxtResponse(400, "response from server".getBytes(), "json");
16-
RobotsTxtResponse responseContentJson = new RobotsFetcher.RobotsTxtResponse(200, "new message".getBytes(), "json");
17-
RobotsTxtResponse responseContentHtml = new RobotsFetcher.RobotsTxtResponse(400, "response from server".getBytes(),
18-
"html");
19-
RobotsTxtResponse responseContent2 = new RobotsFetcher.RobotsTxtResponse(400, "response from server".getBytes(),
20-
null);
12+
RobotsTxtResponse response200 =
13+
new RobotsFetcher.RobotsTxtResponse(200, "response from server".getBytes(), "json");
14+
RobotsTxtResponse response200Same =
15+
new RobotsFetcher.RobotsTxtResponse(200, "response from server".getBytes(), "json");
16+
RobotsTxtResponse response400 =
17+
new RobotsFetcher.RobotsTxtResponse(400, "response from server".getBytes(), "json");
18+
RobotsTxtResponse responseContentJson =
19+
new RobotsFetcher.RobotsTxtResponse(200, "new message".getBytes(), "json");
20+
RobotsTxtResponse responseContentHtml =
21+
new RobotsFetcher.RobotsTxtResponse(400, "response from server".getBytes(), "html");
22+
RobotsTxtResponse responseContent2 =
23+
new RobotsFetcher.RobotsTxtResponse(400, "response from server".getBytes(), null);
2124

2225
@Test
2326
void robotsTxtResponseEquality() {

server/src/test/java/dev/findfirst/core/model/SearchBkmkByTitleReq.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,24 @@ class SearchBkmkByTitleReqTest {
1111

1212
@Test
1313
void titleRequestsEquality() {
14-
assertEquals(new SearchBkmkByTitleReq(new String[] { "java", "spring" }),
15-
new SearchBkmkByTitleReq(new String[] { "java", "spring" }));
16-
assertNotEquals(new SearchBkmkByTitleReq(new String[] { "java" }),
17-
new SearchBkmkByTitleReq(new String[] { "java", "spring" }));
18-
assertNotEquals(new SearchBkmkByTitleReq(new String[] { "java", "spring" }), (Object) null);
19-
assertNotEquals(new SearchBkmkByTitleReq(new String[] { "java", "spring" }), (Object) "String");
14+
assertEquals(new SearchBkmkByTitleReq(new String[] {"java", "spring"}),
15+
new SearchBkmkByTitleReq(new String[] {"java", "spring"}));
16+
assertNotEquals(new SearchBkmkByTitleReq(new String[] {"java"}),
17+
new SearchBkmkByTitleReq(new String[] {"java", "spring"}));
18+
assertNotEquals(new SearchBkmkByTitleReq(new String[] {"java", "spring"}), (Object) null);
19+
assertNotEquals(new SearchBkmkByTitleReq(new String[] {"java", "spring"}), (Object) "String");
2020
}
2121

2222
@Test
2323
void hashCodeTest() {
24-
var keywords = new String[] { "java", "spring" };
24+
var keywords = new String[] {"java", "spring"};
2525
var searchReq = new SearchBkmkByTitleReq(keywords);
2626
assertEquals(Arrays.hashCode(keywords), searchReq.hashCode());
2727
}
2828

2929
@Test
3030
void toStringTest() {
31-
var keywords = new String[] { "java", "spring" };
31+
var keywords = new String[] {"java", "spring"};
3232
var searchReq = new SearchBkmkByTitleReq(keywords);
3333
assertEquals(Arrays.toString(keywords), searchReq.toString());
3434

server/src/test/java/dev/findfirst/core/service/TypesenseServiceTest.java

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44
import static org.mockito.Mockito.*;
55

66
import java.util.Date;
7-
import java.util.HashMap;
87
import java.util.List;
98
import java.util.Map;
109

11-
import org.typesense.model.*;
12-
import org.typesense.api.Documents;
13-
import org.typesense.api.Collection;
14-
1510
import dev.findfirst.core.model.jdbc.TypesenseInitRecord;
1611
import dev.findfirst.core.repository.jdbc.TypsenseInitializationRepository;
1712

@@ -22,7 +17,10 @@
2217
import org.mockito.Mock;
2318
import org.mockito.junit.jupiter.MockitoExtension;
2419
import org.typesense.api.Client;
20+
import org.typesense.api.Collection;
2521
import org.typesense.api.Collections;
22+
import org.typesense.api.Documents;
23+
import org.typesense.model.*;
2624

2725
/**
2826
* Tests for typsense operations such as intitilization, queries/imports.
@@ -83,41 +81,41 @@ void initializationWasNotFinished() throws Exception {
8381
@Disabled("Implement test to save storeScrapedText")
8482
void storeScrapedText() throws Exception {}
8583

86-
@Test
87-
void returnSearchHighlighted() throws Exception {
84+
@Test
85+
void returnSearchHighlighted() throws Exception {
8886

89-
String query = "example";
90-
Long expectedId = 123L;
91-
String expectedSnippet = "<mark>example</mark> text around";
87+
String query = "example";
88+
Long expectedId = 123L;
89+
String expectedSnippet = "<mark>example</mark> text around";
9290

93-
// Mock highlight
94-
SearchHighlight highlight = mock(SearchHighlight.class);
95-
when(highlight.getField()).thenReturn("text");
96-
when(highlight.getSnippet()).thenReturn(expectedSnippet);
91+
// Mock highlight
92+
SearchHighlight highlight = mock(SearchHighlight.class);
93+
when(highlight.getField()).thenReturn("text");
94+
when(highlight.getSnippet()).thenReturn(expectedSnippet);
9795

98-
// Simula un hit como Map
99-
SearchResultHit hit = mock(SearchResultHit.class);
100-
when(hit.getDocument()).thenReturn(Map.of("id", expectedId.toString()));
101-
when(hit.getHighlights()).thenReturn(List.of(highlight));
96+
// Simula un hit como Map
97+
SearchResultHit hit = mock(SearchResultHit.class);
98+
when(hit.getDocument()).thenReturn(Map.of("id", expectedId.toString()));
99+
when(hit.getHighlights()).thenReturn(List.of(highlight));
102100

103-
// Mock search result
104-
SearchResult searchResult = new SearchResult();
105-
searchResult.setHits(List.of(hit));
101+
// Mock search result
102+
SearchResult searchResult = new SearchResult();
103+
searchResult.setHits(List.of(hit));
106104

107105

108-
// Mock client behavior
109-
var documents = mock(Documents.class);
110-
var collection = mock(Collection.class);
111-
when(client.collections("bookmark")).thenReturn(collection);
112-
when(collection.documents()).thenReturn(documents);
113-
when(documents.search(any(SearchParameters.class))).thenReturn(searchResult);
106+
// Mock client behavior
107+
var documents = mock(Documents.class);
108+
var collection = mock(Collection.class);
109+
when(client.collections("bookmark")).thenReturn(collection);
110+
when(collection.documents()).thenReturn(documents);
111+
when(documents.search(any(SearchParameters.class))).thenReturn(searchResult);
114112

115-
// Act
116-
var results = typesense.search(query);
113+
// Act
114+
var results = typesense.search(query);
117115

118-
// Assert
119-
assertEquals(1, results.size());
120-
assertEquals(expectedId, results.get(0).id());
121-
assertEquals(expectedSnippet, results.get(0).highlight());
122-
}
116+
// Assert
117+
assertEquals(1, results.size());
118+
assertEquals(expectedId, results.get(0).id());
119+
assertEquals(expectedSnippet, results.get(0).highlight());
120+
}
123121
}

0 commit comments

Comments
 (0)