How to prioritize/rank FilteredList results within a predicate?

余生长醉 提交于 2019-12-22 08:49:16

问题


My application contains a TextField and a ListView. The TextField allows users to enter search terms that will filter the contents of the ListView as they type.

The filtering process will match several fields within each DataItem in the ListView and return the results if any of them match.

What I want to do, however, is have those results prioritize items that match one particular field over the others.

For example, in the MCVE below, I have two items: Computer and Paper. The Computer item has a keyword for "paper," so searching for "paper" should return Computer as a result.

However, since I also have an item called Paper, the search should return Paper at the top of the list. In the MCVE, though, the results are still alphabetized:

Question: How would I go about ensuring any matches to the DataItem.name are listed above matches to a DataItem.keywords?

EDIT: Entering "pap" in the search field should also return "Paper" at the top, followed by the remaining matches, as the partial search term partially matches the DataItem name.


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

回答1:


If not mistaken you only need to find a way to sort your filtered results correctly. To keep it simple I will use this comparator instead of yours :

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

In the code above, I compare two DataItem. Each one will have a 'score' which depends on how similar their names are from our search keyword. For simplicity lets say we give 1 point if the searchKey appeared in the name of our item and 2 points if the item name starts with the searchKey, so now we can compare those two and sort them. If we return -1 the item1 will be placed first, if we return 1 then the item2 will be placed first and return 0 otherwise.

Here is addSearchFilter() method I used in your example :

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

Of course the findScore() could be more sophisticated if you want to create a more complex score system (for example checking upper and lower case letters, give more points depending the position of the keyword found in the item name etc).




回答2:


I may have found a different way to accomplish this. Instead of using a Predicate, I've changed the ChangeListener to just use a couple of loops and build a new List manually:

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

I'll leave the question unanswered for now to see if anyone has a solution for using the Predicate as well.



来源:https://stackoverflow.com/questions/52434358/how-to-prioritize-rank-filteredlist-results-within-a-predicate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!