简体   繁体   中英

Search multiple HashMaps at the same time

tldr : How can I search for an entry in multiple (read-only) Java HashMaps at the same time?


The long version:

I have several dictionaries of various sizes stored as HashMap< String, String > . Once they are read in, they are never to be changed (strictly read-only). I want to check whether and which dictionary had stored an entry with my key.

My code was originally looking for a key like this:

public DictionaryEntry getEntry(String key) {
    for (int i = 0; i < _numDictionaries; i++) {
        HashMap<String, String> map = getDictionary(i);
        if (map.containsKey(key))
             return new DictionaryEntry(map.get(key), i);
    }
    return null;
}

Then it got a little more complicated: my search string could contain typos, or was a variant of the stored entry. Like, if the stored key was "banana", it is possible that I'd look up "bannana" or "a banana", but still would like the entry for "banana" returned. Using the Levenshtein-Distance, I now loop through all dictionaries and each entry in them:

public DictionaryEntry getEntry(String key) {
    for (int i = 0; i < _numDictionaries; i++) {
        HashMap<String, String> map = getDictionary(i);
        for (Map.Entry entry : map.entrySet) {
            // Calculate Levenshtein distance, store closest match etc.
        }
    }
    // return closest match or null.
}    

So far everything works as it should and I'm getting the entry I want. Unfortunately I have to look up around 7000 strings, in five dictionaries of various sizes (~ 30 - 70k entries) and it takes a while. From my processing output I have the strong impression my lookup dominates overall runtime.

My first idea to improve runtime was to search all dictionaries parallely. Since none of the dictionaries is to be changed and no more than one thread is accessing a dictionary at the same time, I don't see any safety concerns.

The question is just: how do I do this? I have never used multithreading before. My search only came up with Concurrent HashMaps (but to my understanding, I don't need this) and the Runnable-class, where I'd have to put my processing into the method run() . I think I could rewrite my current class to fit into Runnable, but I was wondering if there is maybe a simpler method to do this (or how can I do it simply with Runnable, right now my limited understanding thinks I have to restructure a lot).


Since I was asked to share the Levenshtein-Logic: It's really nothing fancy, but here you go:

private int _maxLSDistance = 10;
public Map.Entry getClosestMatch(String key) {
    Map.Entry _closestMatch = null;
    int lsDist;

    if (key == null) {
        return null;
    }

    for (Map.Entry entry : _dictionary.entrySet()) {
        // Perfect match
        if (entry.getKey().equals(key)) {
            return entry;
        }
        // Similar match
        else {
            int dist = StringUtils.getLevenshteinDistance((String) entry.getKey(), key);

            // If "dist" is smaller than threshold and smaller than distance of already stored entry
            if (dist < _maxLSDistance) {
                if (_closestMatch == null || dist < _lsDistance) {
                    _closestMatch = entry;
                    _lsDistance = dist;
                }
            }
        }
    }
    return _closestMatch
}

In order to use multi-threading in your case, could be something like:

The "monitor" class, which basically stores the results and coordinates the threads;

public class Results {

    private int nrOfDictionaries = 4; //

    private ArrayList<String> results = new ArrayList<String>();

    public void prepare() {
        nrOfDictionaries = 4;
        results = new ArrayList<String>();
    }

    public synchronized void oneDictionaryFinished() {
        nrOfDictionaries--;
        System.out.println("one dictionary finished");
        notifyAll();
    }

    public synchronized boolean isReady() throws InterruptedException {

        while (nrOfDictionaries != 0) {
            wait();
        }

        return true;
    }

    public synchronized void addResult(String result) {
        results.add(result);
    }

    public ArrayList<String> getAllResults() {
        return results;
    }
}

The Thread it's self, which can be set to search for the specific dictionary:

public class ThreadDictionarySearch extends Thread {

    // the actual dictionary
    private String dictionary;
    private Results results;

    public ThreadDictionarySearch(Results results, String dictionary) {
        this.dictionary = dictionary;
        this.results = results;
    }

    @Override
    public void run() {

        for (int i = 0; i < 4; i++) {
            // search dictionary;
            results.addResult("result of " + dictionary);
            System.out.println("adding result from " + dictionary);
        }

        results.oneDictionaryFinished();
    }

}

And the main method for demonstration:

public static void main(String[] args) throws Exception {

    Results results = new Results();

    ThreadDictionarySearch threadA = new ThreadDictionarySearch(results, "dictionary A");
    ThreadDictionarySearch threadB = new ThreadDictionarySearch(results, "dictionary B");
    ThreadDictionarySearch threadC = new ThreadDictionarySearch(results, "dictionary C");
    ThreadDictionarySearch threadD = new ThreadDictionarySearch(results, "dictionary D");

    threadA.start();
    threadB.start();
    threadC.start();
    threadD.start();

    if (results.isReady())
    // it stays here until all dictionaries are searched
    // because in "Results" it's told to wait() while not finished;

for (String string : results.getAllResults()) {
        System.out.println("RESULT: " + string);
    }

I think the easiest would be to use a stream over the entry set:

public DictionaryEntry getEntry(String key) {
  for (int i = 0; i < _numDictionaries; i++) {
    HashMap<String, String> map = getDictionary(i);

    map.entrySet().parallelStream().foreach( (entry) ->
                                     {
                                       // Calculate Levenshtein distance, store closest match etc.
                                     }
      );
  }
  // return closest match or null.
}

Provided you are using java 8 of course. You could also wrap the outer loop into an IntStream as well. Also you could directly use the Stream.reduce to get the entry with the smallest distance.

Maybe try thread pools:

ExecutorService es = Executors.newFixedThreadPool(_numDictionaries);
for (int i = 0; i < _numDictionaries; i++) {
    //prepare a Runnable implementation that contains a logic of your search
    es.submit(prepared_runnable);
}

I believe you may also try to find a quick estimate of strings that completely do not match (ie significant difference in length), and use it to finish your logic ASAP, moving to next candidate.

I have my strong doubts that HashMaps are a suitable solution here, especially if you want to have some fuzzing and stop words. You should utilize a proper full text search solutions like ElaticSearch or Apache Solr or at least an available engine like Apache Lucene .

That being said, you can use a poor man's version: Create an array of your maps and a SortedMap, iterate over the array, take the keys of the current HashMap and store them in the SortedMap with the index of their HashMap. To retrieve a key, you first search in the SortedMap for said key, get the respective HashMap from the array using the index position and lookup the key in only one HashMap. Should be fast enough without the need for multiple threads to dig through the HashMaps. However, you could make the code below into a runnable and you can have multiple lookups in parallel.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

public class Search {

    public static void main(String[] arg) {

        if (arg.length == 0) {
            System.out.println("Must give a search word!");
            System.exit(1);
        }

        String searchString = arg[0].toLowerCase();

        /*
         * Populating our HashMaps.
         */
        HashMap<String, String> english = new HashMap<String, String>();
        english.put("banana", "fruit");
        english.put("tomato", "vegetable");

        HashMap<String, String> german = new HashMap<String, String>();
        german.put("Banane", "Frucht");
        german.put("Tomate", "Gemüse");

        /*
         * Now we create our ArrayList of HashMaps for fast retrieval
         */

        List<HashMap<String, String>> maps = new ArrayList<HashMap<String, String>>();
        maps.add(english);
        maps.add(german);


        /*
         * This is our index
         */
        SortedMap<String, Integer> index = new TreeMap<String, Integer>(String.CASE_INSENSITIVE_ORDER);


        /*
         * Populating the index:
         */
        for (int i = 0; i < maps.size(); i++) {
            // We iterate through or HashMaps...
            HashMap<String, String> currentMap = maps.get(i);

            for (String key : currentMap.keySet()) {
                /* ...and populate our index with lowercase versions of the keys,
                 * referencing the array from which the key originates.
                 */ 
                index.put(key.toLowerCase(), i);
            }

        }


         // In case our index contains our search string...
        if (index.containsKey(searchString)) {

            /* 
             * ... we find out in which map of the ones stored in maps
             * the word in the index originated from.
             */
            Integer mapIndex = index.get(searchString);

            /*
             * Next, we look up said map.
             */
            HashMap<String, String> origin = maps.get(mapIndex);

            /*
             * Last, we retrieve the value from the origin map
             */

            String result = origin.get(searchString);

            /*
             * The above steps can be shortened to
             *  String result = maps.get(index.get(searchString).intValue()).get(searchString);
             */

            System.out.println(result);
        } else {
            System.out.println("\"" + searchString + "\" is not in the index!");
        }
    }

}

Please note that this is a rather naive implementation only provided for illustration purposes. It doesn't address several problems (you can't have duplicate index entries, for example).

With this solution, you are basically trading startup speed for query speed.

Okay!!..

Since your concern is to get faster response.

I would suggest you to divide the work between threads.

Lets you have 5 dictionaries May be keep three dictionaries to one thread and rest two will take care by another thread. And then witch ever thread finds the match will halt or terminate the other thread.

May be you need an extra logic to do that dividing work ... But that wont effect your performance time.

And may be you need little more changes in your code to get your close match:

for (Map.Entry entry : _dictionary.entrySet()) {

you are using EntrySet But you are not using values anyway it seems getting entry set is a bit expensive. And I would suggest you to just use keySet since you are not really interested in the values in that map

 for (Map.Entry entry : _dictionary.keySet()) {

For more details on the proformance of map Please read this link Map performances

Iteration over the collection-views of a LinkedHashMap requires time proportional to the size of the map, regardless of its capacity. Iteration over a HashMap is likely to be more expensive, requiring time proportional to its capacity.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM