如何在谓词中 prioritize/rank FilteredList 结果?
How to prioritize/rank FilteredList results within a predicate?
我的应用程序包含一个 TextField
和一个 ListView
。 TextField
允许用户输入搜索词,这些搜索词将在他们键入时过滤 ListView
的内容。
过滤过程将匹配 ListView
和 return 结果中每个 DataItem
中的几个字段(如果其中任何一个匹配)。
但是,我想要做的是让这些结果优先考虑与一个特定字段匹配的项目。
例如,在下面的 MCVE 中,我有两个项目:Computer
和 Paper
。 Computer
项目有 "paper," 的 keyword
,因此搜索 "paper" 应该 return Computer
作为结果。
但是,由于我还有一个名为 Paper
的项目,因此搜索应该 return Paper
位于列表的顶部。但是,在 MCVE 中,结果仍然按字母顺序排列:
问题: 我将如何确保与 DataItem.name
的任何匹配都列在上面与 DataItem.keywords
的匹配?
编辑: 在搜索字段中输入 "pap" 也应该在顶部 return "Paper",然后是剩余的匹配项,如部分搜索词与 DataItem
名称部分匹配。
MCVE
DataItem.java:
import java.util.List;
public class DataItem {
// Instance properties
private final IntegerProperty id = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
private final StringProperty description = new SimpleStringProperty();
// List of search keywords
private final ObjectProperty<List<String>> keywords = new SimpleObjectProperty<>();
public DataItem(int id, String name, String description, List<String> keywords) {
this.id.set(id);
this.name.set(name);
this.description.set(description);
this.keywords.set(keywords);
}
/**
* Creates a space-separated String of all the keywords; used for filtering later
*/
public String getKeywordsString() {
StringBuilder sb = new StringBuilder();
for (String keyword : keywords.get()) {
sb.append(keyword).append(" ");
}
return sb.toString();
}
public int getId() {
return id.get();
}
public IntegerProperty idProperty() {
return id;
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public String getDescription() {
return description.get();
}
public StringProperty descriptionProperty() {
return description;
}
public List<String> getKeywords() {
return keywords.get();
}
public ObjectProperty<List<String>> keywordsProperty() {
return keywords;
}
@Override
public String toString() {
return name.get();
}
}
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.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class Main extends Application {
// TextField used for filtering the ListView
TextField txtSearch = new TextField();
// ListView to hold our DataItems
ListView<DataItem> dataItemListView = new ListView<>();
// The ObservableList of DataItems
ObservableList<DataItem> dataItems;
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));
// Add the search field and ListView to the layout
root.getChildren().addAll(txtSearch, dataItemListView);
// Build the dataItems List
dataItems = FXCollections.observableArrayList(buildDataItems());
// Add the filter logic
addSearchFilter();
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
/**
* Adds the functionality to filter the list dynamically as search terms are entered
*/
private void addSearchFilter() {
// Wrap the dataItems list in a filtered list, initially showing all items, alphabetically
FilteredList<DataItem> filteredList = new FilteredList<>(
dataItems.sorted(Comparator.comparing(DataItem::getName)));
// Add the predicate to filter the list whenever the search field changes
txtSearch.textProperty().addListener((observable, oldValue, newValue) ->
filteredList.setPredicate(dataItem -> {
// Clear any selection already present
dataItemListView.getSelectionModel().clearSelection();
// If the search field is empty, show all DataItems
if (newValue == null || newValue.isEmpty()) {
return true;
}
// Compare the DataItem's name and keywords with the search query (ignoring case)
String query = newValue.toLowerCase();
if (dataItem.getName().toLowerCase().contains(query)) {
// DataItem's name contains the search query
return true;
} else {
// Otherwise check if any of the search terms match those in the DataItem's keywords
// We split the query by space so we can match DataItems with multiple keywords
String[] searchTerms = query.split(" ");
boolean match = false;
for (String searchTerm : searchTerms) {
match = dataItem.getKeywordsString().toLowerCase().contains(searchTerm);
}
return match;
}
}));
// Wrap the filtered list in a SortedList
SortedList<DataItem> sortedList = new SortedList<>(filteredList);
// Update the ListView
dataItemListView.setItems(sortedList);
}
/**
* Generates a list of sample products
*/
private List<DataItem> buildDataItems() {
List<DataItem> dataItems = new ArrayList<>();
dataItems.add(new DataItem(
1, "School Supplies", "Learn things.",
Arrays.asList("pens", "pencils", "paper", "eraser")));
dataItems.add(new DataItem(
2, "Computer", "Do some things",
Arrays.asList("paper", "cpu", "keyboard", "monitor")));
dataItems.add(new DataItem(
3, "Keyboard", "Type things",
Arrays.asList("keys", "numpad", "input")));
dataItems.add(new DataItem(
4, "Printer", "Print things.",
Arrays.asList("paper", "ink", "computer")));
dataItems.add(new DataItem(
5, "Paper", "Report things.",
Arrays.asList("write", "printer", "notebook")));
return dataItems;
}
}
我可能找到了其他方法来完成此操作。我没有使用 Predicate
,而是将 ChangeListener
更改为只使用几个循环并手动构建一个新的 List
:
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null || newValue.isEmpty()) {
// Reset the ListView to show all items
dataItemListView.setItems(dataItems);
return;
}
ObservableList<DataItem> filteredList = FXCollections.observableArrayList();
String query = newValue.toLowerCase().trim();
// First, look for exact matches within the DataItem's name
for (DataItem item : dataItems) {
if (item.getName().toLowerCase().contains(query)) {
filteredList.add(0, item);
} else {
// If the item's name doesn't match, we'll look through search terms instead
String[] searchTerms = query.split(" ");
for (String searchTerm : searchTerms) {
// If the item has this searchTerm and has not already been added to the filteredList, add it
// now
if (item.getKeywordsString().toLowerCase().contains(searchTerm)
&& !filteredList.contains(item)) {
filteredList.add(item);
}
}
}
}
dataItemListView.setItems(filteredList);
我暂时不回答这个问题,看看是否有人也有使用 Predicate
的解决方案。
如果没记错的话,您只需要找到一种方法来正确排序过滤后的结果即可。为了简单起见,我将使用这个比较器而不是你的:
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 itemName, String searchKey) {
int sum = 0;
if (itemName.startsWith(searchKey)) {
sum += 2;
}
if (itemName.contains(searchKey)) {
sum += 1;
}
return sum;
}
};
在上面的代码中,我比较了两个DataItem。每个人都会有一个 'score',这取决于他们的名字与我们的搜索关键字的相似程度。为简单起见,假设如果 searchKey
出现在我们的项目名称中,我们给 1 分,如果项目名称以 searchKey
开头,我们给 2 分,所以现在我们可以比较这两者并对其进行排序。如果我们 return -1 item1 将首先放置,如果我们 return 1 则 item2 将首先放置,否则 return 0。
这是我在您的示例中使用的 addSearchFilter()
方法:
private void addSearchFilter() {
FilteredList<DataItem> filteredList = new FilteredList<>(dataItems);
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> filteredList.setPredicate(dataItem -> {
dataItemListView.getSelectionModel().clearSelection();
if (newValue == null || newValue.isEmpty()) {
return true;
}
String query = newValue.toLowerCase();
if (dataItem.getName().toLowerCase().contains(query)) {
return true;
} else {
String[] searchTerms = query.split(" ");
boolean match = false;
for (String searchTerm : searchTerms) {
match = dataItem.getKeywordsString().toLowerCase().contains(searchTerm);
}
return match;
}
}));
SortedList<DataItem> sortedList = new SortedList<>(filteredList);
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 itemName, String searchKey) {
int sum = 0;
if (itemName.startsWith(searchKey)) {
sum += 2;
}
if (itemName.contains(searchKey)) {
sum += 1;
}
return sum;
}
};
sortedList.setComparator(byName);
dataItemListView.setItems(sortedList);
}
当然 findScore()
可以更复杂,如果你想创建一个更复杂的评分系统(例如检查大小写字母,根据在项目中找到的关键字的位置给予更多的分数姓名等)。
我的应用程序包含一个 TextField
和一个 ListView
。 TextField
允许用户输入搜索词,这些搜索词将在他们键入时过滤 ListView
的内容。
过滤过程将匹配 ListView
和 return 结果中每个 DataItem
中的几个字段(如果其中任何一个匹配)。
但是,我想要做的是让这些结果优先考虑与一个特定字段匹配的项目。
例如,在下面的 MCVE 中,我有两个项目:Computer
和 Paper
。 Computer
项目有 "paper," 的 keyword
,因此搜索 "paper" 应该 return Computer
作为结果。
但是,由于我还有一个名为 Paper
的项目,因此搜索应该 return Paper
位于列表的顶部。但是,在 MCVE 中,结果仍然按字母顺序排列:
问题: 我将如何确保与 DataItem.name
的任何匹配都列在上面与 DataItem.keywords
的匹配?
编辑: 在搜索字段中输入 "pap" 也应该在顶部 return "Paper",然后是剩余的匹配项,如部分搜索词与 DataItem
名称部分匹配。
MCVE
DataItem.java:
import java.util.List;
public class DataItem {
// Instance properties
private final IntegerProperty id = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
private final StringProperty description = new SimpleStringProperty();
// List of search keywords
private final ObjectProperty<List<String>> keywords = new SimpleObjectProperty<>();
public DataItem(int id, String name, String description, List<String> keywords) {
this.id.set(id);
this.name.set(name);
this.description.set(description);
this.keywords.set(keywords);
}
/**
* Creates a space-separated String of all the keywords; used for filtering later
*/
public String getKeywordsString() {
StringBuilder sb = new StringBuilder();
for (String keyword : keywords.get()) {
sb.append(keyword).append(" ");
}
return sb.toString();
}
public int getId() {
return id.get();
}
public IntegerProperty idProperty() {
return id;
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public String getDescription() {
return description.get();
}
public StringProperty descriptionProperty() {
return description;
}
public List<String> getKeywords() {
return keywords.get();
}
public ObjectProperty<List<String>> keywordsProperty() {
return keywords;
}
@Override
public String toString() {
return name.get();
}
}
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.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class Main extends Application {
// TextField used for filtering the ListView
TextField txtSearch = new TextField();
// ListView to hold our DataItems
ListView<DataItem> dataItemListView = new ListView<>();
// The ObservableList of DataItems
ObservableList<DataItem> dataItems;
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));
// Add the search field and ListView to the layout
root.getChildren().addAll(txtSearch, dataItemListView);
// Build the dataItems List
dataItems = FXCollections.observableArrayList(buildDataItems());
// Add the filter logic
addSearchFilter();
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
/**
* Adds the functionality to filter the list dynamically as search terms are entered
*/
private void addSearchFilter() {
// Wrap the dataItems list in a filtered list, initially showing all items, alphabetically
FilteredList<DataItem> filteredList = new FilteredList<>(
dataItems.sorted(Comparator.comparing(DataItem::getName)));
// Add the predicate to filter the list whenever the search field changes
txtSearch.textProperty().addListener((observable, oldValue, newValue) ->
filteredList.setPredicate(dataItem -> {
// Clear any selection already present
dataItemListView.getSelectionModel().clearSelection();
// If the search field is empty, show all DataItems
if (newValue == null || newValue.isEmpty()) {
return true;
}
// Compare the DataItem's name and keywords with the search query (ignoring case)
String query = newValue.toLowerCase();
if (dataItem.getName().toLowerCase().contains(query)) {
// DataItem's name contains the search query
return true;
} else {
// Otherwise check if any of the search terms match those in the DataItem's keywords
// We split the query by space so we can match DataItems with multiple keywords
String[] searchTerms = query.split(" ");
boolean match = false;
for (String searchTerm : searchTerms) {
match = dataItem.getKeywordsString().toLowerCase().contains(searchTerm);
}
return match;
}
}));
// Wrap the filtered list in a SortedList
SortedList<DataItem> sortedList = new SortedList<>(filteredList);
// Update the ListView
dataItemListView.setItems(sortedList);
}
/**
* Generates a list of sample products
*/
private List<DataItem> buildDataItems() {
List<DataItem> dataItems = new ArrayList<>();
dataItems.add(new DataItem(
1, "School Supplies", "Learn things.",
Arrays.asList("pens", "pencils", "paper", "eraser")));
dataItems.add(new DataItem(
2, "Computer", "Do some things",
Arrays.asList("paper", "cpu", "keyboard", "monitor")));
dataItems.add(new DataItem(
3, "Keyboard", "Type things",
Arrays.asList("keys", "numpad", "input")));
dataItems.add(new DataItem(
4, "Printer", "Print things.",
Arrays.asList("paper", "ink", "computer")));
dataItems.add(new DataItem(
5, "Paper", "Report things.",
Arrays.asList("write", "printer", "notebook")));
return dataItems;
}
}
我可能找到了其他方法来完成此操作。我没有使用 Predicate
,而是将 ChangeListener
更改为只使用几个循环并手动构建一个新的 List
:
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null || newValue.isEmpty()) {
// Reset the ListView to show all items
dataItemListView.setItems(dataItems);
return;
}
ObservableList<DataItem> filteredList = FXCollections.observableArrayList();
String query = newValue.toLowerCase().trim();
// First, look for exact matches within the DataItem's name
for (DataItem item : dataItems) {
if (item.getName().toLowerCase().contains(query)) {
filteredList.add(0, item);
} else {
// If the item's name doesn't match, we'll look through search terms instead
String[] searchTerms = query.split(" ");
for (String searchTerm : searchTerms) {
// If the item has this searchTerm and has not already been added to the filteredList, add it
// now
if (item.getKeywordsString().toLowerCase().contains(searchTerm)
&& !filteredList.contains(item)) {
filteredList.add(item);
}
}
}
}
dataItemListView.setItems(filteredList);
我暂时不回答这个问题,看看是否有人也有使用 Predicate
的解决方案。
如果没记错的话,您只需要找到一种方法来正确排序过滤后的结果即可。为了简单起见,我将使用这个比较器而不是你的:
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 itemName, String searchKey) {
int sum = 0;
if (itemName.startsWith(searchKey)) {
sum += 2;
}
if (itemName.contains(searchKey)) {
sum += 1;
}
return sum;
}
};
在上面的代码中,我比较了两个DataItem。每个人都会有一个 'score',这取决于他们的名字与我们的搜索关键字的相似程度。为简单起见,假设如果 searchKey
出现在我们的项目名称中,我们给 1 分,如果项目名称以 searchKey
开头,我们给 2 分,所以现在我们可以比较这两者并对其进行排序。如果我们 return -1 item1 将首先放置,如果我们 return 1 则 item2 将首先放置,否则 return 0。
这是我在您的示例中使用的 addSearchFilter()
方法:
private void addSearchFilter() {
FilteredList<DataItem> filteredList = new FilteredList<>(dataItems);
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> filteredList.setPredicate(dataItem -> {
dataItemListView.getSelectionModel().clearSelection();
if (newValue == null || newValue.isEmpty()) {
return true;
}
String query = newValue.toLowerCase();
if (dataItem.getName().toLowerCase().contains(query)) {
return true;
} else {
String[] searchTerms = query.split(" ");
boolean match = false;
for (String searchTerm : searchTerms) {
match = dataItem.getKeywordsString().toLowerCase().contains(searchTerm);
}
return match;
}
}));
SortedList<DataItem> sortedList = new SortedList<>(filteredList);
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 itemName, String searchKey) {
int sum = 0;
if (itemName.startsWith(searchKey)) {
sum += 2;
}
if (itemName.contains(searchKey)) {
sum += 1;
}
return sum;
}
};
sortedList.setComparator(byName);
dataItemListView.setItems(sortedList);
}
当然 findScore()
可以更复杂,如果你想创建一个更复杂的评分系统(例如检查大小写字母,根据在项目中找到的关键字的位置给予更多的分数姓名等)。