如何创建自定义比较器来对搜索结果进行排名?

How to create custom Comparator to rank search results?

This is a followup to a prior question I posted .

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

中列出的项目

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

过滤功能正常。

但是,我现在需要根据匹配的字段 用户 Type.

对结果进行排名

MCVE CODE

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 class 中有一个空的 Comparator。这就是我需要帮助的。我过去创建了能够基于一个字段进行排序的比较器(来自我的):

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

不过,我不确定如何针对多个领域进行调整。具体来说,我希望能够选择应该对哪些字段进行排名 "higher."

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

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

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


Whosebug 上有几篇文章涉及按多个字段排序,但我发现的所有文章都是将 PersonPerson 进行比较。在这里,我需要将 Person 字段与 txtSearch.getText() 值进行比较。

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

您可以通过将比较器链接在一起来对多个字段进行排序。如果第一个比较器声明两个对象相等,则委托给下一个比较器并继续这样,直到查询完所有比较器或其中任何一个返回了 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 的解决方案。

你的计分理念很接近,你只要想出因素,遵守规则就可以了。

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

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) ),使您可以访问整数中的更多 "bits"。您可以使用 long,或者您可以使用 BigDecimal 值并根据需要设置任意多的条件。

但基本原理是一样的。

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