龙空技术网

使用Hibernate Search和Angular/Spring Boot进行过滤和全文搜索

墨谈科技 63

前言:

现在大家对“商品筛选过滤怎么操作”大概比较珍视,大家都需要知道一些“商品筛选过滤怎么操作”的相关资讯。那么小编在网上网罗了一些有关“商品筛选过滤怎么操作””的相关资讯,希望各位老铁们能喜欢,姐妹们一起来了解一下吧!

每日分享最新,最流行的软件开发知识与最新行业趋势,希望大家能够一键三连,多多支持,跪求关注,点赞,留言。本文展示了如何创建一个搜索用户界面来查询具有多个可选过滤条件的数据库,并在某些条件下进行全文搜索。

JPA Criteria API 可以为可选过滤器子句的实现提供支持。对于长文本列,Hibernate Search 可以提供全文搜索。

在MovieManager项目中显示了 Hibernate Search 和 JPA Criteria API 的组合。API 用于为电影和演员创建搜索用户界面。

使用 Hibernate Search 和 JPA Criteria API
MovieManager 项目存储电影概述和演员传记的文本数据。电影和演员的新过滤功能包括对文本数据的全文搜索。JPA Criteria API 用于实现额外的过滤器功能,因此它可以帮助处理可选的查询组件,如年龄或发布日期。

后端
MovieController有一个新的rest接口:

@RequestMapping(value = "/filter-criteria", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public List<MovieDto> getMoviesByCriteria(@RequestHeader(value =
HttpHeaders.AUTHORIZATION) String bearerStr,
@RequestBody MovieFilterCriteriaDto filterCriteria) {
return this.service.findMoviesByFilterCriteria(bearerStr,
filterCriteria).stream().map(m -> this.mapper.convert(m)).toList();
}



ActorController有一个类似的 rest 接口:

@RequestMapping(value = "/filter-criteria", method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE, consumes =
MediaType.APPLICATION_JSON_VALUE)
public List<ActorDto> getActorsByCriteria(@RequestHeader(value =
HttpHeaders.AUTHORIZATION) String bearerStr,
@RequestBody ActorFilterCriteriaDto filterCriteria) {
return this.service.findActorsByFilterCriteria(bearerStr,
filterCriteria).stream().map(m -> this.mapper.convert(m)).toList();
}


这些是需要使用@RequestBody注释映射到 FilterCriteria DTO 的已发布 JSON 的其余端点。DTO 用于调用服务进行过滤。

搜索服务
ActorService和MovieService实现过滤服务。MovieService 显示在这里:

public List<Movie> findMoviesByFilterCriteria(String bearerStr,
MovieFilterCriteriaDto filterCriteriaDto) {
List<Movie> jpaMovies =
this.movieRep.findByFilterCriteria(filterCriteriaDto,
this.auds.getCurrentUser(bearerStr).getId());
SearchTermDto searchTermDto = new SearchTermDto();
searchTermDto.setSearchPhraseDto(filterCriteriaDto.getSearchPhraseDto());
List<Movie> ftMovies = this.findMoviesBySearchTerm(bearerStr,
searchTermDto);
List<Movie> results = jpaMovies;
if (filterCriteriaDto.getSearchPhraseDto() != null &&
!Objects.isNull(filterCriteriaDto.getSearchPhraseDto().getPhrase()) &&
filterCriteriaDto.getSearchPhraseDto().getPhrase().length() > 2) {
Collection<Long> dublicates = CommonUtils.
findDublicates(Stream.of(jpaMovies,ftMovies)
.flatMap(List::stream).toList());
results = Stream.of(jpaMovies, ftMovies).flatMap(List::stream)
.filter(myMovie -> CommonUtils.
filterForDublicates(myMovie, dublicates)).toList();
// remove dublicates
results = results.isEmpty() ? ftMovies :
List.copyOf(CommonUtils.filterDublicates(results));
}
return results.subList(0, results.size() > 50 ? 50 : results.size());
}

public List<Movie> findMoviesBySearchTerm(String bearerStr,
SearchTermDto searchTermDto) {
List<Movie> movies = searchTermDto.getSearchPhraseDto() != null ?
this.movieRep.findMoviesByPhrase(searchTermDto.getSearchPhraseDto()) :
this.movieRep.
findMoviesBySearchStrings(searchTermDto.getSearchStringDtos());
List<Movie> filteredMovies = movies.stream().filter(myMovie ->
myMovie.getUsers().stream().anyMatch(myUser -> myUser.getId()
.equals(this.auds.getCurrentUser(bearerStr).getId()))).toList();
return filteredMovies;
}


该findMoviesByFilterCriteria(...)方法首先调用 JPA 存储库来选择电影。该方法getCurrentUser(...)查找为其颁发 JWT 令牌的用户实体并返回 ID。电影有一个与用户相关的数据库。由于这种关系,一部电影在表中只存储一次,并由所有已导入它的用户使用。

然后SearchTermDto创建 来调用findMoviesBySearchTerm(...)全文搜索的方法。该方法使用在MovieRep电影的“概览”索引中执行搜索并过滤当前用户的电影的结果。

然后将 JPA 查询和全文搜索的结果分 3 步合并:

findDublicates(...)返回在两个搜索结果中找到的 id 。
返回 id的filterForDublicates(...)实体。
filterDublicates(...)删除具有重复 ID 的对象并返回它们。如果没有找到共同的结果,则返回全文搜索结果。
组合结果限制为 50 个实体并返回。

数据存储库
MovieRepositoryBean和ActorRepositoryBean实现JPA Criteria 搜索和 Hibernate 搜索搜索。的 JPA 搜索MovieRepositoryBean如下所示:

public List<Movie> findByFilterCriteria(MovieFilterCriteriaDto



该方法的这一部分filterByCriteria(...) 显示了电影标题搜索和演员姓名搜索的 where 条件。

首先,创建条件查询和根电影对象。创建谓词列表以包含搜索的查询条件。

检查电影标题的存在和最小长度。EntityManager 用于为“title”属性创建一个“like”标准,其中包含一个“lower”函数。标题字符串被转换为小写并用“%%”包围以查找包含不区分大小写字符串的所有标题。然后将条件添加到谓词列表中。

检查演员名称的存在和最小长度。然后创建 JPA 元模型以获取要加入的演员实体的 EntityType。EntityManager 用于创建“喜欢”和“较低”标准。根实体 ('cMove') 用于将转换实体加入查询。演员实体的“characterName”用在“like”标准中。搜索的演员姓名字符串被转换为小写并用“%%”包围以查找包含搜索字符串的所有演员姓名。最后将完整的参与者标准添加到谓词列表中。

然后以与演员姓名搜索标准相同的方式创建用户检查标准并将其添加到谓词列表中。

添加条件谓词并添加CriteriaQuery.where(...)“distinct(true)”调用以删除重复项。

查询结果限制为 1000 个实体,以保护服务器免受 I/O 和内存过载的影响。

全文检索
全文搜索在findMoviesBySearchPhrase(...)MovieRepository 的方法中实现:

@SuppressWarnings("unchecked")
public List<Movie> findMoviesByPhrase(SearchPhraseDto searchPhraseDto) {
List<Movie> resultList = List.of();
if (searchPhraseDto.getPhrase() != null &&
searchPhraseDto.getPhrase().trim().length() > 2) {
FullTextEntityManager fullTextEntityManager =
Search.getFullTextEntityManager(entityManager);
QueryBuilder movieQueryBuilder =
fullTextEntityManager.getSearchFactory().buildQueryBuilder()
.forEntity(Movie.class).get();
Query phraseQuery = movieQueryBuilder.phrase()
.withSlop(searchPhraseDto.getOtherWordsInPhrase())
.onField("overview")
.sentence(searchPhraseDto.getPhrase()).createQuery();
resultList = fullTextEntityManager
.createFullTextQuery(phraseQuery, Movie.class)
.setMaxResults(1000).getResultList();
}
return resultList;
}


该方法findMoviesByPhrase(...)具有SearchPhraseDto作为参数。包含以下属性:

otherWordsInPhrase它的默认值为 0。
'phrase' 包含 Hibernate Search 索引中电影概览的搜索字符串。
检查“短语”的存在和长度。然后FullTextEntityManager创建 和 QueryBuilder。'QueryBuilder' 用于使用搜索参数'phrase' 在电影实体字段'overview' 上创建全文查询。与参数一起 otherWordsInPhrase添加到。FullTextEntityManagerwithSlop(…)

用于对FullTextEntityManager电影 'overview' 索引执行全文查询,结果限制为 1000 个。设置限制是为了保护服务器免受 I/O 和内存过载的影响。

使 Hibernate 搜索索引保持最新
在应用程序启动时检查 Hibernate Search 索引以获取CronJobs类中所需的更新:

@Async
@EventListener(ApplicationReadyEvent.class)
public void checkHibernateSearchIndexes() throws InterruptedException {
int movieCount = this.entityManager.createNamedQuery("Movie.count",
Long.class).getSingleResult().intValue();
int actorCount = this.entityManager.createNamedQuery("Actor.count",
Long.class).getSingleResult().intValue();
FullTextEntityManager fullTextEntityManager =
Search.getFullTextEntityManager(entityManager);
int actorResults = checkForActorIndex(fullTextEntityManager);
int movieResults = checkForMovieIndex(fullTextEntityManager);
LOG.info(String.format("DbMovies: %d, DbActors: %d, FtMovies: %d,
FtActors: %d", movieCount, actorCount, movieResults, actorResults));
if (actorResults == 0 || movieResults == 0
|| actorResults != actorCount || movieResults != movieCount) {
fullTextEntityManager.createIndexer().startAndWait();
this.indexDone = true;
LOG.info("Hibernate Search Index ready.");
} else {
this.indexDone = true;
LOG.info("Hibernate Search Index ready.");
}
}

private int checkForMovieIndex(FullTextEntityManager fullTextEntityManager) {
org.apache.lucene.search.Query movieQuery = fullTextEntityManager
.getSearchFactory().buildQueryBuilder()
.forEntity(Movie.class).get().all().createQuery();
int movieResults = fullTextEntityManager.createFullTextQuery(movieQuery,
Movie.class).getResultSize();
return movieResults;
}

Spring的@Async和@EventListener(ApplicationReadyEvent.class)注解checkHibernateSearchIndexes()在自己的后台线程上执行应用程序启动时的方法。

首先,使用命名查询来查询数据库中电影和演员实体的数量。

checkForMovieIndex(...)其次,使用andcheckForActorIndex(...)方法 查询 Hibernate Search 索引中的电影和演员实体的数量。

然后,比较结果,FullTextEntityManager如果发现差异,则重新创建 Hibernate Search 索引。要获得正常的启动时间,该方法必须在其自己的后台线程上执行。Hibernate Search 索引是文件系统上的文件。它们需要在第一次启动时创建或检查。通过在应用程序的每个实例上都有本地索引,可以避免冲突和不一致。

结论后端
查询有几个可选条件,JPA Criteria 查询支持这个用例。代码很冗长,需要一些习惯。替代方法是自己创建查询字符串或添加库(可能生成代码)以获得更多支持。这个项目试图只添加需要的库并且代码是足够可维护的。我还没有找到同时执行 Hibernate Search 和 JPA 条件查询的支持。因此,必须在代码中组合结果。这需要限制结果大小以保护服务器的 I/O 和内存资源,这可能会导致大型结果集中的匹配丢失。

Hibernate Search 易于使用,并且可以在应用程序启动时创建/更新索引。

角前端
电影/演员过滤器显示在延迟加载的 Angular 模块filter-actors和filter-movies 中。这些模块是延迟加载的,以使应用程序启动更快,因为它们是 Ng-Bootstrap 库组件的唯一用户。模板filter-movies.component.html使用 Offcanvas 组件和 Datepicker 组件作为过滤条件:

<ng-template #content let-offcanvas>
<div class="offcanvas-header">
<h4 class="offcanvas-title" id="offcanvas-basic-title"
i18n="@@filtersAvailiable">Filters availiable</h4>
<button type="button" class="btn-close" aria-label="Close"
(click)="offcanvas.dismiss('Cross click')"></button>
</div>
<div class="offcanvas-body">
<form>
<div class="select-range">
<div class="mb-3 me-1">
<label for="releasedAfter" i18n="@@filterMoviesReleasedAfter">
Released after</label>
<div class="input-group">
<input id="releasedBefore" class="form-control"
[(ngModel)]="ngbReleaseFrom" placeholder="yyyy-mm-dd"
name="dpFrom" ngbDatepicker #dpFrom="ngbDatepicker">
<button class="btn btn-outline-secondary calendar"
(click)="dpFrom.toggle()" type="button"></button>
</div>
</div>
...
</ng-template>
<div class="container-fluid">
<div>
<div class="row">
<div class="col">
<button class="btn btn-primary filter-change-btn"
(click)="showFilterActors()"
i18n="@@filterMoviesFilterActors">Filter<br/>Actors</button>
<button class="btn btn-primary open-filters"
(click)="open(content)"
i18n="@@filterMoviesOpenFilters">Open<br/>Filters</button>
...



具有 <ng-template ...> 模板变量“#content”以在组件中识别它。
<div class="offcanvas-header">包含标签及其关闭按钮 。
包含日期选择器组件,其中<div class="offcanvas-body">包含值的输入和打开日期选择器的按钮。模板变量#dpFrom="ngbDatepicker"获取 datepicker 对象。该按钮在“单击”操作中使用它来切换日期选择器。
<div class="container-fluid">包含按钮和结果表 。
该按钮Filter<br/>Actors执行showFilterActors()导航到 filter-actors 模块的方法。
该按钮Open<br/>Filters执行 'open(content)' 方法以打开<ng-template>包含模板变量 '#content' 的 Offcanvas 组件。
filter-movies.component.ts显示Offcanvas 组件,调用过滤器,并显示结果:

@Component({
selector: 'app-filter-movies',
templateUrl: './filter-movies.component.html',
styleUrls: ['./filter-movies.component.scss']
})
export class FilterMoviesComponent implements OnInit {
protected filteredMovies: Movie[] = [];
protected filtering = false;
protected selectedGeneresStr = '';
protected generes: Genere[] = [];
protected closeResult = '';
protected filterCriteria = new MovieFilterCriteria();
protected ngbReleaseFrom: NgbDateStruct;
protected ngbReleaseTo: NgbDateStruct;

constructor(private offcanvasService: NgbOffcanvas,
public ngbRatingConfig: NgbRatingConfig,
private movieService: MoviesService, private router: Router) {}

...

public open(content: unknown) {
this.offcanvasService.open(content,
{ariaLabelledBy: 'offcanvas-basic-title'}).result.then((result) =>
{ this.closeResult = `Closed with: ${result}`;
}, (reason) => {
this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
});
}

public showFilterActors(): void {
this.router.navigate(['/filter-actors']);
}

private getDismissReason(reason: unknown): void {
//console.log(this.filterCriteria);
if (reason === OffcanvasDismissReasons.ESC) {
return this.resetFilters();
} else {
this.filterCriteria.releaseFrom = !this.ngbReleaseFrom ? null :
new Date(this.ngbReleaseFrom.year, this.ngbReleaseFrom.month,
this.ngbReleaseFrom.day);
this.filterCriteria.releaseTo = !this.ngbReleaseTo ? null :
new Date(this.ngbReleaseTo.year, this.ngbReleaseTo.month,
this.ngbReleaseTo.day);
this.movieService.findMoviesByCriteria(this.filterCriteria)
.subscribe({next: result => this.filteredMovies = result,
error: failed => {
console.log(failed);
this.router.navigate(['/']);
}
});
}
}
}


FilterMoviesComponent构造函数获取注入的、、、NgbOffcanvasMoviesService NgbRatingConfig、路由器。

'offCanvasService' 的 open 方法打开 Offcanvas 组件并返回一个返回 'closeResult' 的承诺。

showFilterActors(..)导航到延迟加载的 filter-actors 模块的路由 。

该方法getDismissReason(…)还检查重置过滤器的“ Escape”按钮。FilterCriteria 包含来自ngbDateStruct日期选择器对象的日期,并调用findMoviesByCriteria(…)“MovieService”的对象。该subscribe(…)方法将结果存储在“filteredMovies”属性中。

结论前端
Angular 前端需要一些努力和时间,直到 Ng-Bootstrap 组件仅包含在延迟加载的模块 filter-actors 和 filter-movies 中。这可以实现快速的初始加载时间。Ng-Bootstrap 组件易于使用且运行良好。前端是用户体验挑战。例如,如何使用“and”、“or”和“not”运算符向用户显示全文搜索。

标签: #商品筛选过滤怎么操作