Spelling correction, Query completion and Instant search

In the previous posts we described our new spelling correction algorithm, announced the release of the c# code as Open Source and presented some pretty compelling benchmark results.

FAROO: Spelling Correction and Query completion

Today we are introducing spelling correction, query completion and an improved instant search as integral part of our FAROO search service:

Spelling correction
For a misspelled term the suggested corrections are displayed in a dropdown list.

Query completion (aka query suggestions, autocomplete)
As you type a dropdown list of popular terms and combinations is displayed, which start with the letters you typed so far.

Improved instant search
Results are automatically displayed for the best suggestion/correction, “Enter” searches always for originally entered term.


Opposite to traditional implementations we are not using a predefined dictionary:

  • The whole web serves as a corpus, from which we automatically derive the spelling and completion dictionary.
  • The dictionary is automatically updated as part of the indexing process and learns new terms as they appear on the web.
  • The whole process is fully automated, no manual auditing steps are involved.

The algorithm is completely language independent (pure statistics, no linguistic knowledge).
Of course corrections and suggestions are given only within the scope of the selected search language.


Try it out at faroo.com

1000x Faster Spelling Correction: Source Code released

In a followup to our recent post 1000x Faster Spelling Correction algorithm we are releasing today a C# implementation of our Symmetric Delete Spelling Correction algorithm as Open Source:

Update1: The source code is now also on GitHub.
Update2: Improved implementation now 1,000,000 times faster for edit distance=3.

// SymSpell: 1000x faster through Symmetric Delete spelling correction algorithm
// The Symmetric Delete spelling correction algorithm reduces the complexity of edit candidate generation and dictionary lookup 
// for a given Damerau-Levenshtein distance. It is three orders of magnitude faster and language independent.
// Opposite to other algorithms only deletes are required, no transposes + replaces + inserts.
// Transposes + replaces + inserts of the input term are transformed into deletes of the dictionary term.
// Replaces and inserts are expensive and language dependent: e.g. Chinese has 70,000 Unicode Han characters!
// Copyright (C) 2012 Wolf Garbe, FAROO Limited
// Version: 1.6
// Author: Wolf Garbe <wolf.garbe@faroo.com>
// Maintainer: Wolf Garbe <wolf.garbe@faroo.com>
// URL: http://blog.faroo.com/2012/06/07/improved-edit-distance-based-spelling-correction/
// Description: http://blog.faroo.com/2012/06/07/improved-edit-distance-based-spelling-correction/
// License:
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License, 
// version 3.0 (LGPL-3.0) as published by the Free Software Foundation.
// http://www.opensource.org/licenses/LGPL-3.0
// Usage: single word + Enter:  Display spelling suggestions
//        Enter without input:  Terminate the program

using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.IO;
using System.Diagnostics;

static class SymSpell
    private static int editDistanceMax=2;
    private static int verbose = 0;
    //0: top suggestion
    //1: all suggestions of smallest edit distance 
    //2: all suggestions <= editDistanceMax (slower, no early termination)

    private class dictionaryItem
        public string term = "";
        public List<editItem> suggestions = new List<editItem>();
        public int count = 0;

        public override bool Equals(object obj)
            return Equals(term, ((dictionaryItem)obj).term);
        public override int GetHashCode()
            return term.GetHashCode(); 

    private class editItem
        public string term = "";
        public int distance = 0;

        public override bool Equals(object obj)
            return Equals(term, ((editItem)obj).term);
        public override int GetHashCode()
            return term.GetHashCode();

    private class suggestItem
        public string term = "";
        public int distance = 0;
        public int count = 0;

        public override bool Equals(object obj)
            return Equals(term, ((suggestItem)obj).term);
        public override int GetHashCode()
            return term.GetHashCode();

    private static Dictionary<string, dictionaryItem> dictionary = new Dictionary<string, dictionaryItem>();

    //create a non-unique wordlist from sample text
    //language independent (e.g. works with Chinese characters)
    private static IEnumerable<string> parseWords(string text)
        return Regex.Matches(text.ToLower(), @"[\w-[\d_]]+")
                    .Select(m => m.Value);

    //for every word there all deletes with an edit distance of 1..editDistanceMax created and added to the dictionary
    //every delete entry has a suggestions list, which points to the original term(s) it was created from
    //The dictionary may be dynamically updated (word frequency and new words) at any time by calling createDictionaryEntry
    private static bool CreateDictionaryEntry(string key, string language)
        bool result = false;
        dictionaryItem value;
        if (dictionary.TryGetValue(language+key, out value))
            //already exists:
            //1. word appears several times
            //2. word1==deletes(word2) 
            value = new dictionaryItem();
            dictionary.Add(language+key, value);

        //edits/suggestions are created only once, no matter how often word occurs
        //edits/suggestions are created only as soon as the word occurs in the corpus, 
        //even if the same term existed before in the dictionary as an edit from another word
        if (string.IsNullOrEmpty(value.term))
            result = true;
            value.term = key;

            //create deletes
            foreach (editItem delete in Edits(key, 0, true))
                editItem suggestion = new editItem();
                suggestion.term = key;
                suggestion.distance = delete.distance;

                dictionaryItem value2;
                if (dictionary.TryGetValue(language+delete.term, out value2))
                    //already exists:
                    //1. word1==deletes(word2) 
                    //2. deletes(word1)==deletes(word2) 
                    if (!value2.suggestions.Contains(suggestion)) AddLowestDistance(value2.suggestions, suggestion);
                    value2 = new dictionaryItem();
                    dictionary.Add(language+delete.term, value2);
        return result;

    //create a frequency disctionary from a corpus
    private static void CreateDictionary(string corpus, string language)
        if (!File.Exists(corpus))
            Console.Error.WriteLine("File not found: " + corpus);

        Console.Write("Creating dictionary ...");
        long wordCount = 0;
        foreach (string key in parseWords(File.ReadAllText(corpus)))
            if (CreateDictionaryEntry(key, language)) wordCount++;
        Console.WriteLine("\rDictionary created: " + wordCount.ToString("N0") + " words, " + dictionary.Count.ToString("N0") + " entries, for edit distance=" + editDistanceMax.ToString());

    //save some time and space
    private static void AddLowestDistance(List<editItem> suggestions, editItem suggestion)
        //remove all existing suggestions of higher distance, if verbose<2
        if ((verbose < 2) && (suggestions.Count > 0) && (suggestions[0].distance > suggestion.distance)) suggestions.Clear();
        //do not add suggestion of higher distance than existing, if verbose<2
        if ((verbose == 2) || (suggestions.Count == 0) || (suggestions[0].distance >= suggestion.distance)) suggestions.Add(suggestion);

    //inexpensive and language independent: only deletes, no transposes + replaces + inserts
    //replaces and inserts are expensive and language dependent (Chinese has 70,000 Unicode Han characters)
    private static List<editItem> Edits(string word, int editDistance, bool recursion)
        List<editItem> deletes = new List<editItem>();
        if (word.Length > 1)
            for (int i = 0; i < word.Length; i++)
                editItem delete = new editItem();
                delete.term=word.Remove(i, 1);
                if (!deletes.Contains(delete))
                    //recursion, if maximum edit distance not yet reached
                    if (recursion && (editDistance < editDistanceMax)) 
                        foreach (editItem edit1 in Edits(delete.term, editDistance,recursion))
                            if (!deletes.Contains(edit1)) deletes.Add(edit1); 

        return deletes;

    private static int TrueDistance(editItem dictionaryOriginal, editItem inputDelete, string inputOriginal)
        //We allow simultaneous edits (deletes) of editDistanceMax on on both the dictionary and the input term. 
        //For replaces and adjacent transposes the resulting edit distance stays <= editDistanceMax.
        //For inserts and deletes the resulting edit distance might exceed editDistanceMax.
        //To prevent suggestions of a higher edit distance, we need to calculate the resulting edit distance, if there are simultaneous edits on both sides.
        //Example: (bank==bnak and bank==bink, but bank!=kanb and bank!=xban and bank!=baxn for editDistanceMaxe=1)
        //Two deletes on each side of a pair makes them all equal, but the first two pairs have edit distance=1, the others edit distance=2.

        if (dictionaryOriginal.term == inputOriginal) return 0; else
        if (dictionaryOriginal.distance == 0) return inputDelete.distance;
        else if (inputDelete.distance == 0) return dictionaryOriginal.distance;
        else return DamerauLevenshteinDistance(dictionaryOriginal.term, inputOriginal);//adjust distance, if both distances>0

    private static List<suggestItem> Lookup(string input, string language, int editDistanceMax)
        List<editItem> candidates = new List<editItem>();

        //add original term
        editItem item = new editItem();
        item.term = input;
        item.distance = 0;
        List<suggestItem> suggestions = new List<suggestItem>();
        dictionaryItem value;

        while (candidates.Count>0)
            editItem candidate = candidates[0];

            //save some time
            //early termination
            //suggestion distance=candidate.distance... candidate.distance+editDistanceMax                
            //if canddate distance is already higher than suggestion distance, than there are no better suggestions to be expected
            if ((verbose < 2)&&(suggestions.Count > 0)&&(candidate.distance > suggestions[0].distance)) goto sort;
            if (candidate.distance > editDistanceMax) goto sort;  

            if (dictionary.TryGetValue(language+candidate.term, out value))
                if (!string.IsNullOrEmpty(value.term))
                    //correct term
                    suggestItem si = new suggestItem();
                    si.term = value.term;
                    si.count = value.count;
                    si.distance = candidate.distance;

                    if (!suggestions.Contains(si))
                        //early termination
                        if ((verbose < 2) && (candidate.distance == 0)) goto sort;     

                //edit term (with suggestions to correct term)
                dictionaryItem value2;
                foreach (editItem suggestion in value.suggestions)
                    //save some time 
                    //skipping double items early
                    if (suggestions.Find(x => x.term == suggestion.term) == null)
                        int distance = TrueDistance(suggestion, candidate, input);
                        //save some time.
                        //remove all existing suggestions of higher distance, if verbose<2
                        if ((verbose < 2) && (suggestions.Count > 0) && (suggestions[0].distance > distance)) suggestions.Clear();
                        //do not process higher distances than those already found, if verbose<2
                        if ((verbose < 2) && (suggestions.Count > 0) && (distance > suggestions[0].distance)) continue;

                        if (distance <= editDistanceMax)
                            if (dictionary.TryGetValue(language+suggestion.term, out value2))
                                suggestItem si = new suggestItem();
                                si.term = value2.term;
                                si.count = value2.count;
                                si.distance = distance;

            }//end foreach

            //add edits 
            if (candidate.distance < editDistanceMax)
                foreach (editItem delete in Edits(candidate.term, candidate.distance,false))
                    if (!candidates.Contains(delete)) candidates.Add(delete);
        }//end while

        sort: suggestions = suggestions.OrderBy(c => c.distance).ThenByDescending(c => c.count).ToList();
        if ((verbose == 0)&&(suggestions.Count>1))  return suggestions.GetRange(0, 1); else return suggestions;

    private static void Correct(string input, string language)
        List<suggestItem> suggestions = null;
        //Benchmark: 1000 x Lookup
        Stopwatch stopWatch = new Stopwatch();
        for (int i = 0; i < 1000; i++)
            suggestions = Lookup(input,language,editDistanceMax);
        //check in dictionary for existence and frequency; sort by edit distance, then by word frequency
        suggestions = Lookup(input, language, editDistanceMax);

        //display term and frequency
        foreach (var suggestion in suggestions)
            Console.WriteLine( suggestion.term + " " + suggestion.distance.ToString() + " " + suggestion.count.ToString());
        if (verbose == 2) Console.WriteLine(suggestions.Count.ToString() + " suggestions");

    private static void ReadFromStdIn()
        string word;
        while (!string.IsNullOrEmpty(word = (Console.ReadLine() ?? "").Trim()))

    public static void Main(string[] args)
        //e.g. http://norvig.com/big.txt , or any other large text corpus

    // Damerau–Levenshtein distance algorithm and code 
    // from http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance
    public static Int32 DamerauLevenshteinDistance(String source, String target)
        Int32 m = source.Length;
        Int32 n = target.Length;
        Int32[,] H = new Int32[m + 2, n + 2];

        Int32 INF = m + n;
        H[0, 0] = INF;
        for (Int32 i = 0; i <= m; i++) { H[i + 1, 1] = i; H[i + 1, 0] = INF; }
        for (Int32 j = 0; j <= n; j++) { H[1, j + 1] = j; H[0, j + 1] = INF; }

        SortedDictionary<Char, Int32> sd = new SortedDictionary<Char, Int32>();
        foreach (Char Letter in (source + target))
            if (!sd.ContainsKey(Letter))
                sd.Add(Letter, 0);

        for (Int32 i = 1; i <= m; i++)
            Int32 DB = 0;
            for (Int32 j = 1; j <= n; j++)
                Int32 i1 = sd[target[j - 1]];
                Int32 j1 = DB;

                if (source[i - 1] == target[j - 1])
                    H[i + 1, j + 1] = H[i, j];
                    DB = j;
                    H[i + 1, j + 1] = Math.Min(H[i, j], Math.Min(H[i + 1, j], H[i, j + 1])) + 1;

                H[i + 1, j + 1] = Math.Min(H[i + 1, j + 1], H[i1, j1] + (i - i1 - 1) + 1 + (j - j1 - 1));

            sd[ source[ i - 1 ]] = i;
        return H[m + 1, n + 1];

The implementation supports now edit distances of any size (default=2).

With previous spell checking algorithms the required time explodes with larger edit distances. They try to omit this with early termination when suggestions of smaller edit distances are found.

We did a quick benchmark with 1000 lookups:

Term Best correction Edit distance Faroo
Peter Norvig
marsupilami no correction* >3 1,772 165,025,000 93,129
acamodation accommodation 3 1,874 175,622,000 93,715
acomodation accommodation 2 162 348,191 2,149
hous house 1 71 179 2
house house 0 0 17 n/a

*Correct word, but not in dictionary and there are also no corrections within an edit distance of <=3. This is a quite common case (e.g. rare words, new words, domain specific words, foreign words, names).

The speed advantage grows exponentially with the edit distance:
For an edit distance=1 it’s the same order of magnitude,
for an edit distance=2 it’s 3 orders of magnitude faster,
for an edit distance=3 it’s 5 orders of magnitude faster.

Source code
The C# implementation of our Symmetric Delete Spelling Correction algorithm is released on GitHub as Open Source under the GNU Lesser General Public License (LGPL).

C# (original)

The following third party ports to other programming languages have not been tested by myself whether they are an exact port, error free, provide identical results or are as fast as the original algorithm:

C++ (third party port)

Go (third party port)

Java (third party port)

Javascript (third party port)

Python (third party port)

Ruby (third party port)

Swift (third party port)

1000x Faster Spelling Correction algorithm

1000x faster

Update: SymSpell C# implementation released as Open Source.
Update2: SymSpell 100,000 times faster for edit distance=3.
Update3: Spelling correction is now also part of FAROO search.
Update4: SymSpell source code now on GitHub.
Update5: Improved implementation now 1,000,000 times faster for edit distance=3.
Update6: SymSpellCompound: Compound aware automatic spelling correction.

Recently I answered a question on Quora about spelling correction for search engines. When I described our algorithm I was pointed to Peter Norvig’s page where he outlined his approach.

Both algorithms are based on Edit distance (Damerau-Levenshtein distance).
Both try to find the dictionary entries with smallest edit distance from the query term.
If the edit distance is 0 the term is spelled correctly, if the edit distance is <=2 the dictionary term is used as spelling suggestion. But our way to search the dictionary is different, resulting in a significant performance gain and language independence. Three ways to search for minimum edit distance in a dictionary: 1. Naive approach
The obvious way of doing this is to compute the edit distance from the query term to each dictionary term, before selecting the string(s) of minimum edit distance as spelling suggestion. This exhaustive search is inordinately expensive.
Source: Christopher D. Manning, Prabhakar Raghavan & Hinrich SchĂĽtze: Introduction to Information Retrieval.

The performance can be significantly improved by terminating the edit distance calculation as soon as a treshold of 2 or 3 has been reached.

2. Peter Norvig
Generate all possible terms with an edit distance <=2 (deletes + transposes + replaces + inserts) from the query term and search them in the dictionary.
For a word of length n, an alphabet size a, an edit distance d=1, there will be n deletions, n-1 transpositions, a*n alterations, and a*(n+1) insertions, for a total of 2n+2an+a-1 terms at search time.
Source: Peter Norvig: How to Write a Spelling Corrector.

This is much better than the naive approach, but still expensive at search time (114,324 terms for n=9, a=36, d=2) and language dependent (because the alphabet is used to generate the terms, which is different in many languages and huge in Chinese: a=70,000 Unicode Han characters)

3. Symmetric Delete Spelling Correction (FAROO)
Generate terms with an edit distance <=2 (deletes only) from each dictionary term and add them together with the original term to the dictionary. This has to be done only once during a pre-calculation step.
Generate terms with an edit distance <=2 (deletes only) from the input term and search them in the dictionary.
For a word of length n, an alphabet size of a, an edit distance of 1, there will be just n deletions, for a total of n terms at search time.

This is three orders of magnitude less expensive (36 terms for n=9 and d=2) and language independent (the alphabet is not required to generate deletes).
The cost of this approach is the pre-calculation time and storage space of x deletes for every original dictionary entry, which is acceptable in most cases.

The number x of deletes for a single dictionary entry depends on the maximum edit distance: x=n for edit distance=1, x=n*(n-1)/2 for edit distance=2, x=n!/d!/(n-d)! for edit distance=d (combinatorics: k out of n combinations without repetitions, and k=n-d),
E.g. for a maximum edit distance of 2 and an average word length of 5 and 100,000 dictionary entries we need to additionally store 1,500,000 deletes.

Remark 1: During the precalculation, different words in the dictionary might lead to same delete term: delete(sun,1)==delete(sin,1)==sn.
While we generate only one new dictionary entry (sn), inside we need to store both original terms as spelling correction suggestion (sun,sin)

Remark 2: There are four different comparison pair types:

  1. dictionary entry==input entry,
  2. delete(dictionary entry,p1)==input entry
  3. dictionary entry==delete(input entry,p2)
  4. delete(dictionary entry,p1)==delete(input entry,p2)

The last comparison type is required for replaces and transposes only. But we need to check whether the suggested dictionary term is really a replace or an adjacent transpose of the input term to prevent false positives of higher edit distance (bank==bnak and bank==bink, but bank!=kanb and bank!=xban and bank!=baxn).

Remark 3: Instead of a dedicated spelling dictionary we are using the search engine index itself. This has several benefits:

  1. It is dynamically updated. Every newly indexed word, whose frequency is over a certain threshold, is automatically used for spelling correction as well.
  2. As we need to search the index anyway the spelling correction comes at almost no extra cost.
  3. When indexing misspelled terms (i.e. not marked as a correct in the index) we do a spelling correction on the fly and index the page for the correct term as well.

Remark 4: We have implemented query suggestions/completion in a similar fashion. This is a good way to prevent spelling errors in the first place. Every newly indexed word, whose frequency is over a certain threshold, is stored as a suggestion to all of its prefixes (they are created in the index if they do not yet exist). As we anyway provide an instant search feature the lookup for suggestions comes also at almost no extra cost. Multiple terms are sorted by the number of results stored in the index.

In our algorithm we are exploiting the fact that the edit distance between two terms is symmetrical:

  1. We can generate all terms with an edit distance <2 from the query term (trying to reverse the query term error) and checking them against all dictionary terms,
  2. We can generate all terms with an edit distance <2 from each dictionary term (trying to create the query term error) and check the query term against them.
  3. We can combine both and meet in the middle, by transforming the correct dictionary terms to erroneous strings, and transforming the erroneous input term to the correct strings.
    Because adding a char on the dictionary is equivalent to removing a char from the input string and vice versa, we can on both sides restrict our transformation to deletes only.

We are using variant 3, because the delete-only-transformation is language independent and three orders of magnitude less expensive.

Where does the speed come from?

  • Pre-calculation, i.e. the generation of possible spelling error variants (deletes only) and storing them at index time is the first precondition.
  • A fast index access at search time by using a hash table with an average search time complexity of O(1) is the second precondition.
  • But only our Symmetric Delete Spelling Correction on top of this allows to bring this O(1) speed to spell checking, because it allows a tremendous reduction of the number of spelling error candidates to be pre-calculated (generated and indexed).
  • Applying pre-calculation to Norvig’s approach would not be feasible because pre-calculating all possible delete + transpose + replace + insert candidates of all terms would result in a huge time and space consumption.

Computational Complexity
Our algorithm is constant time ( O(1) time ), i.e. independent of the dictionary size (but depending on the average term length and maximum edit distance), because our index is based on a Hash Table which has an average search time complexity of O(1).

Comparison to other approaches
BK-Trees have a search time of O(log dictionary_size), whereas our algorithm is constant time ( O(1) time ), i.e. independent of the dictionary size.
Tries have a comparable search performance to our approach. But a Trie is a prefix tree, which requires a common prefix. This makes it suitable for autocomplete or search suggestions, but not applicable for spell checking. If your typing error is e.g. in the first letter, than you have no common prefix, hence the Trie will not work for spelling correction.

Possible application fields of our algorithm are those of fast approximate dictionary string matching: spell checkers for word processors and search engines, correction systems for optical character recognition, natural language translation based on translation memory, record linkage, de-duplication, matching DNA sequences, fuzzy string searching and fraud detection.


BTW, by using a similar principle our web search is three orders of magnitude more efficient as well. While Google touches 1000 servers for every query, we need to query just one (server/peer).
That’s not because of DHT! Vice versa, because even for a complex query in a web scale index only one of the servers needs to be queried, it enables the use of DHT for web search.
Our algorithm improves the efficiency of central servers in a data center to the same extent.

Near Duplicate Detection

FAROO now has implemented a robust Near Duplicate Detection.
This promotes original content and diversity in results, while filtering out scraped or syndicated duplicates.

We had to solve two challenges:

  1. a robust algorithm, that identifies duplicates even if they appear within a different template with different menu, header or footer.
  2. do it web scale, i.e. every single new web page needs to be compared to the whole corpus of existing web pages

The screenshots below illustrate the effect of Near Duplicate Detection. See the difference between Google and FAROO results:

FAROO – with Near Duplicate Detection

FAROO: with NEAR Duplicate Detection


Google – without Near Duplicate Detection

Google: without Near Duplicate Detection

Topic Streams

Most of you know what RSS feeds are. You can subscribe to a site’s RSS feed and you stay updated whenever that site releases a new post.

But what when you are not so much intrested in a specific site, but rather in a topic – let’s say about “Facebook Timeline”? Then you would have to subscribe to countless technolgy blogs, and to browse their countless blog posts, perhaps several times a day.

With our Topic Stream you can create your custom, topic based RSS stream. It will aggregate all blog posts about a specific topic from all the diffferent sources we are indexing. That’s convenient.
Even better, you can use your preferred RSS client or Google reader to read all of your topic streams in one place, together with some traditional RSS feeds you may still want to read.

Here an the “Facebook Timeline” example:

Topic Stream
RSS Feed
Google Reader

Topic Aggregation

Today we are introducing two new functions: Trending Topic Discovery and Topic Aggregation.

Trending Topic Discovery identifies those news, which are covered most often in the last 24 hours. This gives you a clear picture what news are trending and will be talk of the town. Most discussed news are on top.

Topic Aggregation brings all the news from different sources together, if they belong to the same topic. News within a topic are sorted chronlogically.

Trending Topic Discovery and Topic Aggregation together provide a convenient news digest, samewhat similar to Techmeme, just fully algorithmical, without human editors.

Your stay above the information overflow. Instead of browsing all the countless sources manually and repeatedly to stay updated, being overwhelmed with both repeating topics, and many less-relevant items, we filter, group and sort the information for you.

See it in action: http://www.faroo.com/#q=&s=1&src=news


You asked, and we listened: Today we are introducing our new API.

Use our results in your application, for free.

We are offering four different functions:

  • Web Search,
  • News Search,
  • Trending News
  • Trending Topics

Results are available in three different formats:

  • JSON
  • XML
  • RSS

Try our FREE Web Search API, if your looking for an alternative to
Google Web Search API (depreciated), Yahoo Boss (commercial) or Bing Web Search API (commercial).

Start using it: http://www.faroo.com/hp/api/api.html

You want to use the API in China? We have both Chinese search results and gateway servers located in China. Contact us!

No Limits – FAROO iPad / iPhone App v2.0

After 6 months and some incremental updates, we just finished our next major release, with many improvements and new features you will love.

Well, this is not merely an update, it’s rather a complete rewrite. Based on a new flexible architecture all of the previous limitations are gone.

While some of the better newsreaders for iOS are able to provide a dozen sources per page, we decided to remove all those limitations once and for all, both for iPhone and iPad.

The FAROO App now supports an unlimited number of feeds. The animation stays smooth as ever.

But thats not all. Here the complete list of the improvements:

  1. New architecture allows an unlimited number of streams and items per stream.
  2. New Google Reader Synchronization.
  3. New Archive function which allows to store articles for later reading.
  4. New RSS Feed search assistant to search for new feeds by person, topic or blog.
  5. New georgeous animated front page.
  6. New sleek animated transition between stream view and text view.
  7. New central menu for the new functions.
  8. New help function.

As always, this is an Universal App, running both on the iPhone and the iPad. And it is FREE. Get it from the AppStore right now.