mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-07 08:26:00 +00:00
fix(search): support short skill queries
This commit is contained in:
parent
725b71d323
commit
df850d1f63
3 changed files with 150 additions and 7 deletions
|
|
@ -27,5 +27,10 @@
|
|||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import java.util.Set;
|
|||
|
||||
@Service
|
||||
public class PostgresFullTextQueryService implements SearchQueryService {
|
||||
private static final int SHORT_KEYWORD_LENGTH = 2;
|
||||
|
||||
private final EntityManager entityManager;
|
||||
|
||||
|
|
@ -21,6 +22,9 @@ public class PostgresFullTextQueryService implements SearchQueryService {
|
|||
|
||||
@Override
|
||||
public SearchResult search(SearchQuery query) {
|
||||
String normalizedKeyword = normalizeKeyword(query.keyword());
|
||||
boolean hasKeyword = normalizedKeyword != null;
|
||||
boolean useShortKeywordFallback = hasKeyword && normalizedKeyword.length() <= SHORT_KEYWORD_LENGTH;
|
||||
Set<Long> memberNamespaceIds = query.visibilityScope().memberNamespaceIds().isEmpty()
|
||||
? Set.of(-1L)
|
||||
: query.visibilityScope().memberNamespaceIds();
|
||||
|
|
@ -48,8 +52,17 @@ public class PostgresFullTextQueryService implements SearchQueryService {
|
|||
}
|
||||
|
||||
// Full-text search
|
||||
if (query.keyword() != null && !query.keyword().isBlank()) {
|
||||
sql.append("AND search_vector @@ plainto_tsquery('simple', :keyword) ");
|
||||
if (hasKeyword) {
|
||||
if (useShortKeywordFallback) {
|
||||
sql.append("AND (");
|
||||
sql.append("LOWER(title) LIKE LOWER(:keywordLike) ");
|
||||
sql.append("OR LOWER(summary) LIKE LOWER(:keywordLike) ");
|
||||
sql.append("OR LOWER(keywords) LIKE LOWER(:keywordLike) ");
|
||||
sql.append("OR LOWER(search_text) LIKE LOWER(:keywordLike)");
|
||||
sql.append(") ");
|
||||
} else {
|
||||
sql.append("AND search_vector @@ plainto_tsquery('simple', :keyword) ");
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting
|
||||
|
|
@ -59,7 +72,7 @@ public class PostgresFullTextQueryService implements SearchQueryService {
|
|||
sql.append("ORDER BY (SELECT rating_avg FROM skill WHERE id = skill_id) DESC ");
|
||||
} else if ("newest".equals(query.sortBy())) {
|
||||
sql.append("ORDER BY (SELECT updated_at FROM skill WHERE id = skill_id) DESC ");
|
||||
} else if ("relevance".equals(query.sortBy()) && query.keyword() != null && !query.keyword().isBlank()) {
|
||||
} else if ("relevance".equals(query.sortBy()) && hasKeyword && !useShortKeywordFallback) {
|
||||
sql.append("ORDER BY ts_rank(search_vector, plainto_tsquery('simple', :keyword)) DESC ");
|
||||
} else {
|
||||
sql.append("ORDER BY updated_at DESC ");
|
||||
|
|
@ -80,8 +93,12 @@ public class PostgresFullTextQueryService implements SearchQueryService {
|
|||
nativeQuery.setParameter("namespaceId", query.namespaceId());
|
||||
}
|
||||
|
||||
if (query.keyword() != null && !query.keyword().isBlank()) {
|
||||
nativeQuery.setParameter("keyword", query.keyword());
|
||||
if (hasKeyword) {
|
||||
if (useShortKeywordFallback) {
|
||||
nativeQuery.setParameter("keywordLike", "%" + normalizedKeyword + "%");
|
||||
} else {
|
||||
nativeQuery.setParameter("keyword", normalizedKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
nativeQuery.setParameter("limit", query.size());
|
||||
|
|
@ -115,12 +132,23 @@ public class PostgresFullTextQueryService implements SearchQueryService {
|
|||
countQuery.setParameter("namespaceId", query.namespaceId());
|
||||
}
|
||||
|
||||
if (query.keyword() != null && !query.keyword().isBlank()) {
|
||||
countQuery.setParameter("keyword", query.keyword());
|
||||
if (hasKeyword) {
|
||||
if (useShortKeywordFallback) {
|
||||
countQuery.setParameter("keywordLike", "%" + normalizedKeyword + "%");
|
||||
} else {
|
||||
countQuery.setParameter("keyword", normalizedKeyword);
|
||||
}
|
||||
}
|
||||
|
||||
long total = ((Number) countQuery.getSingleResult()).longValue();
|
||||
|
||||
return new SearchResult(skillIds, total, query.page(), query.size());
|
||||
}
|
||||
|
||||
private String normalizeKeyword(String keyword) {
|
||||
if (keyword == null || keyword.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return keyword.trim();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
package com.iflytek.skillhub.search.postgres;
|
||||
|
||||
import com.iflytek.skillhub.search.SearchQuery;
|
||||
import com.iflytek.skillhub.search.SearchVisibilityScope;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class PostgresFullTextQueryServiceTest {
|
||||
|
||||
@Test
|
||||
void shortKeywordsShouldUseLikeFallback() {
|
||||
EntityManager entityManager = mock(EntityManager.class);
|
||||
Query nativeQuery = mock(Query.class);
|
||||
Query countQuery = mock(Query.class);
|
||||
when(entityManager.createNativeQuery(anyString()))
|
||||
.thenReturn(nativeQuery)
|
||||
.thenReturn(countQuery);
|
||||
when(nativeQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(nativeQuery);
|
||||
when(countQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(countQuery);
|
||||
when(nativeQuery.getResultList()).thenReturn(List.of(1L));
|
||||
when(countQuery.getSingleResult()).thenReturn(1L);
|
||||
|
||||
PostgresFullTextQueryService service = new PostgresFullTextQueryService(entityManager);
|
||||
|
||||
service.search(new SearchQuery(
|
||||
"ai",
|
||||
null,
|
||||
new SearchVisibilityScope(null, Set.of(), Set.of()),
|
||||
"relevance",
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
verify(nativeQuery).setParameter("keywordLike", "%ai%");
|
||||
verify(countQuery).setParameter("keywordLike", "%ai%");
|
||||
verify(nativeQuery, never()).setParameter("keyword", "ai");
|
||||
verify(countQuery, never()).setParameter("keyword", "ai");
|
||||
}
|
||||
|
||||
@Test
|
||||
void longerKeywordsShouldKeepFullTextSearch() {
|
||||
EntityManager entityManager = mock(EntityManager.class);
|
||||
Query nativeQuery = mock(Query.class);
|
||||
Query countQuery = mock(Query.class);
|
||||
when(entityManager.createNativeQuery(anyString()))
|
||||
.thenReturn(nativeQuery)
|
||||
.thenReturn(countQuery);
|
||||
when(nativeQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(nativeQuery);
|
||||
when(countQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(countQuery);
|
||||
when(nativeQuery.getResultList()).thenReturn(List.of(1L));
|
||||
when(countQuery.getSingleResult()).thenReturn(1L);
|
||||
|
||||
PostgresFullTextQueryService service = new PostgresFullTextQueryService(entityManager);
|
||||
|
||||
service.search(new SearchQuery(
|
||||
"agent",
|
||||
null,
|
||||
new SearchVisibilityScope(null, Set.of(), Set.of()),
|
||||
"relevance",
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
verify(nativeQuery).setParameter("keyword", "agent");
|
||||
verify(countQuery).setParameter("keyword", "agent");
|
||||
verify(nativeQuery, never()).setParameter("keywordLike", "%agent%");
|
||||
verify(countQuery, never()).setParameter("keywordLike", "%agent%");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortKeywordSqlShouldAvoidTsRankOrdering() {
|
||||
EntityManager entityManager = mock(EntityManager.class);
|
||||
Query nativeQuery = mock(Query.class);
|
||||
Query countQuery = mock(Query.class);
|
||||
when(entityManager.createNativeQuery(anyString()))
|
||||
.thenReturn(nativeQuery)
|
||||
.thenReturn(countQuery);
|
||||
when(nativeQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(nativeQuery);
|
||||
when(countQuery.setParameter(anyString(), org.mockito.ArgumentMatchers.any())).thenReturn(countQuery);
|
||||
when(nativeQuery.getResultList()).thenReturn(List.of());
|
||||
when(countQuery.getSingleResult()).thenReturn(0L);
|
||||
|
||||
PostgresFullTextQueryService service = new PostgresFullTextQueryService(entityManager);
|
||||
|
||||
service.search(new SearchQuery(
|
||||
"go",
|
||||
null,
|
||||
new SearchVisibilityScope(null, Set.of(), Set.of()),
|
||||
"relevance",
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
var sqlCaptor = org.mockito.ArgumentCaptor.forClass(String.class);
|
||||
verify(entityManager, org.mockito.Mockito.times(2)).createNativeQuery(sqlCaptor.capture());
|
||||
assertThat(sqlCaptor.getAllValues().getFirst()).contains("LOWER(title) LIKE LOWER(:keywordLike)");
|
||||
assertThat(sqlCaptor.getAllValues().getFirst()).doesNotContain("ts_rank");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue