【问题标题】:How to create custom Comparator to rank search results?如何创建自定义比较器来对搜索结果进行排名?
【发布时间】:2018-10-04 14:01:07
【问题描述】:

这是我之前发布的问题 here 的后续。

在下面的 MCVE 中,我有一个 TableView 显示 Person 对象的列表。在列表上方,我有一个TextField,用于过滤TableView 中列出的项目。

Person 类包含 4 个字段,但我的搜索字段只检查其中 3 个字段中的匹配项:userIdlastNameemailAddress

过滤功能按预期工作。

但是,我现在需要根据匹配的字段用户Type对结果进行排名。

MCVE 代码

Person.java

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public final class Person {

    private StringProperty userType = new SimpleStringProperty();
    private IntegerProperty userId = new SimpleIntegerProperty();
    private StringProperty firstName = new SimpleStringProperty();
    private StringProperty lastName = new SimpleStringProperty();
    private StringProperty emailAddress = new SimpleStringProperty();

    public Person(String type, int id, String firstName, String lastName, String emailAddress) {
        this.userType.set(type);
        this.userId.set(id);
        this.firstName.set(firstName);
        this.lastName.set(lastName);
        this.emailAddress.set(emailAddress);
    }

    public String getUserType() {
        return userType.get();
    }

    public void setUserType(String userType) {
        this.userType.set(userType);
    }

    public StringProperty userTypeProperty() {
        return userType;
    }

    public int getUserId() {
        return userId.get();
    }

    public void setUserId(int userId) {
        this.userId.set(userId);
    }

    public IntegerProperty userIdProperty() {
        return userId;
    }

    public String getFirstName() {
        return firstName.get();
    }

    public void setFirstName(String firstName) {
        this.firstName.set(firstName);
    }

    public StringProperty firstNameProperty() {
        return firstName;
    }

    public String getLastName() {
        return lastName.get();
    }

    public void setLastName(String lastName) {
        this.lastName.set(lastName);
    }

    public StringProperty lastNameProperty() {
        return lastName;
    }

    public String getEmailAddress() {
        return emailAddress.get();
    }

    public void setEmailAddress(String emailAddress) {
        this.emailAddress.set(emailAddress);
    }

    public StringProperty emailAddressProperty() {
        return emailAddress;
    }
}

Main.java

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.Comparator;

public class Main extends Application {

    TableView<Person> tableView;
    private TextField txtSearch;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        // Simple Interface
        VBox root = new VBox(10);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(10));

        // Create the TableView of data
        tableView = new TableView<>();
        TableColumn<Person, Integer> colId = new TableColumn<>("ID");
        TableColumn<Person, String> colFirstName = new TableColumn<>("First Name");
        TableColumn<Person, String> colLastName = new TableColumn<>("Last Name");
        TableColumn<Person, String> colEmailAddress = new TableColumn<>("Email Address");

        // Set the ValueFactories
        colId.setCellValueFactory(new PropertyValueFactory<>("userId"));
        colFirstName.setCellValueFactory(new PropertyValueFactory<>("firstName"));
        colLastName.setCellValueFactory(new PropertyValueFactory<>("lastName"));
        colEmailAddress.setCellValueFactory(new PropertyValueFactory<>("emailAddress"));

        // Add columns to the TableView
        tableView.getColumns().addAll(colId, colFirstName, colLastName, colEmailAddress);

        // Create the filter/search TextField
        txtSearch = new TextField();
        txtSearch.setPromptText("Search ...");

        addSearchFilter(getPersons());

        // Add the controls to the layout
        root.getChildren().addAll(txtSearch, tableView);

        // Show the stage
        primaryStage.setScene(new Scene(root));
        primaryStage.setTitle("Sample");
        primaryStage.show();
    }

    private void addSearchFilter(ObservableList<Person> list) {

        FilteredList<Person> filteredList = new FilteredList<Person>(list);

        txtSearch.textProperty().addListener(((observable, oldValue, newValue) ->
                filteredList.setPredicate(person -> {

                    // Clear any currently-selected item from the TableView
                    tableView.getSelectionModel().clearSelection();

                    // If search field is empty, show everything
                    if (newValue == null || newValue.trim().isEmpty()) {
                        return true;
                    }

                    // Grab the trimmed search string
                    String query = newValue.trim().toLowerCase();

                    // Convert the query to an array of individual search terms
                    String[] keywords = query.split("[\\s]+");

                    // Create a single string containing all the data we will match against
                    // BONUS QUESTION: Is there a better way to do this?
                    String matchString =
                            String.valueOf(person.getUserId())
                                    + person.getLastName().toLowerCase()
                                    + person.getEmailAddress().toLowerCase();

                    // Check if ALL the keywords exist in the matchString; if any are absent, return false;
                    for (String keyword : keywords) {
                        if (!matchString.contains(keyword)) return false;
                    }

                    // All entered keywords exist in this Person's searchable fields
                    return true;

                })));

        SortedList<Person> sortedList = new SortedList<>(filteredList);

        // Create the Comparator to allow ranking of search results
        Comparator<Person> comparator = new Comparator<Person>() {
            @Override
            public int compare(Person person, Person t1) {
                return 0;

            }
        };

        // Set the comparator and bind list to the TableView
        sortedList.setComparator(comparator);
        tableView.setItems(sortedList);

    }

    private ObservableList<Person> getPersons() {

        ObservableList<Person> personList = FXCollections.observableArrayList();

        personList.add(new Person("DECEASED", 123, "Chrissie", "Watkins", "fishfood@email.com"));
        personList.add(new Person("VET", 342, "Matt", "Hooper", "m.hooper@noaa.gov"));
        personList.add(new Person("VET", 526, "Martin", "Brody", "chiefofpolice@amity.gov"));
        personList.add(new Person("NEW", 817, "Larry", "Vaughn", "lvaughn@amity.gov"));

        return personList;
    }
}

您会看到我的Main 班级中有一个空的Comparator。这是我需要帮助的。我过去创建了能够基于一个字段进行排序的比较器(来自我的previous question):

    Comparator<DataItem> byName = new Comparator<DataItem>() {
        @Override
        public int compare(DataItem o1, DataItem o2) {
            String searchKey = txtSearch.getText().toLowerCase();
            int item1Score = findScore(o1.getName().toLowerCase(), searchKey);
            int item2Score = findScore(o2.getName().toLowerCase(), searchKey);

            if (item1Score > item2Score) {
                return -1;
            }

            if (item2Score > item1Score) {
                return 1;
            }

            return 0;
        }

        private int findScore(String item1Name, String searchKey) {
            int sum = 0;
            if (item1Name.startsWith(searchKey)) {
                sum += 2;
            }

            if (item1Name.contains(searchKey)) {
                sum += 1;
            }
            return sum;
        }
    };

不过,我不确定如何使其适用于多个领域。具体来说,我希望能够选择哪些字段应该排名“更高”。

对于这个例子,我想要完成的是按这个顺序对列表进行排序:

  1. userIdkeyword 开头
  2. lastNamekeyword 开头
  3. emailAddresskeyword 开头
  4. lastName 包含一个 keyword
  5. emailAddress 包含一个 keyword
  6. 在匹配范围内,任何 userType = "VET" 都应首先列出

我不是在寻找 Google 级别的算法,而只是在寻找某种方式来确定匹配的优先级。我对 Comparator 类不是很熟悉,并且很难理解它的 JavaDocs,因为它适用于我的需求。


StackOverflow 上有几篇关于按多个字段排序的帖子,但我发现的所有帖子都将PersonPerson 进行比较。在这里,我需要将Person 字段与txtSearch.getText() 值进行比较。

我将如何重构 Comparator 以设置这种性质的自定义排序?

【问题讨论】:

    标签: java comparator


    【解决方案1】:

    你的评分概念很接近,你只需要想出因素并遵守规则。

    所以,这里有一个简单的例子:

    public int score(Item item, String query) {
        int score = 0;
    
        if (item.userId().startsWith(query) {
            score += 2000;
        }
        if (item.lastName().startsWith(query) {
            score += 200;
        } else if (item.lastName().contains(query) {
            score += 100;
        }
        if (item.email().startsWith(query) {
            score += 20;
        } else if (item.email().contains(query) {
            score += 10;
        }
        if (item.userType().equals("VET")) {
            score += 5;
        }
    
        return score;
    }
    

    如您所见,我采用了您的每个标准并将它们转换为分数中的不同数字,并且对于每个标准中的区别,我有不同的值(例如 10 与 20)。最后,我为“VET”类型添加了 5。

    假设评分规则不是排他性的(即,每条规则都会优化评分,而不是停止评分),并且 VET 类型在每个标准中都是平局破坏者,而不是列表的顶部。如果 VET 需要排在列表顶部(即所有 VET 将在所有非 VET 之前显示),您可以将 5 更改为 10000,给它自己的数量级。

    现在,使用十进制数字很简单,但是在 9 之后您将用完数量级(您会溢出 int)——您还可以使用其他基数(在本例中为基数 3),让您可以访问整数中有更多“位”。您可以使用 long,或者您可以使用 BigDecimal 值并拥有任意数量的条件。

    但基本原理是一样的。

    获得分数后,只需比较比较器中两个值的分数即可。

    【讨论】:

      【解决方案2】:

      您可以通过将比较器链接在一起来对多个字段进行排序。如果第一个比较器声明两个对象相等,则将其委托给下一个比较器并像这样继续,直到所有比较器都被查询或其中任何一个返回的值不是 0。

      这是一个例子:

      static class Person {
          String name;
          int age;
          int id;
      }
      
      Comparator<Person> c3 = (p1, p2) -> {
          return Integer.compare(p1.id, p2.id);
      };
      
      Comparator<Person> c2 = (p1, p2) -> {
          if (p1.name.compareTo(p2.name) == 0) {
              return c3.compare(p1, p2);
          }
          return p1.name.compareTo(p2.name);
      };
      
      Comparator<Person> c1 = (p1, p2) -> {
          if (Integer.compare(p1.age, p2.age) == 0) {
              return c2.compare(p1, p2);
          }
          return Integer.compare(p1.age, p2.age);
      };
      

      比较器按c1、c2、c3的顺序查询。

      当然,这是一个过于简单的例子。在生产代码中,您最好使用更简洁、更面向 OOP 的解决方案。

      【讨论】:

      • 这些实际上是如何分配给我的示例对象的?
      • 我不明白你的问题。你到底是什么意思?哪一部分给你带来了麻烦?
      • 我的意思是,这些比较器是什么时候调用的?我的问题是如何在 Main 类中创建一个比较器来执行多个过滤器;我不知道如何调整您的答案以适应它,因为您已经修改了 Person 类。
      猜你喜欢
      • 2015-07-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-11-25
      • 2011-09-04
      • 1970-01-01
      • 1970-01-01
      • 2010-09-08
      相关资源
      最近更新 更多