Quinstor is an extremely lightweight in-memory, queryable & indexable store.
It is designed to maximise the number of objects you can store and to minimise the amount of time required to query them.
Disclaimer - Quinstor is currently in a very early alpha state. It is currently free for development use only however at this time no support or warranties are offered. You can download the latest version from quinstor-0.0.1.jar. Quinstor uses Javassist as part of its query compilation optimisation which can be downloaded from its website or added using the Maven dependency:
<dependency> <groupId>javassist</groupId> <artifactId>javassist</artifactId> <version>3.12.1.GA</version> </dependency>
Why?
In short, because I couldn't believe the overheads imposed by some of the big names in in-memory storage and decided to see if it was possible to do better. During some work on memory density, I noticed that I was able to store far fewer objects in existing stores than I expected - in some cases by more than a factor of 2. Worse, the capacity reduced by 20% each time I added an index - even one with low cardinality.
Quinstor proves that overhead is unnecessary - its native storage has virtually the same overhead as an ArrayList and it can query millions of objects per-second per-core - either via an OQL-like language or its own DSL. Its index support enables sub-millisecond queries despite the index consuming less than 1% of capacity even for high-cardinality/unique indexes.
The chart above compares Quinstor's storage to a number of other mainstream in-memory data stores. All were constrained to the same 4GB heap. For comparison, the same test was performed with an ArrayList - a dynamically sized container with one of the lowest overheads.
This chart compares the time to query 3 million objects in the same stores. The ArrayList implementation was a simple iterate over the contents with hand-crafted Java to query the object - the fastest we could expect without parallelism or indexes. Again, Quinstor comfortably outperforms the alternatives.
Basic Introduction
Creating the store is as simple as:
import com.andrewelmore.quinstor.Quinstor; import static com.andrewelmore.quinstor.query.dsl.Query.*; Quinstor<Type> quinstor = new Quinstor<>(Type.class, blockSize);
- Type is the type of object you wish to store and
- blockSize is the increments (number of objects) in which Quinstor will grow its internal store as more objects are added
and to add an object to the store:
quinstor.add(object);
Querying
Quinstor has both a DSL and an OQL query interface. Let's assume that our store is populated with objects of type Person:
public class Person { public static class Name { private String first; private String last; public String getFirst() { return first; } public String getLast() { return last; } } private Name name; private int age; public Name getName() { return name; } public int getAge() { return age; } }
int count = quinstor.query(select(count("*")) .where("name.last").is("Elmore"));
int count = quinstor.query( "select count(*) where name.last = 'Elmore'" );
Collection<String> names = quinstor.query( "select name.first where age in (20, 21)" ); for(String name : names) { ... }
quinstor.query("select mean(age) where name.first = 'John'");
// Find all objects with a first name of Matthew //and pass their age and an in-scope object 'aLiteral' to method java.lang.reflect.Method method = ... quinstor.query(select(collect( invoke(method, project("age"), aLiteral) )) .where("name.first").is("Matthew"));
Going Faster
There are a number of ways to speed up your queries. The first is to compile them - this can have substantial gains, especially where projections are concerned. To compile the query, simply add the instruction to compile to the end:
quinstor.query(select(count("*") .where("name.last").is("Elmore") .compile(Person.class));
Finally of course there are indexes. Currently these need to be added before objects are added to Quinstor. 2 types of index are supported:
-
Balanced tree-based indexes. These are well-suited for low cardinality data:
quinstor.addIndex("age");
-
Hash-based indexes. These are well-suited for high-cardinality or unique data:
quinstor.addHashedIndex("name.first");
What's Next?
There are still a number of basic features missing from Quinstor and a lot of more advanced ones. Currently on my list are:
- Documentation!
- API improvements to simplify usage
- AND/OR support in predicates
- Standalone server (with network support)
- Replication
- Registering a listener (with a predicate) to be informed of new objects as they're added
- Further compilation enhancements - the further up the tree we push compilation, the faster it runs