fix: 搜索main路径跑通

This commit is contained in:
tim
2025-09-26 18:03:25 +08:00
parent 0bc65077df
commit 44addd2a7b
5 changed files with 143 additions and 39 deletions

View File

@@ -24,14 +24,12 @@ public class OpenSearchIndexer implements SearchIndexer {
builder.index(index).id(document.entityId().toString()).document(document)
);
IndexResponse response = client.index(request);
if (log.isDebugEnabled()) {
log.debug(
"Indexed document {} into {} with result {}",
document.entityId(),
index,
response.result()
);
}
log.info(
"Indexed document {} into {} with result {}",
document.entityId(),
index,
response.result()
);
} catch (IOException e) {
log.warn("Failed to index document {} into {}", document.entityId(), index, e);
}

View File

@@ -1,6 +1,5 @@
package com.openisle.search;
import java.time.Instant;
import java.util.List;
public record SearchDocument(
@@ -12,5 +11,5 @@ public record SearchDocument(
String category,
List<String> tags,
Long postId,
Instant createdAt
Long createdAt
) {}

View File

@@ -5,7 +5,6 @@ import com.openisle.model.Comment;
import com.openisle.model.Post;
import com.openisle.model.Tag;
import com.openisle.model.User;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
@@ -38,7 +37,7 @@ public final class SearchDocumentFactory {
post.getCategory() != null ? post.getCategory().getName() : null,
tags,
post.getId(),
toInstant(post.getCreatedAt())
toEpochMillis(post.getCreatedAt())
);
}
@@ -64,7 +63,7 @@ public final class SearchDocumentFactory {
post != null && post.getCategory() != null ? post.getCategory().getName() : null,
tags,
post != null ? post.getId() : null,
toInstant(comment.getCreatedAt())
toEpochMillis(comment.getCreatedAt())
);
}
@@ -81,7 +80,7 @@ public final class SearchDocumentFactory {
null,
Collections.emptyList(),
null,
toInstant(user.getCreatedAt())
toEpochMillis(user.getCreatedAt())
);
}
@@ -115,11 +114,14 @@ public final class SearchDocumentFactory {
null,
Collections.emptyList(),
null,
toInstant(tag.getCreatedAt())
toEpochMillis(tag.getCreatedAt())
);
}
private static Instant toInstant(LocalDateTime time) {
return time == null ? null : time.atZone(ZoneId.systemDefault()).toInstant();
private static Long toEpochMillis(LocalDateTime time) {
if (time == null) {
return null;
}
return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}
}

View File

@@ -53,7 +53,10 @@ public class SearchIndexInitializer {
.properties("category", Property.of(p -> p.keyword(k -> k)))
.properties("tags", Property.of(p -> p.keyword(k -> k)))
.properties("postId", Property.of(p -> p.long_(l -> l)))
.properties("createdAt", Property.of(p -> p.date(d -> d)))
.properties(
"createdAt",
Property.of(p -> p.date(d -> d.format("strict_date_optional_time||epoch_millis")))
)
);
}
@@ -67,7 +70,10 @@ public class SearchIndexInitializer {
.properties("category", Property.of(p -> p.keyword(k -> k)))
.properties("tags", Property.of(p -> p.keyword(k -> k)))
.properties("postId", Property.of(p -> p.long_(l -> l)))
.properties("createdAt", Property.of(p -> p.date(d -> d)))
.properties(
"createdAt",
Property.of(p -> p.date(d -> d.format("strict_date_optional_time||epoch_millis")))
)
);
}
@@ -77,7 +83,10 @@ public class SearchIndexInitializer {
.properties("type", Property.of(p -> p.keyword(k -> k)))
.properties("title", Property.of(p -> p.text(t -> t)))
.properties("content", Property.of(p -> p.text(t -> t)))
.properties("createdAt", Property.of(p -> p.date(d -> d)))
.properties(
"createdAt",
Property.of(p -> p.date(d -> d.format("strict_date_optional_time||epoch_millis")))
)
);
}
@@ -96,7 +105,10 @@ public class SearchIndexInitializer {
.properties("type", Property.of(p -> p.keyword(k -> k)))
.properties("title", Property.of(p -> p.text(t -> t)))
.properties("content", Property.of(p -> p.text(t -> t)))
.properties("createdAt", Property.of(p -> p.date(d -> d)))
.properties(
"createdAt",
Property.of(p -> p.date(d -> d.format("strict_date_optional_time||epoch_millis")))
)
);
}
}

View File

@@ -176,34 +176,127 @@ public class SearchService {
}
private List<SearchResult> searchWithOpenSearch(String keyword) throws IOException {
OpenSearchClient client = openSearchClient.orElse(null);
if (client == null) {
return List.of();
}
String trimmed = keyword.trim();
SearchResponse<SearchDocument> response = client.search(
builder ->
builder
var client = openSearchClient.orElse(null);
if (client == null) return List.of();
final String qRaw = keyword == null ? "" : keyword.trim();
if (qRaw.isEmpty()) return List.of();
final boolean enableWildcard = qRaw.length() >= 2;
final String qsEscaped = escapeForQueryString(qRaw);
SearchResponse<SearchDocument> resp = client.search(
b ->
b
.index(searchIndices())
.query(q ->
q.multiMatch(mm ->
mm
.query(trimmed)
.fields(List.of("title^3", "content^2", "author^2", "category", "tags"))
.type(TextQueryType.BestFields)
)
.trackTotalHits(t -> t.enabled(true))
.query(qb ->
qb.bool(bool -> {
// 1) 主召回title/content
bool.should(s ->
s.multiMatch(mm ->
mm
.query(qRaw)
.fields("title^3", "content^2")
.type(TextQueryType.BestFields)
.fuzziness("AUTO")
.minimumShouldMatch("70%")
.lenient(true)
)
);
// 2) 兜底open* 前缀命中
if (enableWildcard) {
bool.should(s ->
s.queryString(qs ->
qs
.query("(title:" + qsEscaped + "* OR content:" + qsEscaped + "*)")
.analyzeWildcard(true)
)
);
}
// 3) 结构化字段keyword
// term 需要 FieldValue用 lambda 设置 stringValue
bool.should(s ->
s.term(t ->
t
.field("author")
.value(v -> v.stringValue(qRaw))
.boost(2.0f)
)
);
if (enableWildcard) {
// prefix/wildcard 这里的 value 是 String直接传即可
bool.should(s -> s.prefix(p -> p.field("category").value(qRaw).boost(1.2f)));
bool.should(s ->
s.wildcard(w -> w.field("category").value("*" + qRaw + "*").boost(1.0f))
);
bool.should(s ->
s.term(t ->
t
.field("tags")
.value(v -> v.stringValue(qRaw))
.boost(1.2f)
)
);
bool.should(s ->
s.wildcard(w -> w.field("tags").value("*" + qRaw + "*").boost(1.0f))
);
}
return bool.minimumShouldMatch("1");
})
)
.highlight(h ->
h
.preTags("<mark>")
.postTags("</mark>")
.fields("content", f -> f.fragmentSize(highlightFragmentSize()).numberOfFragments(1))
.fields("title", f -> f.fragmentSize(highlightFragmentSize()).numberOfFragments(1))
.fields("content", f -> f.fragmentSize(highlightFragmentSize()).numberOfFragments(1))
)
.size(DEFAULT_OPEN_SEARCH_LIMIT),
.size(DEFAULT_OPEN_SEARCH_LIMIT > 0 ? DEFAULT_OPEN_SEARCH_LIMIT : 10),
SearchDocument.class
);
return mapHits(response.hits().hits(), trimmed);
return mapHits(resp.hits().hits(), qRaw);
}
/** Lucene query_string 安全转义(保留 * 由我们自己追加) */
private static String escapeForQueryString(String s) {
if (s == null || s.isEmpty()) return "";
StringBuilder sb = new StringBuilder(s.length() * 2);
for (char c : s.toCharArray()) {
switch (c) {
case '+':
case '-':
case '=':
case '&':
case '|':
case '>':
case '<':
case '!':
case '(':
case ')':
case '{':
case '}':
case '[':
case ']':
case '^':
case '"':
case '~': /* case '*': */ /* case '?': */
case ':':
case '\\':
case '/':
sb.append('\\').append(c);
break;
default:
sb.append(c);
}
}
return sb.toString();
}
private int highlightFragmentSize() {