使用 Angular 管道实现自定义搜索相关性

Implement custom search relevancy using an Angular pipe

我目前有一个带有自定义管道过滤器的基本课程搜索功能,stackblitz here。它 return 的结果基于 course titledescriptionkeywords 中是否存在匹配项,但它 return 以任意顺序排列课程。我想首先将管道修改为 return 更相关的结果,优先级是搜索查询匹配 1. 标题,2. 描述,3. 关键字。

一些测试用例示例:

  1. 查询 "chinese" 当前 returns "1 French""1X Chinese" 之前,因为 "chinese" 的关键字与 "1 French".

  2. 查询 "linear algebra" returns 54 Math 首先在 89A statistics 之前,即使后者在所有 3 个字段中都有匹配。

执行此操作的最佳方法是什么?我目前正在查看 this response,这意味着我可以尝试使用以下逻辑(在伪代码中)定义我自己的比较器

orderByComparator(a:Course, b:Course):number{
    if(searchRelevancy(a) < searchRelevancy(b)) return -1;
    else if(searchRelevancy(a) > searchRelevancy(b)) return 1;
    else return 0;
  }

其中 searchRelevancy 可以是一个根据查询计算每门课程排名的函数

searchRelevancy(course: Course, query: string): number{
   var rank: number;
   if(course.course_title.toUpperCase().includes(query)) rank += 5;
   if(course.description.toUpperCase().includes(query)) rank += 3;
   if(course.keywords.toUpperCase().includes(query)) rank += 1;
return rank
}

并且视图将包括

  <li *ngFor="let course of courses | courseFilter: searchText |
 orderBy: ['title', 'description', ''keywords]">

但是,我对 Angular 还比较陌生,而且实现细节超出了我目前所能了解的范围。这是最直接的方法吗?如果我知道我只过滤课程 objects,是否可以在不定义第二个 orderBy 管道的情况下修改当前 course-filter 管道?

如有指点,我们将不胜感激!

您首先要为每门课程添加排名,然后按此排序。即

addRank(a: Course, query: string): number{

    var rank: number;
    if(course.course_title.toUpperCase().includes(query)) rank += 5;
    if(course.description.toUpperCase().includes(query)) rank += 3;
    if(course.keywords.toUpperCase().includes(query)) rank += 1;
    course.rank = rank

然后按该排名降序排列:

<li *ngFor="let course of courses | addRank: searchText | orderBy: '-rank'">

但是,Angular(不像AngularJs/Angular1)不提供orderBy,或者filter,他们建议你在组件中自己做这些,而不是使用管道:

Angular doesn't provide pipes for filtering or sorting lists. Developers familiar with AngularJS know these as filter and orderBy. There are no equivalents in Angular.

This isn't an oversight. Angular doesn't offer such pipes because they perform poorly and prevent aggressive minification.

参考:AngularPipes

注意到这一点,您可能会使用一个单独的 courseResults 列表,该列表是 courses 数组的过滤和排序版本。当然,您可以在输入任何 search/filter 标准之前将其初始化为课程。您还可以考虑通过在搜索输入中添加 来最大程度地减少不必要的 search/filter 调用(如果您在键入时这样做;如果您在单击按钮时这样做,那么您d 跳过那个)。

简单的解决方案:

所以,在你的 StackBlitz 的基础上,我完全摆脱了管道的使用,制作了一个新的 StackBlitz:

<input type='text' [(ngModel)]='searchText' placeholder=" Search a Topic, Subject, or a Course!" (keyup)="search($event)"> 

我们不显示所有课程,但 courseResults...:[=​​24=]

<ul>
    <li *ngFor="let course of courseResults">

...初始化为空或所有课程(如此处):

public courseResults: Course[] = this.courses;

然后搜索可以是:

search($event) {
  this.courseResults = this.sortByRank(this.rankAndFilter(this.courses, this.searchText.toUpperCase()));
}

private rankAndFilter(courses: Course[], query: string) {
  var result = [];
  var rank: number;

  for (let course of courses) {
    rank = 0;
    if(course.course_title.toUpperCase().includes(query)) {rank += 5};
    if(course.description.toUpperCase().includes(query)) {rank += 3};
    if(course.keywords.toUpperCase().includes(query)) {rank += 1};
    course.rank = rank;
    if (rank > 0) result.push(course);
  }      
  return result;
}

private sortByRank(courses: Course[]) {
  return courses.sort(function(a:Course,b:Course){
    return b.rank - a.rank;
  });
}

扩展解:

[编辑] 新的 forked StackBlitz 使用去抖动(如果用户打字速度非常快,则不会对每次击键进行整个过滤和排序)和 distinctUntilChanged(如果用户退格字符,它不会打扰),并创建了数组的全局扩展(sortBy 和 sortByDesc)。

现在我们有了输入:

<input type='text' #searchInput placeholder=" Search a Topic, Subject, or a Course!"> 

然后获取该 ViewChild 作为 ElementRef:

@ViewChild('searchInput') searchInputRef: ElementRef;

然后在初始化之后,我们在本机元素上设置可观察的输入并订阅它:

ngAfterViewInit() {
  var subscription = fromEvent(this.searchInputRef.nativeElement, 'input').pipe(
    map((e: KeyboardEvent) => (<HTMLInputElement>e.target).value),
    debounceTime(400),
    distinctUntilChanged()
    //switchMap(() => this.performSearch())
  );
  subscription.subscribe(data => {
    this.performSearch(data);
  });

}  

private performSearch(searchText) {
  console.log("Search: " + searchText);
  this.searchText = searchText;
  this.courseResults = this.rankAndFilter(this.courses, searchText).sortByDesc('rank'); 
  return this.courseResults;
}