Как расставить приоритеты / ранжировать результаты FilteredList внутри предиката? - PullRequest
0 голосов
/ 21 сентября 2018

Мое приложение содержит TextField и ListView.TextField позволяет пользователям вводить поисковые термины, которые будут фильтровать содержимое ListView при вводе.

Процесс фильтрации будет сопоставлять несколько полей в каждом DataItem в ListView и возвращатьрезультаты, если какой-либо из них совпадает.

Однако я хочу, чтобы эти результаты располагали по приоритетам элементы, которые соответствуют одному конкретному полю над остальными.

Например, в MCVE ниже,У меня есть два предмета: Computer и Paper.Элемент Computer имеет keyword для «бумаги», поэтому при поиске «paper» в результате будет возвращено Computer.

Однако, поскольку у меня также есть элемент с именем Paper,поиск должен вернуть Paper вверху списка.В MCVE, тем не менее, результаты по-прежнему расположены в алфавитном порядке:

Screenshot

Вопрос: Как мне обеспечить соответствие любымDataItem.name перечисленные выше соответствуют DataItem.keywords?

РЕДАКТИРОВАТЬ: При вводе «pap» в поле поиска также должно возвращаться «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;
    }
}

Ответы [ 2 ]

0 голосов
/ 21 сентября 2018

Если не ошибаюсь, вам нужно только найти способ правильно отсортировать отфильтрованные результаты.Для простоты я буду использовать этот компаратор вместо вашего:

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.У каждого будет «оценка», которая зависит от того, насколько похожи их имена из нашего поискового ключевого слова.Для простоты, скажем, мы даем 1 балл, если в названии нашего элемента появилось searchKey, и 2 балла, если имя элемента начинается с searchKey, поэтому теперь мы можем сравнить эти два и отсортировать их.Если мы вернем -1, item1 будет помещен первым, если мы вернем 1, тогда item2 будет помещен первым и вернет 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() может быть более изощренным, если вы хотите создать более сложную систему оценок (например, проверять заглавные и строчные буквы, давать больше очков в зависимости от позиции ключевого слова, найденного в имени элемента и т. Д.).).

0 голосов
/ 21 сентября 2018

Возможно, я нашел другой способ сделать это.Вместо того, чтобы использовать 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.

...