最近,我加入了一个使用 Hibernate Search 的项目。
我怀疑我们的应用程序中有一个故障,由于在 2 个地方使用
FullTextEntityManager
,导致忽略其他后台作业新索引的数据:
1)在从UI执行目标数据搜索时,我们在第一次搜索请求时使用MassIndexer对数据进行索引,并且所有后续搜索请求都不会导致重新索引:
private final AtomicBoolean initialized = new AtomicBoolean(false);
...
public FullTextQuery buildTransactionSearchQuery(SearchRequestDTO request) {
final FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
final Query expression = buildTransactionSearchExpression(request.getFilter(), fullTextEntityManager);
final FullTextQuery query = fullTextEntityManager.createFullTextQuery(expression, Transaction.class);
return query;
}
...
private FullTextEntityManager getFullTextEntityManager() {
final FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
if (initialized.get()) {
return fullTextEntityManager;
} else {
synchronized (initialized) {
if (!initialized.getAndSet(true)) {
try {
fullTextEntityManager.createIndexer().startAndWait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return fullTextEntityManager;
}
}
}
2)在后台工作:
@Scheduled(initialDelay = 1_000, fixedDelay = 5_000)
private void indexAuditValues() {
Instant previousRunTime = ...; // assume data is set
Instant currentTime = ...;
int page = 0;
boolean hasMore = true;
while (hasMore) {
hasMore = hsIndexingService.indexAuditValues(previousRunTime, currentTime, page++);
}
}
@Transactional(readOnly = true)
public boolean indexAuditValues(Instant previousRunTime, Instant currentTime, int page) {
PageRequest pageRequest = return new PageRequest(page, batchSize, Sort.Direction.ASC, AUDIT_VALUE_SORT_COLUMN);
Page<AuditValue> pageResults = auditValueRepository.findByAuditTransactionLastModifiedDateBetween(previousRunTime, currentTime, pageRequest);
FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
List<AuditValue> content = pageResults.getContent();
content.forEach(fullTextEntityManager::index); // here we do index the data
return pageResults.hasNext();
}
private FullTextEntityManager getFullTextEntityManager() {
return Search.getFullTextEntityManager(entityManager);
}
最近,我们的用户报告说新数据没有出现在搜索页面上,是否可能是因为在两个不同步的独立线程中使用了 2 个
FullTextEntityManager
?如果有的话如何解决?
我们使用文件Spring boot、Hibernate Search、Lucene,并将索引存储在文件系统中。 实体用
@Indexed
注释,可搜索字段用 @Field
注释。
我不确定这是你问题的一部分,但无论如何我都会说清楚:
FullTextEntityManager
可以在两个单独的线程中使用,只要你使用不同的实体管理器。如果您正在使用 Spring,那么您很可能会这样做。所以那里一切都很好。
我在您的设置中看到的主要问题是,这两种方法可能会同时执行(如果第一个搜索查询是在第一个计划索引之前或期间发送的)。但在这种情况下,您宁愿在索引中获得重复的文档,也不愿丢失文档(因为质量索引器的工作方式)。所以我真的不知道出了什么问题。
我建议不要在查询方法中延迟执行批量索引,更重要的是避免在请求线程中等待可能长时间运行的操作(批量索引):这是一个主要的反模式。
理想情况下,您应该仅在重新部署应用程序时(当客户不使用该应用程序时)进行批量索引,并在重新启动后重新使用索引。这样,您就不必让请求等待大规模索引:当任何人访问应用程序时,所有内容都已被索引。
但你没有这样做,所以我假设你有你的理由。如果您确实想在启动时重新索引所有内容,并在大规模索引尚未结束时阻止搜索请求,那么像下面这样的东西应该更安全。也许不是完美无缺的(这取决于你的模型,真的:我不知道审计值是否可以更新),但更安全。
1)在从 UI 执行目标数据搜索时,阻止请求,直到初始索引结束[再一次,这是一个坏主意,但对每个人来说]。
// Assuming the background job class is named "IndexInitializer"
@Autowired
IndexInitializer indexInitializer;
...
public FullTextQuery buildTransactionSearchQuery(SearchRequestDTO request) {
final FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
final Query expression = buildTransactionSearchExpression(request.getFilter(), fullTextEntityManager);
final FullTextQuery query = fullTextEntityManager.createFullTextQuery(expression, Transaction.class);
return query;
}
...
private FullTextEntityManager getFullTextEntityManager() {
indexInitializer.awaitInitialIndexing();
return Search.getFullTextEntityManager(entityManager);
}
2) 在后台作业中,在第一个刻度上使用质量索引器,并在每个后续刻度上使用增量索引:
private final CountDownLatch initialIndexingsRemaining = new CountDownLatch(1);
public void awaitInitialIndexing() {
initialIndexingsRemaining.await();
}
@Scheduled(initialDelay = 0, fixedDelay = 5_000)
private void indexAuditValues() {
if (isInitialIndexingDone()) {
doIncrementalIndexing();
} else {
doInitialIndexing();
}
}
private boolean isInitialIndexingDone() {
return initialIndexingsRemaining.await(0, TimeUnit.NANOSECONDS);
}
private void doInitialIndexing() {
// Synchronization is only necessary here if the scheduled method may be called again before the previous execution is over. Not sure it's possible?
synchronized (this) {
if (isInitialIndexingDone()) {
return;
}
try {
fullTextEntityManager.createIndexer().startAndWait();
initialIndexingsRemaining.countDown();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
private void doIncrementalIndexing() {
Instant previousRunTime = ...; // assume data is set
Instant currentTime = ...;
int page = 0;
boolean hasMore = true;
while (hasMore) {
hasMore = hsIndexingService.indexAuditValues(previousRunTime, currentTime, page++);
}
}
@Transactional(readOnly = true)
public boolean indexAuditValues(Instant previousRunTime, Instant currentTime, int page) {
PageRequest pageRequest = return new PageRequest(page, batchSize, Sort.Direction.ASC, AUDIT_VALUE_SORT_COLUMN);
Page<AuditValue> pageResults = auditValueRepository.findByAuditTransactionLastModifiedDateBetween(previousRunTime, currentTime, pageRequest);
FullTextEntityManager fullTextEntityManager = getFullTextEntityManager();
List<AuditValue> content = pageResults.getContent();
content.forEach(fullTextEntityManager::index); // here we do index the data
return pageResults.hasNext();
}
private FullTextEntityManager getFullTextEntityManager() {
return Search.getFullTextEntityManager(entityManager);
}
顺便说一句,您还可以用自动、即时索引替换手动、定期索引:当 Hibernate ORM 中持久/更新/删除实体时,Hibernate Search 将自动更新索引。
import com.example.demo.model.Book;
import jakarta.persistence.EntityManager;
import org.hibernate.search.mapper.orm.Search;
import org.hibernate.search.mapper.orm.massindexing.MassIndexer;
import org.hibernate.search.mapper.orm.session.SearchSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class EntityIndexer implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private final EntityManager entityManager;
public EntityIndexer(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
@Transactional
@Async
public void onApplicationEvent(ContextRefreshedEvent event) {
SearchSession searchSession = Search.session(entityManager);
MassIndexer indexer = searchSession.massIndexer(Book.class).threadsToLoadObjects(7);
try {
indexer.startAndWait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}