mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-07-22 20:46:09 +00:00
fix: 搜索main路径跑通
This commit is contained in:
@@ -24,14 +24,12 @@ public class OpenSearchIndexer implements SearchIndexer {
|
|||||||
builder.index(index).id(document.entityId().toString()).document(document)
|
builder.index(index).id(document.entityId().toString()).document(document)
|
||||||
);
|
);
|
||||||
IndexResponse response = client.index(request);
|
IndexResponse response = client.index(request);
|
||||||
if (log.isDebugEnabled()) {
|
log.info(
|
||||||
log.debug(
|
"Indexed document {} into {} with result {}",
|
||||||
"Indexed document {} into {} with result {}",
|
document.entityId(),
|
||||||
document.entityId(),
|
index,
|
||||||
index,
|
response.result()
|
||||||
response.result()
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.warn("Failed to index document {} into {}", document.entityId(), index, e);
|
log.warn("Failed to index document {} into {}", document.entityId(), index, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.openisle.search;
|
package com.openisle.search;
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public record SearchDocument(
|
public record SearchDocument(
|
||||||
@@ -12,5 +11,5 @@ public record SearchDocument(
|
|||||||
String category,
|
String category,
|
||||||
List<String> tags,
|
List<String> tags,
|
||||||
Long postId,
|
Long postId,
|
||||||
Instant createdAt
|
Long createdAt
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import com.openisle.model.Comment;
|
|||||||
import com.openisle.model.Post;
|
import com.openisle.model.Post;
|
||||||
import com.openisle.model.Tag;
|
import com.openisle.model.Tag;
|
||||||
import com.openisle.model.User;
|
import com.openisle.model.User;
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -38,7 +37,7 @@ public final class SearchDocumentFactory {
|
|||||||
post.getCategory() != null ? post.getCategory().getName() : null,
|
post.getCategory() != null ? post.getCategory().getName() : null,
|
||||||
tags,
|
tags,
|
||||||
post.getId(),
|
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,
|
post != null && post.getCategory() != null ? post.getCategory().getName() : null,
|
||||||
tags,
|
tags,
|
||||||
post != null ? post.getId() : null,
|
post != null ? post.getId() : null,
|
||||||
toInstant(comment.getCreatedAt())
|
toEpochMillis(comment.getCreatedAt())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +80,7 @@ public final class SearchDocumentFactory {
|
|||||||
null,
|
null,
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
null,
|
null,
|
||||||
toInstant(user.getCreatedAt())
|
toEpochMillis(user.getCreatedAt())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +114,14 @@ public final class SearchDocumentFactory {
|
|||||||
null,
|
null,
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
null,
|
null,
|
||||||
toInstant(tag.getCreatedAt())
|
toEpochMillis(tag.getCreatedAt())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Instant toInstant(LocalDateTime time) {
|
private static Long toEpochMillis(LocalDateTime time) {
|
||||||
return time == null ? null : time.atZone(ZoneId.systemDefault()).toInstant();
|
if (time == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ public class SearchIndexInitializer {
|
|||||||
.properties("category", Property.of(p -> p.keyword(k -> k)))
|
.properties("category", Property.of(p -> p.keyword(k -> k)))
|
||||||
.properties("tags", 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("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("category", Property.of(p -> p.keyword(k -> k)))
|
||||||
.properties("tags", 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("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("type", Property.of(p -> p.keyword(k -> k)))
|
||||||
.properties("title", Property.of(p -> p.text(t -> t)))
|
.properties("title", Property.of(p -> p.text(t -> t)))
|
||||||
.properties("content", 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("type", Property.of(p -> p.keyword(k -> k)))
|
||||||
.properties("title", Property.of(p -> p.text(t -> t)))
|
.properties("title", Property.of(p -> p.text(t -> t)))
|
||||||
.properties("content", 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")))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,34 +176,127 @@ public class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<SearchResult> searchWithOpenSearch(String keyword) throws IOException {
|
private List<SearchResult> searchWithOpenSearch(String keyword) throws IOException {
|
||||||
OpenSearchClient client = openSearchClient.orElse(null);
|
var client = openSearchClient.orElse(null);
|
||||||
if (client == null) {
|
if (client == null) return List.of();
|
||||||
return List.of();
|
|
||||||
}
|
final String qRaw = keyword == null ? "" : keyword.trim();
|
||||||
String trimmed = keyword.trim();
|
if (qRaw.isEmpty()) return List.of();
|
||||||
SearchResponse<SearchDocument> response = client.search(
|
|
||||||
builder ->
|
final boolean enableWildcard = qRaw.length() >= 2;
|
||||||
builder
|
final String qsEscaped = escapeForQueryString(qRaw);
|
||||||
|
|
||||||
|
SearchResponse<SearchDocument> resp = client.search(
|
||||||
|
b ->
|
||||||
|
b
|
||||||
.index(searchIndices())
|
.index(searchIndices())
|
||||||
.query(q ->
|
.trackTotalHits(t -> t.enabled(true))
|
||||||
q.multiMatch(mm ->
|
.query(qb ->
|
||||||
mm
|
qb.bool(bool -> {
|
||||||
.query(trimmed)
|
// 1) 主召回:title/content
|
||||||
.fields(List.of("title^3", "content^2", "author^2", "category", "tags"))
|
bool.should(s ->
|
||||||
.type(TextQueryType.BestFields)
|
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 ->
|
.highlight(h ->
|
||||||
h
|
h
|
||||||
.preTags("<mark>")
|
.preTags("<mark>")
|
||||||
.postTags("</mark>")
|
.postTags("</mark>")
|
||||||
.fields("content", f -> f.fragmentSize(highlightFragmentSize()).numberOfFragments(1))
|
|
||||||
.fields("title", 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
|
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() {
|
private int highlightFragmentSize() {
|
||||||
|
|||||||
Reference in New Issue
Block a user