fix: 修复 Lucene 特殊字符查询失败

- 将用户关键词按普通文本转义后再解析

- 避免空查询和解析失败触发二次空指针

- 补充特殊字符与空查询回归测试
This commit is contained in:
2026-09-01 15:11:55 +08:00
parent 876517f821
commit 1b36067e6c
2 changed files with 57 additions and 20 deletions

View File

@@ -199,10 +199,13 @@ public class LuceneSearcher implements DocumentSearcher, AutoCloseable {
@Override
public List<Document> searchDocuments(KeywordSearchRequest request) {
List<Document> results = new ArrayList<>();
if (request == null || request.getKeyword() == null || request.getKeyword().trim().isEmpty()) {
return results;
}
try (IndexReader reader = DirectoryReader.open(directory)) {
IndexSearcher searcher = new IndexSearcher(reader);
Query query = buildQuery(request);
TopDocs topDocs = searcher.search(query, request == null ? 10 : request.getCount());
TopDocs topDocs = searcher.search(query, request.getCount());
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
org.apache.lucene.document.Document doc = searcher.doc(scoreDoc.doc);
Document resultDoc = new Document();
@@ -224,29 +227,24 @@ public class LuceneSearcher implements DocumentSearcher, AutoCloseable {
return results;
}
Query buildQuery(KeywordSearchRequest request) {
try {
String keyword = request == null ? null : request.getKeyword();
Query buildQuery(KeywordSearchRequest request) throws ParseException {
String escapedKeyword = QueryParser.escape(request.getKeyword());
QueryParser titleQueryParser = new QueryParser("title", analyzer);
Query titleQuery = titleQueryParser.parse(keyword);
Query titleQuery = titleQueryParser.parse(escapedKeyword);
BooleanClause titleBooleanClause = new BooleanClause(titleQuery, BooleanClause.Occur.SHOULD);
QueryParser contentQueryParser = new QueryParser("content", analyzer);
Query contentQuery = contentQueryParser.parse(keyword);
Query contentQuery = contentQueryParser.parse(escapedKeyword);
BooleanClause contentBooleanClause = new BooleanClause(contentQuery, BooleanClause.Occur.SHOULD);
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add(titleBooleanClause)
.add(contentBooleanClause);
if (request != null && request.getKnowledgeId() != null && !request.getKnowledgeId().trim().isEmpty()) {
if (request.getKnowledgeId() != null && !request.getKnowledgeId().trim().isEmpty()) {
builder.add(new TermQuery(new Term(KeywordSearchMetadataKeys.KNOWLEDGE_ID, request.getKnowledgeId().trim())), BooleanClause.Occur.MUST);
}
return builder.build();
} catch (ParseException e) {
LOG.error(e.toString(), e);
}
return null;
}
private static Analyzer createAnalyzer() {

View File

@@ -57,6 +57,45 @@ public class LuceneSearcherTest {
}
}
/**
* 验证用户输入中的 Lucene 特殊字符按普通文本检索。
*
* @throws Exception 临时目录或 Lucene 资源操作失败时抛出
*/
@Test
public void shouldTreatLuceneSpecialCharactersAsPlainText() throws Exception {
Path tempDir = Files.createTempDirectory("lucene-searcher-special-character-test");
LuceneConfig config = new LuceneConfig();
config.setIndexDirPath(tempDir.toString());
try (LuceneSearcher searcher = new LuceneSearcher(config)) {
Document document = new Document();
document.setId("special-character");
document.setContent("A/C.\\nPLEASE");
Assert.assertTrue(searcher.addDocument(document));
List<Document> results = searcher.searchDocuments("A/C.\\nPLEASE", 10);
Assert.assertEquals(1, results.size());
Assert.assertEquals("special-character", String.valueOf(results.get(0).getId()));
}
}
/**
* 验证空查询直接返回空结果。
*
* @throws Exception 临时目录或 Lucene 资源操作失败时抛出
*/
@Test
public void shouldReturnEmptyForMissingKeyword() throws Exception {
Path tempDir = Files.createTempDirectory("lucene-searcher-empty-keyword-test");
LuceneConfig config = new LuceneConfig();
config.setIndexDirPath(tempDir.toString());
try (LuceneSearcher searcher = new LuceneSearcher(config)) {
Assert.assertTrue(searcher.searchDocuments((KeywordSearchRequest) null).isEmpty());
Assert.assertTrue(searcher.searchDocuments(" ", 10).isEmpty());
}
}
/**
* 验证多个导入线程可共享同一个 IndexWriter 完成批量写入。
*