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

时间:2018-07-23 03:36:21

标签: angular pipe angular-pipe

我目前具有基本的课程搜索功能,其中包含自定义管道过滤器stackblitz here。它根据course titledescriptionkeywords中是否存在匹配项返回结果,但是它以任意顺序返回课程。我想修改管道以首先返回更相关的结果,优先是搜索查询在1.标题,2。描述,3。关键字中匹配。

一些测试用例示例:

  1. 查询"chinese"当前在"1 French"之前返回"1X Chinese",因为"chinese"的关键字与"1 French"匹配。

  2. 查询"linear algebra"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的新手,实现细节超出了我目前的了解范围。这是最直接的方法吗?如果我知道我只过滤课程对象,是否可以修改当前的课程过滤器管道而不定义第二个orderBy管道?

任何指针将不胜感激!

1 个答案:

答案 0 :(得分:1)

您首先要为每个课程添加排名,然后按该顺序排序。即

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 / Angular 1不同)不提供orderBy或filter,它们建议您在组件中自己做这些,而不是使用管道:

  

Angular不提供用于过滤或排序列表的管道。   熟悉AngularJS的开发人员将其称为filter和orderBy。   Angular中没有等效项。

     

这不是疏忽。 Angular不提供此类管道,因为它们   表现不佳并防止过度缩小。

参考:AngularPipes

注意这一点,您可能会使用一个单独的courseResults列表,该列表是Course数组的经过过滤和排序的版本。在输入课程的任何搜索/过滤条件之前,您可以将其初始化为课程。而且,您还可以考虑通过在搜索输入中添加debounce来最大程度地减少不必要的搜索/过滤器调用(如果您在输入时执行此操作;如果在按钮上执行操作,请单击然后跳过)。

简单解决方案:

因此,在您的StackBlitz的基础上,我不再使用管道,而是制作了一个新的StackBlitz

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

我们不显示所有课程,但显示课程结果...

<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;
  });
}

扩展解决方案:

[Edit]一个新的forked StackBlitz,它使用去抖功能(如果用户输入速度非常快,则不会对每个按键进行整个过滤和排序)和distinctUntilChanged(如果用户将一个字符退格,不会打扰),并创建了Array的全局扩展(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;
}