Integrate SASI index into Cassandra

patch by xedin; reviewed by beobal for CASSANDRA-10661
This commit is contained in:
Pavel Yaskevich 2015-12-02 19:23:54 -08:00
parent 11c8ca6b56
commit 72790dc8e3
137 changed files with 40339 additions and 26 deletions

View File

@ -1,4 +1,5 @@
3.4
* Integrate SASI index into Cassandra (CASSANDRA-10661)
* Add --skip-flush option to nodetool snapshot
* Skip values for non-queried columns (CASSANDRA-10657)
* Add support for secondary indexes on static columns (CASSANDRA-8103)

View File

@ -243,6 +243,15 @@
</wikitext-to-html>
</target>
<!--
Generates Java sources for tokenization support from jflex
grammar files
-->
<target name="generate-jflex-java" description="Generate Java from jflex grammar">
<taskdef classname="jflex.anttask.JFlexTask" classpath="${build.lib}/jflex-1.6.0.jar" name="jflex" />
<jflex file="${build.src.java}/org/apache/cassandra/index/sasi/analyzer/StandardTokenizerImpl.jflex" destdir="${build.src.gen-java}/" />
</target>
<!--
Fetch Maven Ant Tasks and Cassandra's dependencies
These targets are intentionally free of dependencies so that they
@ -405,6 +414,11 @@
<exclusion groupId="log4j" artifactId="log4j"/>
</dependency>
<dependency groupId="joda-time" artifactId="joda-time" version="2.4" />
<dependency groupId="com.carrotsearch" artifactId="hppc" version="0.5.4" />
<dependency groupId="de.jflex" artifactId="jflex" version="1.6.0" />
<dependency groupId="net.mintern" artifactId="primitive" version="1.0" />
<dependency groupId="com.github.rholder" artifactId="snowball-stemmer" version="1.3.0.581.1" />
<dependency groupId="com.googlecode.concurrent-trees" artifactId="concurrent-trees" version="2.4.0" />
</dependencyManagement>
<developer id="alakshman" name="Avinash Lakshman"/>
@ -568,6 +582,12 @@
<dependency groupId="org.slf4j" artifactId="log4j-over-slf4j"/>
<dependency groupId="org.slf4j" artifactId="jcl-over-slf4j"/>
<dependency groupId="org.apache.thrift" artifactId="libthrift"/>
<dependency groupId="com.carrotsearch" artifactId="hppc" version="0.5.4" />
<dependency groupId="de.jflex" artifactId="jflex" version="1.6.0" />
<dependency groupId="net.mintern" artifactId="primitive" version="1.0" />
<dependency groupId="com.github.rholder" artifactId="snowball-stemmer" version="1.3.0.581.1" />
<dependency groupId="com.googlecode.concurrent-trees" artifactId="concurrent-trees" version="2.4.0" />
</artifact:pom>
<artifact:pom id="clientutil-pom"
artifactId="cassandra-clientutil"
@ -734,7 +754,7 @@
depends="maven-ant-tasks-retrieve-build,build-project" description="Compile Cassandra classes"/>
<target name="codecoverage" depends="jacoco-run,jacoco-report" description="Create code coverage report"/>
<target depends="init,gen-cql3-grammar,generate-cql-html"
<target depends="init,gen-cql3-grammar,generate-cql-html,generate-jflex-java"
name="build-project">
<echo message="${ant.project.name}: ${ant.file}"/>
<!-- Order matters! -->

768
doc/SASI.md Normal file
View File

@ -0,0 +1,768 @@
# SASIIndex
[`SASIIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/SASIIndex.java),
or "SASI" for short, is an implementation of Cassandra's
`Index` interface that can be used as an alternative to the
existing implementations. SASI's indexing and querying improves on
existing implementations by tailoring it specifically to Cassandra's
needs. SASI has superior performance in cases where queries would
previously require filtering. In achieving this performance, SASI aims
to be significantly less resource intensive than existing
implementations, in memory, disk, and CPU usage. In addition, SASI
supports prefix and contains queries on strings (similar to SQL's
`LIKE = "foo*"` or `LIKE = "*foo*"'`).
The following goes on describe how to get up and running with SASI,
demonstrates usage with examples, and provides some details on its
implementation.
## Using SASI
The examples below walk through creating a table and indexes on its
columns, and performing queries on some inserted data. The patchset in
this repository includes support for the Thrift and CQL3 interfaces.
The examples below assume the `demo` keyspace has been created and is
in use.
```
cqlsh> CREATE KEYSPACE demo WITH replication = {
... 'class': 'SimpleStrategy',
... 'replication_factor': '1'
... };
cqlsh> USE demo;
```
All examples are performed on the `sasi` table:
```
cqlsh:demo> CREATE TABLE sasi (id uuid, first_name text, last_name text,
... age int, height int, created_at bigint, primary key (id));
```
#### Creating Indexes
To create SASI indexes use CQLs `CREATE CUSTOM INDEX` statement:
```
cqlsh:demo> CREATE CUSTOM INDEX ON sasi (first_name) USING 'org.apache.cassandra.index.sasi.SASIIndex'
... WITH OPTIONS = {
... 'analyzer_class':
... 'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer',
... 'case_sensitive': 'false'
... };
cqlsh:demo> CREATE CUSTOM INDEX ON sasi (last_name) USING 'org.apache.cassandra.index.sasi.SASIIndex'
... WITH OPTIONS = {'mode': 'CONTAINS'};
cqlsh:demo> CREATE CUSTOM INDEX ON sasi (age) USING 'org.apache.cassandra.index.sasi.SASIIndex';
cqlsh:demo> CREATE CUSTOM INDEX ON sasi (created_at) USING 'org.apache.cassandra.index.sasi.SASIIndex'
... WITH OPTIONS = {'mode': 'SPARSE'};
```
The indexes created have some options specified that customize their
behaviour and potentially performance. The index on `first_name` is
case-insensitive. The analyzers are discussed more in a subsequent
example. The `NonTokenizingAnalyzer` performs no analysis on the
text. Each index has a mode: `PREFIX`, `CONTAINS`, or `SPARSE`, the
first being the default. The `last_name` index is created with the
mode `CONTAINS` which matches terms on suffixes instead of prefix
only. Examples of this are available below and more detail can be
found in the section on
[OnDiskIndex](https://github.com/xedin/sasi#ondiskindexbuilder).The
`created_at` column is created with its mode set to `SPARSE`, which is
meant to improve performance of querying large, dense number ranges
like timestamps for data inserted every millisecond. Details of the
`SPARSE` implementation can also be found in the section on the
[OnDiskIndex](https://github.com/xedin/sasi#ondiskindexbuilder). The `age`
index is created with the default `PREFIX` mode and no
case-sensitivity or text analysis options are specified since the
field is numeric.
After inserting the following data and performing a `nodetool flush`,
SASI performing index flushes to disk can be seen in Cassandra's logs
-- although the direct call to flush is not required (see
[IndexMemtable](#indexmemtable) for more details).
```
cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
... VALUES (556ebd54-cbe5-4b75-9aae-bf2a31a24500, 'Pavel', 'Yaskevich', 27, 181, 1442959315018);
cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
... VALUES (5770382a-c56f-4f3f-b755-450e24d55217, 'Jordan', 'West', 26, 173, 1442959315019);
cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
... VALUES (96053844-45c3-4f15-b1b7-b02c441d3ee1, 'Mikhail', 'Stepura', 36, 173, 1442959315020);
cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
... VALUES (f5dfcabe-de96-4148-9b80-a1c41ed276b4, 'Michael', 'Kjellman', 26, 180, 1442959315021);
cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
... VALUES (2970da43-e070-41a8-8bcb-35df7a0e608a, 'Johnny', 'Zhang', 32, 175, 1442959315022);
cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
... VALUES (6b757016-631d-4fdb-ac62-40b127ccfbc7, 'Jason', 'Brown', 40, 182, 1442959315023);
cqlsh:demo> INSERT INTO sasi (id, first_name, last_name, age, height, created_at)
... VALUES (8f909e8a-008e-49dd-8d43-1b0df348ed44, 'Vijay', 'Parthasarathy', 34, 183, 1442959315024);
cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi;
first_name | last_name | age | height | created_at
------------+---------------+-----+--------+---------------
Michael | Kjellman | 26 | 180 | 1442959315021
Mikhail | Stepura | 36 | 173 | 1442959315020
Jason | Brown | 40 | 182 | 1442959315023
Pavel | Yaskevich | 27 | 181 | 1442959315018
Vijay | Parthasarathy | 34 | 183 | 1442959315024
Jordan | West | 26 | 173 | 1442959315019
Johnny | Zhang | 32 | 175 | 1442959315022
(7 rows)
```
#### Equality & Prefix Queries
SASI supports simple queries already supported by CQL, however, for
text fields like `first_name` equals queries perform prefix searches
-- this is similar to `first_name LIKE 'M*'` in SQL (excluding case
sensitivity, which is dependent on the index configuration). The
semantics of CQL's `=` were modified instead of making further
modifications of the grammar with the introduction of a `LIKE`
operator. Ideally, CQL would be modified to include such an operator,
supporting both prefix and suffix searches.
```
cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
... WHERE first_name = 'M';
first_name | last_name | age | height | created_at
------------+-----------+-----+--------+---------------
Michael | Kjellman | 26 | 180 | 1442959315021
Mikhail | Stepura | 36 | 173 | 1442959315020
(2 rows)
```
Of course, the case of the query does not matter for the `first_name`
column because of the options provided at index creation time.
```
cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
... WHERE first_name = 'm';
first_name | last_name | age | height | created_at
------------+-----------+-----+--------+---------------
Michael | Kjellman | 26 | 180 | 1442959315021
Mikhail | Stepura | 36 | 173 | 1442959315020
(2 rows)
```
#### Compound Queries
SASI supports queries with multiple predicates, however, due to the
nature of the default indexing implementation, CQL requires the user
to specify `ALLOW FILTERING` to opt-in to the potential performance
pitfalls of such a query. With SASI, while the requirement to include
`ALLOW FILTERING` remains, to reduce modifications to the grammar, the
performance pitfalls do not exist because filtering is not
performed. Details on how SASI joins data from multiple predicates is
available below in the
[Implementation Details](https://github.com/xedin/sasi#implementation-details)
section.
```
cqlsh:demo> SELECT first_name, last_name, age, height, created_at FROM sasi
... WHERE first_name = 'M' and age < 30 ALLOW FILTERING;
first_name | last_name | age | height | created_at
------------+-----------+-----+--------+---------------
Michael | Kjellman | 26 | 180 | 1442959315021
(1 rows)
```
#### Suffix Queries
The next example demonstrates `CONTAINS` mode on the `last_name`
column. By using this mode predicates can search for any strings
containing the search string as a sub-string. In this case the strings
containing "a" or "an".
```
cqlsh:demo> SELECT * FROM sasi WHERE last_name = 'a';
id | age | created_at | first_name | height | last_name
--------------------------------------+-----+---------------+------------+--------+---------------
f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman
96053844-45c3-4f15-b1b7-b02c441d3ee1 | 36 | 1442959315020 | Mikhail | 173 | Stepura
556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | 1442959315018 | Pavel | 181 | Yaskevich
8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | 1442959315024 | Vijay | 183 | Parthasarathy
2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang
(5 rows)
cqlsh:demo> SELECT * FROM sasi WHERE last_name = 'an';
id | age | created_at | first_name | height | last_name
--------------------------------------+-----+---------------+------------+--------+-----------
f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman
2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang
(2 rows)
```
#### Expressions on Non-Indexed Columns
SASI also supports filtering on non-indexed columns like `height`. The
expression can only narrow down an existing query using `AND`.
```
cqlsh:demo> SELECT * FROM sasi WHERE last_name = 'a' AND height >= 175 ALLOW FILTERING;
id | age | created_at | first_name | height | last_name
--------------------------------------+-----+---------------+------------+--------+---------------
f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | 1442959315021 | Michael | 180 | Kjellman
556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | 1442959315018 | Pavel | 181 | Yaskevich
8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | 1442959315024 | Vijay | 183 | Parthasarathy
2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | 1442959315022 | Johnny | 175 | Zhang
(4 rows)
```
#### Text Analysis (Tokenization and Stemming)
Lastly, to demonstrate text analysis an additional column is needed on
the table. Its definition, index, and statements to update rows are shown below.
```
cqlsh:demo> ALTER TABLE sasi ADD bio text;
cqlsh:demo> CREATE CUSTOM INDEX ON sasi (bio) USING 'org.apache.cassandra.index.sasi.SASIIndex'
... WITH OPTIONS = {
... 'analyzer_class': 'org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer',
... 'tokenization_enable_stemming': 'true',
... 'analyzed': 'true',
... 'tokenization_normalize_lowercase': 'true',
... 'tokenization_locale': 'en'
... };
cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, who likes distributed systems, doesnt like to argue.' WHERE id = 5770382a-c56f-4f3f-b755-450e24d55217;
cqlsh:demo> UPDATE sasi SET bio = 'Software Engineer, works on the freight distribution at nights and likes arguing' WHERE id = 556ebd54-cbe5-4b75-9aae-bf2a31a24500;
cqlsh:demo> SELECT * FROM sasi;
id | age | bio | created_at | first_name | height | last_name
--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+---------------
f5dfcabe-de96-4148-9b80-a1c41ed276b4 | 26 | null | 1442959315021 | Michael | 180 | Kjellman
96053844-45c3-4f15-b1b7-b02c441d3ee1 | 36 | null | 1442959315020 | Mikhail | 173 | Stepura
6b757016-631d-4fdb-ac62-40b127ccfbc7 | 40 | null | 1442959315023 | Jason | 182 | Brown
556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich
8f909e8a-008e-49dd-8d43-1b0df348ed44 | 34 | null | 1442959315024 | Vijay | 183 | Parthasarathy
5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West
2970da43-e070-41a8-8bcb-35df7a0e608a | 32 | null | 1442959315022 | Johnny | 175 | Zhang
(7 rows)
```
Index terms and query search strings are stemmed for the `bio` column
because it was configured to use the
[`StandardAnalyzer`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java)
and `analyzed` is set to `true`. The
`tokenization_normalize_lowercase` is similar to the `case_sensitive`
property but for the
[`StandardAnalyzer`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java). These
query demonstrates the stemming applied by [`StandardAnalyzer`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java).
```
cqlsh:demo> SELECT * FROM sasi WHERE bio = 'distributing';
id | age | bio | created_at | first_name | height | last_name
--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich
5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West
(2 rows)
cqlsh:demo> SELECT * FROM sasi WHERE bio = 'they argued';
id | age | bio | created_at | first_name | height | last_name
--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich
5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West
(2 rows)
cqlsh:demo> SELECT * FROM sasi WHERE bio = 'working at the company';
id | age | bio | created_at | first_name | height | last_name
--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich
(1 rows)
cqlsh:demo> SELECT * FROM sasi WHERE bio = 'soft eng';
id | age | bio | created_at | first_name | height | last_name
--------------------------------------+-----+----------------------------------------------------------------------------------+---------------+------------+--------+-----------
556ebd54-cbe5-4b75-9aae-bf2a31a24500 | 27 | Software Engineer, works on the freight distribution at nights and likes arguing | 1442959315018 | Pavel | 181 | Yaskevich
5770382a-c56f-4f3f-b755-450e24d55217 | 26 | Software Engineer, who likes distributed systems, doesnt like to argue. | 1442959315019 | Jordan | 173 | West
(2 rows)
```
## Implementation Details
While SASI, at the surface, is simply an implementation of the
`Index` interface, at its core there are several data
structures and algorithms used to satisfy it. These are described
here. Additionally, the changes internal to Cassandra to support SASIs
integration are described.
The `Index` interface divides responsibility of the
implementer into two parts: Indexing and Querying. Further, Cassandra
makes it possible to divide those responsibilities into the memory and
disk components. SASI takes advantage of Cassandra's write-once,
immutable, ordered data model to build indexes along with the flushing
of the memtable to disk -- this is the origin of the name "SSTable
Attached Secondary Index".
The SASI index data structures are built in memory as the SSTable is
being written and they are flushed to disk before the writing of the
SSTable completes. The writing of each index file only requires
sequential writes to disk. In some cases, partial flushes are
performed, and later stitched back together, to reduce memory
usage. These data structures are optimized for this use case.
Taking advantage of Cassandra's ordered data model, at query time,
candidate indexes are narrowed down for searching minimize the amount
of work done. Searching is then performed using an efficient method
that streams data off disk as needed.
### Indexing
Per SSTable, SASI writes an index file for each indexed column. The
data for these files is built in memory using the
[`OnDiskIndexBuilder`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java). Once
flushed to disk, the data is read using the
[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java)
class. These are composed of bytes representing indexed terms,
organized for efficient writing or searching respectively. The keys
and values they hold represent tokens and positions in an SSTable and
these are stored per-indexed term in
[`TokenTreeBuilder`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java)s
for writing, and
[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s
for querying. These index files are memory mapped after being written
to disk, for quicker access. For indexing data in the memtable SASI
uses its
[`IndexMemtable`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java)
class.
#### OnDiskIndex(Builder)
Each
[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java)
is an instance of a modified
[Suffix Array](https://en.wikipedia.org/wiki/Suffix_array) data
structure. The
[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java)
is comprised of page-size blocks of sorted terms and pointers to the
terms' associated data, as well as the data itself, stored also in one
or more page-sized blocks. The
[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java)
is structured as a tree of arrays, where each level describes the
terms in the level below, the final level being the terms
themselves. The `PointerLevel`s and their `PointerBlock`s contain
terms and pointers to other blocks that *end* with those terms. The
`DataLevel`, the final level, and its `DataBlock`s contain terms and
point to the data itself, contained in [`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s.
The terms written to the
[`OnDiskIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndex.java)
vary depending on its "mode": either `PREFIX`, `CONTAINS`, or
`SPARSE`. In the `PREFIX` and `SPARSE` cases terms exact values are
written exactly once per `OnDiskIndex`. For example, a `PREFIX` index
with terms `Jason`, `Jordan`, `Pavel`, all three will be included in
the index. A `CONTAINS` index writes additional terms for each suffix of
each term recursively. Continuing with the example, a `CONTAINS` index
storing the previous terms would also store `ason`, `ordan`, `avel`,
`son`, `rdan`, `vel`, etc. This allows for queries on the suffix of
strings. The `SPARSE` mode differs from `PREFIX` in that for every 64
blocks of terms a
[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)
is built merging all the
[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s
for each term into a single one. This copy of the data is used for
efficient iteration of large ranges of e.g. timestamps. The index
"mode" is configurable per column at index creation time.
#### TokenTree(Builder)
The
[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)
is an implementation of the well-known
[B+-tree](https://en.wikipedia.org/wiki/B%2B_tree) that has been
modified to optimize for its use-case. In particular, it has been
optimized to associate tokens, longs, with a set of positions in an
SSTable, also longs. Allowing the set of long values accommodates
the possibility of a hash collision in the token, but the data
structure is optimized for the unlikely possibility of such a
collision.
To optimize for its write-once environment the
[`TokenTreeBuilder`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTreeBuilder.java)
completely loads its interior nodes as the tree is built and it uses
the well-known algorithm optimized for bulk-loading the data
structure.
[`TokenTree`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/TokenTree.java)s provide the means to iterate a tokens, and file
positions, that match a given term, and to skip forward in that
iteration, an operation used heavily at query time.
#### IndexMemtable
The
[`IndexMemtable`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java)
handles indexing the in-memory data held in the memtable. The
[`IndexMemtable`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/IndexMemtable.java)
in turn manages either a
[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java)
or a
[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java)
per-column. The choice of which index type is used is data
dependent. The
[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java)
is used for literal types. `AsciiType` and `UTF8Type` are literal
types by defualt but any column can be configured as a literal type
using the `is_literal` option at index creation time. For non-literal
types the
[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java)
is used. The
[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java)
is an implementation that can efficiently support prefix queries on
character-like data. The
[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java),
conversely, is better suited for Cassandra other data types like
numbers.
The
[`TrieMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java)
is built using either the `ConcurrentRadixTree` or
`ConcurrentSuffixTree` from the `com.goooglecode.concurrenttrees`
package. The choice between the two is made based on the indexing
mode, `PREFIX` or other modes, and `CONTAINS` mode, respectively.
The
[`SkipListMemIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/memory/SkipListMemIndex.java)
is built on top of `java.util.concurrent.ConcurrentSkipListSet`.
### Querying
Responsible for converting the internal `IndexExpression`
representation into SASI's
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)
and
[`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)
tree, optimizing the tree to reduce the amount of work done, and
driving the query itself the
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
is the work horse of SASI's querying implementation. To efficiently
perform union and intersection operations SASI provides several
iterators similar to Cassandra's `MergeIterator` but tailored
specifically for SASIs use, and with more features. The
[`RangeUnionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java),
like its name suggests, performs set union over sets of tokens/keys
matching the query, only reading as much data as it needs from each
set to satisfy the query. The
[`RangeIntersectionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java),
similar to its counterpart, performs set intersection over its data.
#### QueryPlan
The
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
instantiated per search query is at the core of SASIs querying
implementation. Its work can be divided in two stages: analysis and
execution.
During the analysis phase,
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
converts from Cassandra's internal representation of
`IndexExpression`s, which has also been modified to support encoding
queries that contain ORs and groupings of expressions using
parentheses (see the
[Cassandra Internal Changes](https://github.com/xedin/sasi#cassandra-internal-changes)
section below for more details). This process produces a tree of
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s, which in turn may contain [`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s, all of which
provide an alternative, more efficient, representation of the query.
During execution the
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
uses the `DecoratedKey`-generating iterator created from the
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) tree. These keys are read from disk and a final check to
ensure they satisfy the query is made, once again using the
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) tree. At the point the desired amount of matching data has
been found, or there is no more matching data, the result set is
returned to the coordinator through the existing internal components.
The number of queries (total/failed/timed-out), and their latencies,
are maintined per-table/column family.
SASI also supports concurrently iterating terms for the same index
accross SSTables. The concurrency factor is controlled by the
`cassandra.search_concurrency_factor` system property. The default is
`1`.
##### QueryController
Each
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
references a
[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java)
used throughout the execution phase. The
[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java)
has two responsibilities: to manage and ensure the proper cleanup of
resources (indexes), and to strictly enforce the time bound for query,
specified by the user via the range slice timeout. All indexes are
accessed via the
[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java)
so that they can be safely released by it later. The
[`QueryController`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryController.java)'s
`checkpoint` function is called in specific places in the execution
path to ensure the time-bound is enforced.
##### QueryPlan Optimizations
While in the analysis phase, the
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
performs several potential optimizations to the query. The goal of
these optimizations is to reduce the amount of work performed during
the execution phase.
The simplest optimization performed is compacting multiple expressions
joined by logical intersection (`AND`) into a single [`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java) with
three or more [`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s. For example, the query `WHERE age < 100 AND
fname = 'p*' AND first_name != 'pa*' AND age > 21` would,
without modification, have the following tree:
┌───────┐
┌────────│ AND │──────┐
│ └───────┘ │
▼ ▼
┌───────┐ ┌──────────┐
┌─────│ AND │─────┐ │age < 100
│ └───────┘ │ └──────────┘
▼ ▼
┌──────────┐ ┌───────┐
│ fname=p* │ ┌─│ AND │───┐
└──────────┘ │ └───────┘ │
▼ ▼
┌──────────┐ ┌──────────┐
│fname!=pa*│ │ age > 21 │
└──────────┘ └──────────┘
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
will remove the redundant right branch whose root is the final `AND`
and has leaves `fname != pa*` and `age > 21`. These [`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s will
be compacted into the parent `AND`, a safe operation due to `AND`
being associative and commutative. The resulting tree looks like the
following:
┌───────┐
┌────────│ AND │──────┐
│ └───────┘ │
▼ ▼
┌───────┐ ┌──────────┐
┌───────────│ AND │────────┐ │age < 100
│ └───────┘ │ └──────────┘
▼ │ ▼
┌──────────┐ │ ┌──────────┐
│ fname=p* │ ▼ │ age > 21 │
└──────────┘ ┌──────────┐ └──────────┘
│fname!=pa*│
└──────────┘
When excluding results from the result set, using `!=`, the
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
determines the best method for handling it. For range queries, for
example, it may be optimal to divide the range into multiple parts
with a hole for the exclusion. For string queries, such as this one,
it is more optimal, however, to simply note which data to skip, or
exclude, while scanning the index. Following this optimization the
tree looks like this:
┌───────┐
┌────────│ AND │──────┐
│ └───────┘ │
▼ ▼
┌───────┐ ┌──────────┐
┌───────│ AND │────────┐ │age < 100
│ └───────┘ │ └──────────┘
▼ ▼
┌──────────────────┐ ┌──────────┐
│ fname=p* │ │ age > 21 │
│ exclusions=[pa*] │ └──────────┘
└──────────────────┘
The last type of optimization applied, for this query, is to merge
range expressions across branches of the tree -- without modifying the
meaning of the query, of course. In this case, because the query
contains all `AND`s the `age` expressions can be collapsed. Along with
this optimization, the initial collapsing of unneeded `AND`s can also
be applied once more to result in this final tree using to execute the
query:
┌───────┐
┌──────│ AND │───────┐
│ └───────┘ │
▼ ▼
┌──────────────────┐ ┌────────────────┐
│ fname=p* │ │ 21 < age < 100
│ exclusions=[pa*] │ └────────────────┘
└──────────────────┘
#### Operations and Expressions
As discussed, the
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
optimizes a tree represented by
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s
as interior nodes, and
[`Expression`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Expression.java)s
as leaves. The
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)
class, more specifically, can have zero, one, or two
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s
as children and an unlimited number of expressions. The iterators used
to perform the queries, discussed below in the
"Range(Union|Intersection)Iterator" section, implement the necessary
logic to merge results transparently regardless of the
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)s
children.
Besides participating in the optimizations performed by the
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java),
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)
is also responsible for taking a row that has been returned by the
query and making a final validation that it in fact does match. This
`satisfiesBy` operation is performed recursively from the root of the
[`Operation`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java)
tree for a given query. These checks are performed directly on the
data in a given row. For more details on how `satisfiesBy` works see
the documentation
[in the code](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/Operation.java#L87-L123).
#### Range(Union|Intersection)Iterator
The abstract `RangeIterator` class provides a unified interface over
the two main operations performed by SASI at various layers in the
execution path: set intersection and union. These operations are
performed in a iterated, or "streaming", fashion to prevent unneeded
reads of elements from either set. In both the intersection and union
cases the algorithms take advantage of the data being pre-sorted using
the same sort order, e.g. term or token order.
The
[`RangeUnionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java)
performs the "Merge-Join" portion of the
[Sort-Merge-Join](https://en.wikipedia.org/wiki/Sort-merge_join)
algorithm, with the properties of an outer-join, or union. It is
implemented with several optimizations to improve its performance over
a large number of iterators -- sets to union. Specifically, the
iterator exploits the likely case of the data having many sub-groups
of overlapping ranges and the unlikely case that all ranges will
overlap each other. For more details see the
[javadoc](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java#L9-L21).
The
[`RangeIntersectionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java)
itself is not a subclass of `RangeIterator`. It is a container for
several classes, one of which, `AbstractIntersectionIterator`,
sub-classes `RangeIterator`. SASI supports two methods of performing
the intersection operation, and the ability to be adaptive in choosing
between them based on some properties of the data.
`BounceIntersectionIterator`, and the `BOUNCE` strategy, works like
the
[`RangeUnionIterator`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeUnionIterator.java)
in that it performs a "Merge-Join", however, its nature is similar to
a inner-join, where like values are merged by a data-specific merge
function (e.g. merging two tokens in a list to lookup in a SSTable
later). See the
[javadoc](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L88-L101)
for more details on its implementation.
`LookupIntersectionIterator`, and the `LOOKUP` strategy, performs a
different operation, more similar to a lookup in an associative data
structure, or "hash lookup" in database terminology. Once again,
details on the implementation can be found in the
[javadoc](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/utils/RangeIntersectionIterator.java#L199-L208).
The choice between the two iterators, or the `ADAPTIVE` strategy, is
based upon the ratio of data set sizes of the minimum and maximum
range of the sets being intersected. If the number of the elements in
minimum range divided by the number of elements is the maximum range
is less than or equal to `0.01`, then the `ADAPTIVE` strategy chooses
the `LookupIntersectionIterator`, otherwise the
`BounceIntersectionIterator` is chosen.
### The SASIIndex Class
The above components are glued together by the
[`SASIIndex`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasiIndex.java)
class which implements `Index`, and is instantiated
per-table containing SASI indexes. It manages all indexes for a table
via the
[`sasi.conf.DataTracker`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/DataTracker.java)
and
[`sasi.conf.view.View`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/conf/view/View.java)
components, controls writing of all indexes for an SSTable via its
[`PerSSTableIndexWriter`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/disk/PerSSTableIndexWriter.java), and initiates searches with
`Indexer`. These classes glue the previously
mentioned indexing components together with Cassandra's SSTable
life-cycle ensuring indexes are not only written when Memtable's flush
but also as SSTable's are compacted. For querying, the
`Indexer` does little but defer to
[`QueryPlan`](https://github.com/apache/cassandra/blob/trunk/src/java/org/apache/cassandra/index/sasi/plan/QueryPlan.java)
and update e.g. latency metrics exposed by SASI.
### Cassandra Internal Changes
To support the above changes and integrate them into Cassandra a few
minor internal changes were made to Cassandra itself. These are
described here.
#### SSTable Write Life-cycle Notifications
The `SSTableFlushObserver` is an observer pattern-like interface,
whose sub-classes can register to be notified about events in the
life-cycle of writing out a SSTable. Sub-classes can be notified when a
flush begins and ends, as well as when each next row is about to be
written, and each next column. SASI's `PerSSTableIndexWriter`,
discussed above, is the only current subclass.
### Limitations and Caveats
The following are items that can be addressed in future updates but are not
available in this repository or are not currently implemented.
* The cluster must be configured to use a partitioner that produces
`LongToken`s, e.g. `Murmur3Partitioner`. Other existing partitioners which
don't produce LongToken e.g. `ByteOrderedPartitioner` and `RandomPartitioner`
will not work with SASI.
* `ALLOW FILTERING`, the requirement of at least one indexes `=`
expression, and lack of `LIKE` limit SASIs
feature-set. Modifications to the grammar to allow `Index`
implementations to enumerate its supported features would allow SASI
to expose more features without need to support them in other
implementations.
* Not Equals and OR support have been removed in this release while
changes are made to Cassandra itself to support them.
### Contributors
* [Pavel Yaskevich](https://github.com/xedin)
* [Jordan West](https://github.com/jrwest)
* [Michael Kjellman](https://github.com/mkjellman)
* [Jason Brown](https://github.com/jasobrown)
* [Mikhail Stepura](https://github.com/mishail)

Binary file not shown.

BIN
lib/hppc-0.5.4.jar Normal file

Binary file not shown.

BIN
lib/jflex-1.6.0.jar Normal file

Binary file not shown.

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

202
lib/licenses/hppc-0.5.4.txt Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

BIN
lib/primitive-1.0.jar Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1964,5 +1964,10 @@ public class DatabaseDescriptor
public static void setEncryptionContext(EncryptionContext ec)
{
encryptionContext = ec;
}
}
public static int searchConcurrencyFactor()
{
return Integer.valueOf(System.getProperty("cassandra.search_concurrency_factor", "1"));
}
}

View File

@ -152,9 +152,9 @@ public class ColumnIndex
UnfilteredSerializer.serializer.serialize(unfiltered, header, writer, pos - previousRowStart, version);
// notify observers about each new cell added to the row
if (!observers.isEmpty() && unfiltered.isRow())
((Row) unfiltered).stream().forEach(cell -> observers.forEach((o) -> o.nextCell(cell)));
// notify observers about each new row
if (!observers.isEmpty())
observers.forEach((o) -> o.nextUnfilteredCluster(unfiltered));
lastClustering = unfiltered.clustering();
previousRowStart = pos;

View File

@ -171,6 +171,11 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
return withNewExpressions(newExpressions);
}
public RowFilter withoutExpressions()
{
return withNewExpressions(Collections.emptyList());
}
protected abstract RowFilter withNewExpressions(List<Expression> expressions);
public boolean isEmpty()
@ -312,7 +317,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
// Note: the order of this enum matter, it's used for serialization
protected enum Kind { SIMPLE, MAP_EQUALITY, THRIFT_DYN_EXPR, CUSTOM }
abstract Kind kind();
protected abstract Kind kind();
protected final ColumnDefinition column;
protected final Operator operator;
protected final ByteBuffer value;
@ -689,7 +694,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
}
@Override
Kind kind()
protected Kind kind()
{
return Kind.SIMPLE;
}
@ -782,7 +787,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
}
@Override
Kind kind()
protected Kind kind()
{
return Kind.MAP_EQUALITY;
}
@ -833,7 +838,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
}
@Override
Kind kind()
protected Kind kind()
{
return Kind.THRIFT_DYN_EXPR;
}
@ -883,7 +888,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
.customExpressionValueType());
}
Kind kind()
protected Kind kind()
{
return Kind.CUSTOM;
}

View File

@ -665,6 +665,11 @@ public class SecondaryIndexManager implements IndexRegistry
return selected;
}
public Optional<Index> getBestIndexFor(RowFilter.Expression expression)
{
return indexes.values().stream().filter((i) -> i.supportsExpression(expression.column(), expression.operator())).findFirst();
}
/**
* Called at write time to ensure that values present in the update
* are valid according to the rules of all registered indexes which
@ -1040,6 +1045,12 @@ public class SecondaryIndexManager implements IndexRegistry
private static void executeAllBlocking(Stream<Index> indexers, Function<Index, Callable<?>> function)
{
if (function == null)
{
logger.error("failed to flush indexes: {} because flush task is missing.", indexers);
return;
}
List<Future<?>> waitFor = new ArrayList<>();
indexers.forEach(indexer -> {
Callable<?> task = function.apply(indexer);

View File

@ -0,0 +1,288 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.function.BiFunction;
import com.googlecode.concurrenttrees.common.Iterables;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.Schema;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.partitions.PartitionIterator;
import org.apache.cassandra.db.partitions.PartitionUpdate;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.IndexRegistry;
import org.apache.cassandra.index.SecondaryIndexBuilder;
import org.apache.cassandra.index.internal.CassandraIndex;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.PerSSTableIndexWriter;
import org.apache.cassandra.index.sasi.plan.QueryPlan;
import org.apache.cassandra.index.transactions.IndexTransaction;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.notifications.*;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.concurrent.OpOrder;
public class SASIIndex implements Index, INotificationConsumer
{
private static class SASIIndexBuildingSupport implements IndexBuildingSupport
{
public SecondaryIndexBuilder getIndexBuildTask(ColumnFamilyStore cfs,
Set<Index> indexes,
Collection<SSTableReader> sstablesToRebuild)
{
NavigableMap<SSTableReader, Map<ColumnDefinition, ColumnIndex>> sstables = new TreeMap<>((a, b) -> {
return Integer.compare(a.descriptor.generation, b.descriptor.generation);
});
indexes.stream()
.filter((i) -> i instanceof SASIIndex)
.forEach((i) -> {
SASIIndex sasi = (SASIIndex) i;
sstablesToRebuild.stream()
.filter((sstable) -> !sasi.index.hasSSTable(sstable))
.forEach((sstable) -> {
Map<ColumnDefinition, ColumnIndex> toBuild = sstables.get(sstable);
if (toBuild == null)
sstables.put(sstable, (toBuild = new HashMap<>()));
toBuild.put(sasi.index.getDefinition(), sasi.index);
});
});
return new SASIIndexBuilder(cfs, sstables);
}
}
private static final SASIIndexBuildingSupport INDEX_BUILDER_SUPPORT = new SASIIndexBuildingSupport();
private final ColumnFamilyStore baseCfs;
private final IndexMetadata config;
private final ColumnIndex index;
public SASIIndex(ColumnFamilyStore baseCfs, IndexMetadata config)
{
this.baseCfs = baseCfs;
this.config = config;
ColumnDefinition column = CassandraIndex.parseTarget(baseCfs.metadata, config).left;
this.index = new ColumnIndex(baseCfs.metadata.getKeyValidator(), column, config);
baseCfs.getTracker().subscribe(this);
}
public static Map<String, String> validateOptions(Map<String, String> options)
{
return Collections.emptyMap();
}
public void register(IndexRegistry registry)
{
registry.registerIndex(this);
}
public IndexMetadata getIndexMetadata()
{
return config;
}
public Callable<?> getInitializationTask()
{
return null;
}
public Callable<?> getMetadataReloadTask(IndexMetadata indexMetadata)
{
return null;
}
public Callable<?> getBlockingFlushTask()
{
return null; // SASI indexes are flushed along side memtable
}
public Callable<?> getInvalidateTask()
{
return getTruncateTask(FBUtilities.timestampMicros());
}
public Callable<?> getTruncateTask(long truncatedAt)
{
return () -> {
index.dropData(truncatedAt);
return null;
};
}
public boolean shouldBuildBlocking()
{
return true;
}
public Optional<ColumnFamilyStore> getBackingTable()
{
return Optional.empty();
}
public boolean indexes(PartitionColumns columns)
{
return columns.contains(index.getDefinition());
}
public boolean dependsOn(ColumnDefinition column)
{
return index.getDefinition().compareTo(column) == 0;
}
public boolean supportsExpression(ColumnDefinition column, Operator operator)
{
return dependsOn(column);
}
public AbstractType<?> customExpressionValueType()
{
return null;
}
public RowFilter getPostIndexQueryFilter(RowFilter filter)
{
return filter.withoutExpressions();
}
public long getEstimatedResultRows()
{
// this is temporary (until proper QueryPlan is integrated into Cassandra)
// and allows us to priority SASI indexes if any in the query since they
// are going to be more efficient, to query and intersect, than built-in indexes.
return Long.MIN_VALUE;
}
public void validate(PartitionUpdate update) throws InvalidRequestException
{}
public Indexer indexerFor(DecoratedKey key, PartitionColumns columns, int nowInSec, OpOrder.Group opGroup, IndexTransaction.Type transactionType)
{
return new Indexer()
{
public void begin()
{}
public void partitionDelete(DeletionTime deletionTime)
{}
public void rangeTombstone(RangeTombstone tombstone)
{}
public void insertRow(Row row)
{
if (isNewData())
adjustMemtableSize(index.index(key, row), opGroup);
}
public void updateRow(Row oldRow, Row newRow)
{
insertRow(newRow);
}
public void removeRow(Row row)
{}
public void finish()
{}
// we are only interested in the data from Memtable
// everything else is going to be handled by SSTableWriter observers
private boolean isNewData()
{
return transactionType == IndexTransaction.Type.UPDATE;
}
public void adjustMemtableSize(long additionalSpace, OpOrder.Group opGroup)
{
baseCfs.getTracker().getView().getCurrentMemtable().getAllocator().onHeap().allocate(additionalSpace, opGroup);
}
};
}
public Searcher searcherFor(ReadCommand command) throws InvalidRequestException
{
CFMetaData config = command.metadata();
ColumnFamilyStore cfs = Schema.instance.getColumnFamilyStoreInstance(config.cfId);
return controller -> new QueryPlan(cfs, command, DatabaseDescriptor.getRangeRpcTimeout()).execute(controller);
}
public SSTableFlushObserver getFlushObserver(Descriptor descriptor, OperationType opType)
{
return newWriter(baseCfs.metadata.getKeyValidator(), descriptor, Collections.singletonMap(index.getDefinition(), index), opType);
}
public BiFunction<PartitionIterator, ReadCommand, PartitionIterator> postProcessorFor(ReadCommand command)
{
return (partitionIterator, readCommand) -> partitionIterator;
}
public IndexBuildingSupport getBuildTaskSupport()
{
return INDEX_BUILDER_SUPPORT;
}
public void handleNotification(INotification notification, Object sender)
{
// unfortunately, we can only check the type of notification via instanceof :(
if (notification instanceof SSTableAddedNotification)
{
SSTableAddedNotification notice = (SSTableAddedNotification) notification;
index.update(Collections.<SSTableReader>emptyList(), Iterables.toList(notice.added));
}
else if (notification instanceof SSTableListChangedNotification)
{
SSTableListChangedNotification notice = (SSTableListChangedNotification) notification;
index.update(notice.removed, notice.added);
}
else if (notification instanceof MemtableRenewedNotification)
{
index.switchMemtable();
}
}
public ColumnIndex getIndex()
{
return index;
}
protected static PerSSTableIndexWriter newWriter(AbstractType<?> keyValidator,
Descriptor descriptor,
Map<ColumnDefinition, ColumnIndex> indexes,
OperationType opType)
{
return new PerSSTableIndexWriter(keyValidator, descriptor, opType, indexes);
}
}

View File

@ -0,0 +1,128 @@
package org.apache.cassandra.index.sasi;
import java.io.File;
import java.io.IOException;
import java.util.*;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.db.ColumnFamilyStore;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.RowIndexEntry;
import org.apache.cassandra.db.compaction.CompactionInfo;
import org.apache.cassandra.db.compaction.CompactionInterruptedException;
import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.index.SecondaryIndexBuilder;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.PerSSTableIndexWriter;
import org.apache.cassandra.io.FSReadError;
import org.apache.cassandra.io.sstable.KeyIterator;
import org.apache.cassandra.io.sstable.SSTable;
import org.apache.cassandra.io.sstable.SSTableIdentityIterator;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.util.RandomAccessReader;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.UUIDGen;
class SASIIndexBuilder extends SecondaryIndexBuilder
{
private final ColumnFamilyStore cfs;
private final UUID compactionId = UUIDGen.getTimeUUID();
private final SortedMap<SSTableReader, Map<ColumnDefinition, ColumnIndex>> sstables;
private long bytesProcessed = 0;
private final long totalSizeInBytes;
public SASIIndexBuilder(ColumnFamilyStore cfs, SortedMap<SSTableReader, Map<ColumnDefinition, ColumnIndex>> sstables)
{
long totalIndexBytes = 0;
for (SSTableReader sstable : sstables.keySet())
totalIndexBytes += getPrimaryIndexLength(sstable);
this.cfs = cfs;
this.sstables = sstables;
this.totalSizeInBytes = totalIndexBytes;
}
public void build()
{
AbstractType<?> keyValidator = cfs.metadata.getKeyValidator();
for (Map.Entry<SSTableReader, Map<ColumnDefinition, ColumnIndex>> e : sstables.entrySet())
{
SSTableReader sstable = e.getKey();
Map<ColumnDefinition, ColumnIndex> indexes = e.getValue();
try (RandomAccessReader dataFile = sstable.openDataReader())
{
PerSSTableIndexWriter indexWriter = SASIIndex.newWriter(keyValidator, sstable.descriptor, indexes, OperationType.COMPACTION);
long previousKeyPosition = 0;
try (KeyIterator keys = new KeyIterator(sstable.descriptor, cfs.metadata))
{
while (keys.hasNext())
{
if (isStopRequested())
throw new CompactionInterruptedException(getCompactionInfo());
final DecoratedKey key = keys.next();
final long keyPosition = keys.getKeyPosition();
indexWriter.startPartition(key, keyPosition);
try
{
RowIndexEntry indexEntry = sstable.getPosition(key, SSTableReader.Operator.EQ);
dataFile.seek(indexEntry.position + indexEntry.headerOffset());
ByteBufferUtil.readWithShortLength(dataFile); // key
try (SSTableIdentityIterator partition = new SSTableIdentityIterator(sstable, dataFile, key))
{
while (partition.hasNext())
indexWriter.nextUnfilteredCluster(partition.next());
}
}
catch (IOException ex)
{
throw new FSReadError(ex, sstable.getFilename());
}
bytesProcessed += keyPosition - previousKeyPosition;
previousKeyPosition = keyPosition;
}
completeSSTable(indexWriter, sstable, indexes.values());
}
}
}
}
public CompactionInfo getCompactionInfo()
{
return new CompactionInfo(cfs.metadata,
OperationType.INDEX_BUILD,
bytesProcessed,
totalSizeInBytes,
compactionId);
}
private long getPrimaryIndexLength(SSTable sstable)
{
File primaryIndex = new File(sstable.getIndexFilename());
return primaryIndex.exists() ? primaryIndex.length() : 0;
}
private void completeSSTable(PerSSTableIndexWriter indexWriter, SSTableReader sstable, Collection<ColumnIndex> indexes)
{
indexWriter.complete();
for (ColumnIndex index : indexes)
{
File tmpIndex = new File(sstable.descriptor.filenameFor(index.getComponent()));
if (!tmpIndex.exists()) // no data was inserted into the index for given sstable
continue;
index.update(Collections.<SSTableReader>emptyList(), Collections.singletonList(sstable));
}
}
}

View File

@ -0,0 +1,187 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.io.FSReadError;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.utils.concurrent.Ref;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import com.google.common.base.Function;
public class SSTableIndex
{
private final ColumnIndex columnIndex;
private final Ref<SSTableReader> sstableRef;
private final SSTableReader sstable;
private final OnDiskIndex index;
private final AtomicInteger references = new AtomicInteger(1);
private final AtomicBoolean obsolete = new AtomicBoolean(false);
public SSTableIndex(ColumnIndex index, File indexFile, SSTableReader referent)
{
this.columnIndex = index;
this.sstableRef = referent.tryRef();
this.sstable = sstableRef.get();
if (sstable == null)
throw new IllegalStateException("Couldn't acquire reference to the sstable: " + referent);
AbstractType<?> validator = columnIndex.getValidator();
assert validator != null;
assert indexFile.exists() : String.format("SSTable %s should have index %s.",
sstable.getFilename(),
columnIndex.getIndexName());
this.index = new OnDiskIndex(indexFile, validator, new DecoratedKeyFetcher(sstable));
}
public ByteBuffer minTerm()
{
return index.minTerm();
}
public ByteBuffer maxTerm()
{
return index.maxTerm();
}
public ByteBuffer minKey()
{
return index.minKey();
}
public ByteBuffer maxKey()
{
return index.maxKey();
}
public RangeIterator<Long, Token> search(Expression expression)
{
return index.search(expression);
}
public SSTableReader getSSTable()
{
return sstable;
}
public String getPath()
{
return index.getIndexPath();
}
public boolean reference()
{
while (true)
{
int n = references.get();
if (n <= 0)
return false;
if (references.compareAndSet(n, n + 1))
return true;
}
}
public void release()
{
int n = references.decrementAndGet();
if (n == 0)
{
FileUtils.closeQuietly(index);
sstableRef.release();
if (obsolete.get() || sstableRef.globalCount() == 0)
FileUtils.delete(index.getIndexPath());
}
}
public void markObsolete()
{
obsolete.getAndSet(true);
release();
}
public boolean isObsolete()
{
return obsolete.get();
}
public boolean equals(Object o)
{
return o instanceof SSTableIndex && index.getIndexPath().equals(((SSTableIndex) o).index.getIndexPath());
}
public int hashCode()
{
return new HashCodeBuilder().append(index.getIndexPath()).build();
}
public String toString()
{
return String.format("SSTableIndex(column: %s, SSTable: %s)", columnIndex.getColumnName(), sstable.descriptor);
}
private static class DecoratedKeyFetcher implements Function<Long, DecoratedKey>
{
private final SSTableReader sstable;
DecoratedKeyFetcher(SSTableReader reader)
{
sstable = reader;
}
public DecoratedKey apply(Long offset)
{
try
{
return sstable.keyAt(offset);
}
catch (IOException e)
{
throw new FSReadError(new IOException("Failed to read key from " + sstable.descriptor, e), sstable.getFilename());
}
}
public int hashCode()
{
return sstable.descriptor.hashCode();
}
public boolean equals(Object other)
{
return other instanceof DecoratedKeyFetcher
&& sstable.descriptor.equals(((DecoratedKeyFetcher) other).sstable.descriptor);
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi;
import java.nio.ByteBuffer;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.TermSize;
import org.apache.cassandra.index.sasi.utils.MappedBuffer;
import org.apache.cassandra.db.marshal.AbstractType;
public class Term
{
protected final MappedBuffer content;
protected final TermSize termSize;
public Term(MappedBuffer content, TermSize size)
{
this.content = content;
this.termSize = size;
}
public ByteBuffer getTerm()
{
long offset = termSize.isConstant() ? content.position() : content.position() + 2;
int length = termSize.isConstant() ? termSize.size : content.getShort(content.position());
return content.getPageRegion(offset, length);
}
public long getDataOffset()
{
long position = content.position();
return position + (termSize.isConstant() ? termSize.size : 2 + content.getShort(position));
}
public int compareTo(AbstractType<?> comparator, ByteBuffer query)
{
return compareTo(comparator, query, true);
}
public int compareTo(AbstractType<?> comparator, ByteBuffer query, boolean checkFully)
{
long position = content.position();
int padding = termSize.isConstant() ? 0 : 2;
int len = termSize.isConstant() ? termSize.size : content.getShort(position);
return content.comparePageTo(position + padding, checkFully ? len : Math.min(len, query.remaining()), comparator, query);
}
}

View File

@ -0,0 +1,208 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.io.util.FileUtils;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Uninterruptibles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TermIterator extends RangeIterator<Long, Token>
{
private static final Logger logger = LoggerFactory.getLogger(TermIterator.class);
private static final ThreadLocal<ExecutorService> SEARCH_EXECUTOR = new ThreadLocal<ExecutorService>()
{
public ExecutorService initialValue()
{
final String currentThread = Thread.currentThread().getName();
final int concurrencyFactor = DatabaseDescriptor.searchConcurrencyFactor();
logger.info("Search Concurrency Factor is set to {} for {}", concurrencyFactor, currentThread);
return (concurrencyFactor <= 1)
? MoreExecutors.newDirectExecutorService()
: Executors.newFixedThreadPool(concurrencyFactor, new ThreadFactory()
{
public final AtomicInteger count = new AtomicInteger();
public Thread newThread(Runnable task)
{
return new Thread(task, currentThread + "-SEARCH-" + count.incrementAndGet()) {{ setDaemon(true); }};
}
});
}
};
private final Expression expression;
private final RangeIterator<Long, Token> union;
private final Set<SSTableIndex> referencedIndexes;
private TermIterator(Expression e,
RangeIterator<Long, Token> union,
Set<SSTableIndex> referencedIndexes)
{
super(union.getMinimum(), union.getMaximum(), union.getCount());
this.expression = e;
this.union = union;
this.referencedIndexes = referencedIndexes;
}
public static TermIterator build(final Expression e, Set<SSTableIndex> perSSTableIndexes)
{
final List<RangeIterator<Long, Token>> tokens = new CopyOnWriteArrayList<>();
final AtomicLong tokenCount = new AtomicLong(0);
RangeIterator<Long, Token> memtableIterator = e.index.searchMemtable(e);
if (memtableIterator != null)
{
tokens.add(memtableIterator);
tokenCount.addAndGet(memtableIterator.getCount());
}
final Set<SSTableIndex> referencedIndexes = new CopyOnWriteArraySet<>();
try
{
final CountDownLatch latch = new CountDownLatch(perSSTableIndexes.size());
final ExecutorService searchExecutor = SEARCH_EXECUTOR.get();
for (final SSTableIndex index : perSSTableIndexes)
{
if (!index.reference())
{
latch.countDown();
continue;
}
// add to referenced right after the reference was acquired,
// that helps to release index if something goes bad inside of the search
referencedIndexes.add(index);
searchExecutor.submit((Runnable) () -> {
try
{
e.checkpoint();
RangeIterator<Long, Token> keyIterator = index.search(e);
if (keyIterator == null)
{
releaseIndex(referencedIndexes, index);
return;
}
tokens.add(keyIterator);
tokenCount.getAndAdd(keyIterator.getCount());
}
catch (Throwable e1)
{
releaseIndex(referencedIndexes, index);
if (logger.isDebugEnabled())
logger.debug(String.format("Failed search an index %s, skipping.", index.getPath()), e1);
}
finally
{
latch.countDown();
}
});
}
Uninterruptibles.awaitUninterruptibly(latch);
// checkpoint right away after all indexes complete search because we might have crossed the quota
e.checkpoint();
RangeIterator<Long, Token> ranges = RangeUnionIterator.build(tokens);
return ranges == null ? null : new TermIterator(e, ranges, referencedIndexes);
}
catch (Throwable ex)
{
// if execution quota was exceeded while opening indexes or something else happened
// local (yet to be tracked) indexes should be released first before re-throwing exception
referencedIndexes.forEach(TermIterator::releaseQuietly);
throw ex;
}
}
protected Token computeNext()
{
try
{
return union.hasNext() ? union.next() : endOfData();
}
finally
{
expression.checkpoint();
}
}
protected void performSkipTo(Long nextToken)
{
try
{
union.skipTo(nextToken);
}
finally
{
expression.checkpoint();
}
}
public void close()
{
FileUtils.closeQuietly(union);
referencedIndexes.forEach(TermIterator::releaseQuietly);
referencedIndexes.clear();
}
private static void releaseIndex(Set<SSTableIndex> indexes, SSTableIndex index)
{
indexes.remove(index);
releaseQuietly(index);
}
private static void releaseQuietly(SSTableIndex index)
{
try
{
index.release();
}
catch (Throwable e)
{
logger.error(String.format("Failed to release index %s", index.getPath()), e);
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer;
import java.nio.ByteBuffer;
import java.text.Normalizer;
import java.util.Iterator;
import java.util.Map;
import org.apache.cassandra.db.marshal.AbstractType;
public abstract class AbstractAnalyzer implements Iterator<ByteBuffer>
{
protected ByteBuffer next = null;
public ByteBuffer next()
{
return next;
}
public void remove()
{
throw new UnsupportedOperationException();
}
public abstract void init(Map<String, String> options, AbstractType validator);
public abstract void reset(ByteBuffer input);
public static String normalize(String original)
{
return Normalizer.isNormalized(original, Normalizer.Form.NFC)
? original
: Normalizer.normalize(original, Normalizer.Form.NFC);
}
}

View File

@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer;
import java.nio.ByteBuffer;
import java.util.Map;
import org.apache.cassandra.db.marshal.AbstractType;
/**
* Default noOp tokenizer. The iterator will iterate only once
* returning the unmodified input
*/
public class NoOpAnalyzer extends AbstractAnalyzer
{
private ByteBuffer input;
private boolean hasNext = false;
public void init(Map<String, String> options, AbstractType validator)
{}
public boolean hasNext()
{
if (hasNext)
{
this.next = input;
this.hasNext = false;
return true;
}
return false;
}
public void reset(ByteBuffer input)
{
this.next = null;
this.input = input;
this.hasNext = true;
}
}

View File

@ -0,0 +1,126 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.cassandra.index.sasi.analyzer.filter.BasicResultFilters;
import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineBuilder;
import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineExecutor;
import org.apache.cassandra.index.sasi.analyzer.filter.FilterPipelineTask;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.AsciiType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.serializers.MarshalException;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Analyzer that does *not* tokenize the input. Optionally will
* apply filters for the input output as defined in analyzers options
*/
public class NonTokenizingAnalyzer extends AbstractAnalyzer
{
private static final Logger logger = LoggerFactory.getLogger(NonTokenizingAnalyzer.class);
private static final Set<AbstractType<?>> VALID_ANALYZABLE_TYPES = new HashSet<AbstractType<?>>()
{{
add(UTF8Type.instance);
add(AsciiType.instance);
}};
private AbstractType validator;
private NonTokenizingOptions options;
private FilterPipelineTask filterPipeline;
private ByteBuffer input;
private boolean hasNext = false;
public void init(Map<String, String> options, AbstractType validator)
{
init(NonTokenizingOptions.buildFromMap(options), validator);
}
public void init(NonTokenizingOptions tokenizerOptions, AbstractType validator)
{
this.validator = validator;
this.options = tokenizerOptions;
this.filterPipeline = getFilterPipeline();
}
public boolean hasNext()
{
// check that we know how to handle the input, otherwise bail
if (!VALID_ANALYZABLE_TYPES.contains(validator))
return false;
if (hasNext)
{
String inputStr;
try
{
inputStr = validator.getString(input);
if (inputStr == null)
throw new MarshalException(String.format("'null' deserialized value for %s with %s", ByteBufferUtil.bytesToHex(input), validator));
Object pipelineRes = FilterPipelineExecutor.execute(filterPipeline, inputStr);
if (pipelineRes == null)
return false;
next = validator.fromString(normalize((String) pipelineRes));
return true;
}
catch (MarshalException e)
{
logger.error("Failed to deserialize value with " + validator, e);
return false;
}
finally
{
hasNext = false;
}
}
return false;
}
public void reset(ByteBuffer input)
{
this.next = null;
this.input = input;
this.hasNext = true;
}
private FilterPipelineTask getFilterPipeline()
{
FilterPipelineBuilder builder = new FilterPipelineBuilder(new BasicResultFilters.NoOperation());
if (options.isCaseSensitive() && options.shouldLowerCaseOutput())
builder = builder.add("to_lower", new BasicResultFilters.LowerCase());
if (options.isCaseSensitive() && options.shouldUpperCaseOutput())
builder = builder.add("to_upper", new BasicResultFilters.UpperCase());
if (!options.isCaseSensitive())
builder = builder.add("to_lower", new BasicResultFilters.LowerCase());
return builder.build();
}
}

View File

@ -0,0 +1,147 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer;
import java.util.Map;
public class NonTokenizingOptions
{
public static final String NORMALIZE_LOWERCASE = "normalize_lowercase";
public static final String NORMALIZE_UPPERCASE = "normalize_uppercase";
public static final String CASE_SENSITIVE = "case_sensitive";
private boolean caseSensitive;
private boolean upperCaseOutput;
private boolean lowerCaseOutput;
public boolean isCaseSensitive()
{
return caseSensitive;
}
public void setCaseSensitive(boolean caseSensitive)
{
this.caseSensitive = caseSensitive;
}
public boolean shouldUpperCaseOutput()
{
return upperCaseOutput;
}
public void setUpperCaseOutput(boolean upperCaseOutput)
{
this.upperCaseOutput = upperCaseOutput;
}
public boolean shouldLowerCaseOutput()
{
return lowerCaseOutput;
}
public void setLowerCaseOutput(boolean lowerCaseOutput)
{
this.lowerCaseOutput = lowerCaseOutput;
}
public static class OptionsBuilder
{
private boolean caseSensitive = true;
private boolean upperCaseOutput = false;
private boolean lowerCaseOutput = false;
public OptionsBuilder()
{
}
public OptionsBuilder caseSensitive(boolean caseSensitive)
{
this.caseSensitive = caseSensitive;
return this;
}
public OptionsBuilder upperCaseOutput(boolean upperCaseOutput)
{
this.upperCaseOutput = upperCaseOutput;
return this;
}
public OptionsBuilder lowerCaseOutput(boolean lowerCaseOutput)
{
this.lowerCaseOutput = lowerCaseOutput;
return this;
}
public NonTokenizingOptions build()
{
if (lowerCaseOutput && upperCaseOutput)
throw new IllegalArgumentException("Options to normalize terms cannot be " +
"both uppercase and lowercase at the same time");
NonTokenizingOptions options = new NonTokenizingOptions();
options.setCaseSensitive(caseSensitive);
options.setUpperCaseOutput(upperCaseOutput);
options.setLowerCaseOutput(lowerCaseOutput);
return options;
}
}
public static NonTokenizingOptions buildFromMap(Map<String, String> optionsMap)
{
OptionsBuilder optionsBuilder = new OptionsBuilder();
if (optionsMap.containsKey(CASE_SENSITIVE) && (optionsMap.containsKey(NORMALIZE_LOWERCASE)
|| optionsMap.containsKey(NORMALIZE_UPPERCASE)))
throw new IllegalArgumentException("case_sensitive option cannot be specified together " +
"with either normalize_lowercase or normalize_uppercase");
for (Map.Entry<String, String> entry : optionsMap.entrySet())
{
switch (entry.getKey())
{
case NORMALIZE_LOWERCASE:
{
boolean bool = Boolean.parseBoolean(entry.getValue());
optionsBuilder = optionsBuilder.lowerCaseOutput(bool);
break;
}
case NORMALIZE_UPPERCASE:
{
boolean bool = Boolean.parseBoolean(entry.getValue());
optionsBuilder = optionsBuilder.upperCaseOutput(bool);
break;
}
case CASE_SENSITIVE:
{
boolean bool = Boolean.parseBoolean(entry.getValue());
optionsBuilder = optionsBuilder.caseSensitive(bool);
break;
}
}
}
return optionsBuilder.build();
}
public static NonTokenizingOptions getDefaultOptions()
{
return new OptionsBuilder()
.caseSensitive(true).lowerCaseOutput(false)
.upperCaseOutput(false)
.build();
}
}

View File

@ -0,0 +1,143 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Generated using ICU4J 52.1.0.0
// by org.apache.lucene.analysis.icu.GenerateJFlexSupplementaryMacros
ALetterSupp = (
([\ud83b][\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB])
| ([\ud81a][\uDC00-\uDE38])
| ([\ud81b][\uDF00-\uDF44\uDF50\uDF93-\uDF9F])
| ([\ud835][\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB])
| ([\ud80d][\uDC00-\uDC2E])
| ([\ud80c][\uDC00-\uDFFF])
| ([\ud809][\uDC00-\uDC62])
| ([\ud808][\uDC00-\uDF6E])
| ([\ud805][\uDE80-\uDEAA])
| ([\ud804][\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD83-\uDDB2\uDDC1-\uDDC4])
| ([\ud801][\uDC00-\uDC9D])
| ([\ud800][\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1E\uDF30-\uDF4A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5])
| ([\ud803][\uDC00-\uDC48])
| ([\ud802][\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72])
)
FormatSupp = (
([\ud804][\uDCBD])
| ([\ud834][\uDD73-\uDD7A])
| ([\udb40][\uDC01\uDC20-\uDC7F])
)
NumericSupp = (
([\ud805][\uDEC0-\uDEC9])
| ([\ud804][\uDC66-\uDC6F\uDCF0-\uDCF9\uDD36-\uDD3F\uDDD0-\uDDD9])
| ([\ud835][\uDFCE-\uDFFF])
| ([\ud801][\uDCA0-\uDCA9])
)
ExtendSupp = (
([\ud81b][\uDF51-\uDF7E\uDF8F-\uDF92])
| ([\ud805][\uDEAB-\uDEB7])
| ([\ud804][\uDC00-\uDC02\uDC38-\uDC46\uDC80-\uDC82\uDCB0-\uDCBA\uDD00-\uDD02\uDD27-\uDD34\uDD80-\uDD82\uDDB3-\uDDC0])
| ([\ud834][\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44])
| ([\ud800][\uDDFD])
| ([\udb40][\uDD00-\uDDEF])
| ([\ud802][\uDE01-\uDE03\uDE05\uDE06\uDE0C-\uDE0F\uDE38-\uDE3A\uDE3F])
)
KatakanaSupp = (
([\ud82c][\uDC00])
)
MidLetterSupp = (
[]
)
MidNumSupp = (
[]
)
MidNumLetSupp = (
[]
)
ExtendNumLetSupp = (
[]
)
ExtendNumLetSupp = (
[]
)
ComplexContextSupp = (
[]
)
HanSupp = (
([\ud87e][\uDC00-\uDE1D])
| ([\ud86b][\uDC00-\uDFFF])
| ([\ud86a][\uDC00-\uDFFF])
| ([\ud869][\uDC00-\uDED6\uDF00-\uDFFF])
| ([\ud868][\uDC00-\uDFFF])
| ([\ud86e][\uDC00-\uDC1D])
| ([\ud86d][\uDC00-\uDF34\uDF40-\uDFFF])
| ([\ud86c][\uDC00-\uDFFF])
| ([\ud863][\uDC00-\uDFFF])
| ([\ud862][\uDC00-\uDFFF])
| ([\ud861][\uDC00-\uDFFF])
| ([\ud860][\uDC00-\uDFFF])
| ([\ud867][\uDC00-\uDFFF])
| ([\ud866][\uDC00-\uDFFF])
| ([\ud865][\uDC00-\uDFFF])
| ([\ud864][\uDC00-\uDFFF])
| ([\ud858][\uDC00-\uDFFF])
| ([\ud859][\uDC00-\uDFFF])
| ([\ud85a][\uDC00-\uDFFF])
| ([\ud85b][\uDC00-\uDFFF])
| ([\ud85c][\uDC00-\uDFFF])
| ([\ud85d][\uDC00-\uDFFF])
| ([\ud85e][\uDC00-\uDFFF])
| ([\ud85f][\uDC00-\uDFFF])
| ([\ud850][\uDC00-\uDFFF])
| ([\ud851][\uDC00-\uDFFF])
| ([\ud852][\uDC00-\uDFFF])
| ([\ud853][\uDC00-\uDFFF])
| ([\ud854][\uDC00-\uDFFF])
| ([\ud855][\uDC00-\uDFFF])
| ([\ud856][\uDC00-\uDFFF])
| ([\ud857][\uDC00-\uDFFF])
| ([\ud849][\uDC00-\uDFFF])
| ([\ud848][\uDC00-\uDFFF])
| ([\ud84b][\uDC00-\uDFFF])
| ([\ud84a][\uDC00-\uDFFF])
| ([\ud84d][\uDC00-\uDFFF])
| ([\ud84c][\uDC00-\uDFFF])
| ([\ud84f][\uDC00-\uDFFF])
| ([\ud84e][\uDC00-\uDFFF])
| ([\ud841][\uDC00-\uDFFF])
| ([\ud840][\uDC00-\uDFFF])
| ([\ud843][\uDC00-\uDFFF])
| ([\ud842][\uDC00-\uDFFF])
| ([\ud845][\uDC00-\uDFFF])
| ([\ud844][\uDC00-\uDFFF])
| ([\ud847][\uDC00-\uDFFF])
| ([\ud846][\uDC00-\uDFFF])
)
HiraganaSupp = (
([\ud83c][\uDE00])
| ([\ud82c][\uDC01])
)
SingleQuoteSupp = (
[]
)
DoubleQuoteSupp = (
[]
)
HebrewLetterSupp = (
[]
)
RegionalIndicatorSupp = (
([\ud83c][\uDDE6-\uDDFF])
)

View File

@ -0,0 +1,194 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.ByteBuffer;
import java.util.Map;
import org.apache.cassandra.index.sasi.analyzer.filter.*;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.io.util.DataInputBuffer;
import org.apache.cassandra.utils.ByteBufferUtil;
import com.google.common.annotations.VisibleForTesting;
import com.carrotsearch.hppc.IntObjectMap;
import com.carrotsearch.hppc.IntObjectOpenHashMap;
public class StandardAnalyzer extends AbstractAnalyzer
{
public enum TokenType
{
EOF(-1),
ALPHANUM(0),
NUM(6),
SOUTHEAST_ASIAN(9),
IDEOGRAPHIC(10),
HIRAGANA(11),
KATAKANA(12),
HANGUL(13);
private static final IntObjectMap<TokenType> TOKENS = new IntObjectOpenHashMap<>();
static
{
for (TokenType type : TokenType.values())
TOKENS.put(type.value, type);
}
public final int value;
TokenType(int value)
{
this.value = value;
}
public int getValue()
{
return value;
}
public static TokenType fromValue(int val)
{
return TOKENS.get(val);
}
}
private AbstractType validator;
private StandardTokenizerInterface scanner;
private StandardTokenizerOptions options;
private FilterPipelineTask filterPipeline;
protected Reader inputReader = null;
public String getToken()
{
return scanner.getText();
}
public final boolean incrementToken() throws IOException
{
while(true)
{
TokenType currentTokenType = TokenType.fromValue(scanner.getNextToken());
if (currentTokenType == TokenType.EOF)
return false;
if (scanner.yylength() <= options.getMaxTokenLength()
&& scanner.yylength() >= options.getMinTokenLength())
return true;
}
}
protected String getFilteredCurrentToken() throws IOException
{
String token = getToken();
Object pipelineRes;
while (true)
{
pipelineRes = FilterPipelineExecutor.execute(filterPipeline, token);
if (pipelineRes != null)
break;
boolean reachedEOF = incrementToken();
if (!reachedEOF)
break;
token = getToken();
}
return (String) pipelineRes;
}
private FilterPipelineTask getFilterPipeline()
{
FilterPipelineBuilder builder = new FilterPipelineBuilder(new BasicResultFilters.NoOperation());
if (!options.isCaseSensitive() && options.shouldLowerCaseTerms())
builder = builder.add("to_lower", new BasicResultFilters.LowerCase());
if (!options.isCaseSensitive() && options.shouldUpperCaseTerms())
builder = builder.add("to_upper", new BasicResultFilters.UpperCase());
if (options.shouldStemTerms())
builder = builder.add("term_stemming", new StemmingFilters.DefaultStemmingFilter(options.getLocale()));
if (options.shouldIgnoreStopTerms())
builder = builder.add("skip_stop_words", new StopWordFilters.DefaultStopWordFilter(options.getLocale()));
return builder.build();
}
public void init(Map<String, String> options, AbstractType validator)
{
init(StandardTokenizerOptions.buildFromMap(options), validator);
}
@VisibleForTesting
protected void init(StandardTokenizerOptions options)
{
init(options, UTF8Type.instance);
}
public void init(StandardTokenizerOptions tokenizerOptions, AbstractType validator)
{
this.validator = validator;
this.options = tokenizerOptions;
this.filterPipeline = getFilterPipeline();
Reader reader = new InputStreamReader(new DataInputBuffer(ByteBufferUtil.EMPTY_BYTE_BUFFER, false));
this.scanner = new StandardTokenizerImpl(reader);
this.inputReader = reader;
}
public boolean hasNext()
{
try
{
if (incrementToken())
{
if (getFilteredCurrentToken() != null)
{
this.next = validator.fromString(normalize(getFilteredCurrentToken()));
return true;
}
}
}
catch (IOException e)
{}
return false;
}
public void reset(ByteBuffer input)
{
this.next = null;
Reader reader = new InputStreamReader(new DataInputBuffer(input, false));
scanner.yyreset(reader);
this.inputReader = reader;
}
public void reset(InputStream input)
{
this.next = null;
Reader reader = new InputStreamReader(input);
scanner.yyreset(reader);
this.inputReader = reader;
}
}

View File

@ -0,0 +1,220 @@
package org.apache.cassandra.index.sasi.analyzer;
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.util.Arrays;
/**
* This class implements Word Break rules from the Unicode Text Segmentation
* algorithm, as specified in
* <a href="http://unicode.org/reports/tr29/">Unicode Standard Annex #29</a>. ∂
* <p/>
* Tokens produced are of the following types:
* <ul>
* <li>&lt;ALPHANUM&gt;: A sequence of alphabetic and numeric characters</li>
* <li>&lt;NUM&gt;: A number</li>
* <li>&lt;SOUTHEAST_ASIAN&gt;: A sequence of characters from South and Southeast
* Asian languages, including Thai, Lao, Myanmar, and Khmer</li>
* <li>&lt;IDEOGRAPHIC&gt;: A single CJKV ideographic character</li>
* <li>&lt;HIRAGANA&gt;: A single hiragana character</li>
* <li>&lt;KATAKANA&gt;: A sequence of katakana characters</li>
* <li>&lt;HANGUL&gt;: A sequence of Hangul characters</li>
* </ul>
*/
%%
%unicode 6.3
%integer
%final
%public
%class StandardTokenizerImpl
%implements StandardTokenizerInterface
%function getNextToken
%char
%buffer 4096
%include SUPPLEMENTARY.jflex-macro
ALetter = (\p{WB:ALetter} | {ALetterSupp})
Format = (\p{WB:Format} | {FormatSupp})
Numeric = ([\p{WB:Numeric}[\p{Blk:HalfAndFullForms}&&\p{Nd}]] | {NumericSupp})
Extend = (\p{WB:Extend} | {ExtendSupp})
Katakana = (\p{WB:Katakana} | {KatakanaSupp})
MidLetter = (\p{WB:MidLetter} | {MidLetterSupp})
MidNum = (\p{WB:MidNum} | {MidNumSupp})
MidNumLet = (\p{WB:MidNumLet} | {MidNumLetSupp})
ExtendNumLet = (\p{WB:ExtendNumLet} | {ExtendNumLetSupp})
ComplexContext = (\p{LB:Complex_Context} | {ComplexContextSupp})
Han = (\p{Script:Han} | {HanSupp})
Hiragana = (\p{Script:Hiragana} | {HiraganaSupp})
SingleQuote = (\p{WB:Single_Quote} | {SingleQuoteSupp})
DoubleQuote = (\p{WB:Double_Quote} | {DoubleQuoteSupp})
HebrewLetter = (\p{WB:Hebrew_Letter} | {HebrewLetterSupp})
RegionalIndicator = (\p{WB:Regional_Indicator} | {RegionalIndicatorSupp})
HebrewOrALetter = ({HebrewLetter} | {ALetter})
// UAX#29 WB4. X (Extend | Format)* --> X
//
HangulEx = [\p{Script:Hangul}&&[\p{WB:ALetter}\p{WB:Hebrew_Letter}]] ({Format} | {Extend})*
HebrewOrALetterEx = {HebrewOrALetter} ({Format} | {Extend})*
NumericEx = {Numeric} ({Format} | {Extend})*
KatakanaEx = {Katakana} ({Format} | {Extend})*
MidLetterEx = ({MidLetter} | {MidNumLet} | {SingleQuote}) ({Format} | {Extend})*
MidNumericEx = ({MidNum} | {MidNumLet} | {SingleQuote}) ({Format} | {Extend})*
ExtendNumLetEx = {ExtendNumLet} ({Format} | {Extend})*
HanEx = {Han} ({Format} | {Extend})*
HiraganaEx = {Hiragana} ({Format} | {Extend})*
SingleQuoteEx = {SingleQuote} ({Format} | {Extend})*
DoubleQuoteEx = {DoubleQuote} ({Format} | {Extend})*
HebrewLetterEx = {HebrewLetter} ({Format} | {Extend})*
RegionalIndicatorEx = {RegionalIndicator} ({Format} | {Extend})*
%{
/** Alphanumeric sequences */
public static final int WORD_TYPE = StandardAnalyzer.TokenType.ALPHANUM.value;
/** Numbers */
public static final int NUMERIC_TYPE = StandardAnalyzer.TokenType.NUM.value;
/**
* Chars in class \p{Line_Break = Complex_Context} are from South East Asian
* scripts (Thai, Lao, Myanmar, Khmer, etc.). Sequences of these are kept
* together as as a single token rather than broken up, because the logic
* required to break them at word boundaries is too complex for UAX#29.
* <p>
* See Unicode Line Breaking Algorithm: http://www.unicode.org/reports/tr14/#SA
*/
public static final int SOUTH_EAST_ASIAN_TYPE = StandardAnalyzer.TokenType.SOUTHEAST_ASIAN.value;
public static final int IDEOGRAPHIC_TYPE = StandardAnalyzer.TokenType.IDEOGRAPHIC.value;
public static final int HIRAGANA_TYPE = StandardAnalyzer.TokenType.HIRAGANA.value;
public static final int KATAKANA_TYPE = StandardAnalyzer.TokenType.KATAKANA.value;
public static final int HANGUL_TYPE = StandardAnalyzer.TokenType.HANGUL.value;
public final int yychar()
{
return yychar;
}
public String getText()
{
return String.valueOf(zzBuffer, zzStartRead, zzMarkedPos-zzStartRead);
}
public char[] getArray()
{
return Arrays.copyOfRange(zzBuffer, zzStartRead, zzMarkedPos);
}
public byte[] getBytes()
{
return getText().getBytes();
}
%}
%%
// UAX#29 WB1. sot ÷
// WB2. ÷ eot
//
<<EOF>> { return StandardAnalyzer.TokenType.EOF.value; }
// UAX#29 WB8. Numeric × Numeric
// WB11. Numeric (MidNum | MidNumLet | Single_Quote) × Numeric
// WB12. Numeric × (MidNum | MidNumLet | Single_Quote) Numeric
// WB13a. (ALetter | Hebrew_Letter | Numeric | Katakana | ExtendNumLet) × ExtendNumLet
// WB13b. ExtendNumLet × (ALetter | Hebrew_Letter | Numeric | Katakana)
//
{ExtendNumLetEx}* {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )* {ExtendNumLetEx}*
{ return NUMERIC_TYPE; }
// subset of the below for typing purposes only!
{HangulEx}+
{ return HANGUL_TYPE; }
{KatakanaEx}+
{ return KATAKANA_TYPE; }
// UAX#29 WB5. (ALetter | Hebrew_Letter) × (ALetter | Hebrew_Letter)
// WB6. (ALetter | Hebrew_Letter) × (MidLetter | MidNumLet | Single_Quote) (ALetter | Hebrew_Letter)
// WB7. (ALetter | Hebrew_Letter) (MidLetter | MidNumLet | Single_Quote) × (ALetter | Hebrew_Letter)
// WB7a. Hebrew_Letter × Single_Quote
// WB7b. Hebrew_Letter × Double_Quote Hebrew_Letter
// WB7c. Hebrew_Letter Double_Quote × Hebrew_Letter
// WB9. (ALetter | Hebrew_Letter) × Numeric
// WB10. Numeric × (ALetter | Hebrew_Letter)
// WB13. Katakana × Katakana
// WB13a. (ALetter | Hebrew_Letter | Numeric | Katakana | ExtendNumLet) × ExtendNumLet
// WB13b. ExtendNumLet × (ALetter | Hebrew_Letter | Numeric | Katakana)
//
{ExtendNumLetEx}* ( {KatakanaEx} ( {ExtendNumLetEx}* {KatakanaEx} )*
| ( {HebrewLetterEx} ( {SingleQuoteEx} | {DoubleQuoteEx} {HebrewLetterEx} )
| {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )*
| {HebrewOrALetterEx} ( ( {ExtendNumLetEx}* | {MidLetterEx} ) {HebrewOrALetterEx} )*
)+
)
({ExtendNumLetEx}+ ( {KatakanaEx} ( {ExtendNumLetEx}* {KatakanaEx} )*
| ( {HebrewLetterEx} ( {SingleQuoteEx} | {DoubleQuoteEx} {HebrewLetterEx} )
| {NumericEx} ( ( {ExtendNumLetEx}* | {MidNumericEx} ) {NumericEx} )*
| {HebrewOrALetterEx} ( ( {ExtendNumLetEx}* | {MidLetterEx} ) {HebrewOrALetterEx} )*
)+
)
)*
{ExtendNumLetEx}*
{ return WORD_TYPE; }
// From UAX #29:
//
// [C]haracters with the Line_Break property values of Contingent_Break (CB),
// Complex_Context (SA/South East Asian), and XX (Unknown) are assigned word
// boundary property values based on criteria outside of the scope of this
// annex. That means that satisfactory treatment of languages like Chinese
// or Thai requires special handling.
//
// In Unicode 6.3, only one character has the \p{Line_Break = Contingent_Break}
// property: U+FFFC ( ) OBJECT REPLACEMENT CHARACTER.
//
// In the ICU implementation of UAX#29, \p{Line_Break = Complex_Context}
// character sequences (from South East Asian scripts like Thai, Myanmar, Khmer,
// Lao, etc.) are kept together. This grammar does the same below.
//
// See also the Unicode Line Breaking Algorithm:
//
// http://www.unicode.org/reports/tr14/#SA
//
{ComplexContext}+ { return SOUTH_EAST_ASIAN_TYPE; }
// UAX#29 WB14. Any ÷ Any
//
{HanEx} { return IDEOGRAPHIC_TYPE; }
{HiraganaEx} { return HIRAGANA_TYPE; }
// UAX#29 WB3. CR × LF
// WB3a. (Newline | CR | LF) ÷
// WB3b. ÷ (Newline | CR | LF)
// WB13c. Regional_Indicator × Regional_Indicator
// WB14. Any ÷ Any
//
{RegionalIndicatorEx} {RegionalIndicatorEx}+ | [^]
{ /* Break so we don't hit fall-through warning: */ break; /* Not numeric, word, ideographic, hiragana, or SE Asian -- ignore it. */ }

View File

@ -0,0 +1,65 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer;
import java.io.IOException;
import java.io.Reader;
/**
* Internal interface for supporting versioned grammars.
*/
public interface StandardTokenizerInterface
{
String getText();
char[] getArray();
byte[] getBytes();
/**
* Returns the current position.
*/
int yychar();
/**
* Returns the length of the matched text region.
*/
int yylength();
/**
* Resumes scanning until the next regular expression is matched,
* the end of input is encountered or an I/O-Error occurs.
*
* @return the next token, {@link #YYEOF} on end of stream
* @exception java.io.IOException if any I/O-Error occurs
*/
int getNextToken() throws IOException;
/**
* Resets the scanner to read from a new input stream.
* Does not close the old reader.
*
* All internal variables are reset, the old input stream
* <b>cannot</b> be reused (internal buffer is discarded and lost).
* Lexical state is set to <tt>ZZ_INITIAL</tt>.
*
* @param reader the new input stream
*/
void yyreset(Reader reader);
}

View File

@ -0,0 +1,272 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer;
import java.util.Locale;
import java.util.Map;
/**
* Various options for controlling tokenization and enabling
* or disabling features
*/
public class StandardTokenizerOptions
{
public static final String TOKENIZATION_ENABLE_STEMMING = "tokenization_enable_stemming";
public static final String TOKENIZATION_SKIP_STOP_WORDS = "tokenization_skip_stop_words";
public static final String TOKENIZATION_LOCALE = "tokenization_locale";
public static final String TOKENIZATION_NORMALIZE_LOWERCASE = "tokenization_normalize_lowercase";
public static final String TOKENIZATION_NORMALIZE_UPPERCASE = "tokenization_normalize_uppercase";
public static final int DEFAULT_MAX_TOKEN_LENGTH = 255;
public static final int DEFAULT_MIN_TOKEN_LENGTH = 0;
private boolean stemTerms;
private boolean ignoreStopTerms;
private Locale locale;
private boolean caseSensitive;
private boolean allTermsToUpperCase;
private boolean allTermsToLowerCase;
private int minTokenLength;
private int maxTokenLength;
public boolean shouldStemTerms()
{
return stemTerms;
}
public void setStemTerms(boolean stemTerms)
{
this.stemTerms = stemTerms;
}
public boolean shouldIgnoreStopTerms()
{
return ignoreStopTerms;
}
public void setIgnoreStopTerms(boolean ignoreStopTerms)
{
this.ignoreStopTerms = ignoreStopTerms;
}
public Locale getLocale()
{
return locale;
}
public void setLocale(Locale locale)
{
this.locale = locale;
}
public boolean isCaseSensitive()
{
return caseSensitive;
}
public void setCaseSensitive(boolean caseSensitive)
{
this.caseSensitive = caseSensitive;
}
public boolean shouldUpperCaseTerms()
{
return allTermsToUpperCase;
}
public void setAllTermsToUpperCase(boolean allTermsToUpperCase)
{
this.allTermsToUpperCase = allTermsToUpperCase;
}
public boolean shouldLowerCaseTerms()
{
return allTermsToLowerCase;
}
public void setAllTermsToLowerCase(boolean allTermsToLowerCase)
{
this.allTermsToLowerCase = allTermsToLowerCase;
}
public int getMinTokenLength()
{
return minTokenLength;
}
public void setMinTokenLength(int minTokenLength)
{
this.minTokenLength = minTokenLength;
}
public int getMaxTokenLength()
{
return maxTokenLength;
}
public void setMaxTokenLength(int maxTokenLength)
{
this.maxTokenLength = maxTokenLength;
}
public static class OptionsBuilder {
private boolean stemTerms;
private boolean ignoreStopTerms;
private Locale locale;
private boolean caseSensitive;
private boolean allTermsToUpperCase;
private boolean allTermsToLowerCase;
private int minTokenLength = DEFAULT_MIN_TOKEN_LENGTH;
private int maxTokenLength = DEFAULT_MAX_TOKEN_LENGTH;
public OptionsBuilder()
{
}
public OptionsBuilder stemTerms(boolean stemTerms)
{
this.stemTerms = stemTerms;
return this;
}
public OptionsBuilder ignoreStopTerms(boolean ignoreStopTerms)
{
this.ignoreStopTerms = ignoreStopTerms;
return this;
}
public OptionsBuilder useLocale(Locale locale)
{
this.locale = locale;
return this;
}
public OptionsBuilder caseSensitive(boolean caseSensitive)
{
this.caseSensitive = caseSensitive;
return this;
}
public OptionsBuilder alwaysUpperCaseTerms(boolean allTermsToUpperCase)
{
this.allTermsToUpperCase = allTermsToUpperCase;
return this;
}
public OptionsBuilder alwaysLowerCaseTerms(boolean allTermsToLowerCase)
{
this.allTermsToLowerCase = allTermsToLowerCase;
return this;
}
/**
* Set the min allowed token length. Any token shorter
* than this is skipped.
*/
public OptionsBuilder minTokenLength(int minTokenLength)
{
if (minTokenLength < 1)
throw new IllegalArgumentException("minTokenLength must be greater than zero");
this.minTokenLength = minTokenLength;
return this;
}
/**
* Set the max allowed token length. Any token longer
* than this is skipped.
*/
public OptionsBuilder maxTokenLength(int maxTokenLength)
{
if (maxTokenLength < 1)
throw new IllegalArgumentException("maxTokenLength must be greater than zero");
this.maxTokenLength = maxTokenLength;
return this;
}
public StandardTokenizerOptions build()
{
if(allTermsToLowerCase && allTermsToUpperCase)
throw new IllegalArgumentException("Options to normalize terms cannot be " +
"both uppercase and lowercase at the same time");
StandardTokenizerOptions options = new StandardTokenizerOptions();
options.setIgnoreStopTerms(ignoreStopTerms);
options.setStemTerms(stemTerms);
options.setLocale(locale);
options.setCaseSensitive(caseSensitive);
options.setAllTermsToLowerCase(allTermsToLowerCase);
options.setAllTermsToUpperCase(allTermsToUpperCase);
options.setMinTokenLength(minTokenLength);
options.setMaxTokenLength(maxTokenLength);
return options;
}
}
public static StandardTokenizerOptions buildFromMap(Map<String, String> optionsMap)
{
OptionsBuilder optionsBuilder = new OptionsBuilder();
for (Map.Entry<String, String> entry : optionsMap.entrySet())
{
switch(entry.getKey())
{
case TOKENIZATION_ENABLE_STEMMING:
{
boolean bool = Boolean.parseBoolean(entry.getValue());
optionsBuilder = optionsBuilder.stemTerms(bool);
break;
}
case TOKENIZATION_SKIP_STOP_WORDS:
{
boolean bool = Boolean.parseBoolean(entry.getValue());
optionsBuilder = optionsBuilder.ignoreStopTerms(bool);
break;
}
case TOKENIZATION_LOCALE:
{
Locale locale = new Locale(entry.getValue());
optionsBuilder = optionsBuilder.useLocale(locale);
break;
}
case TOKENIZATION_NORMALIZE_UPPERCASE:
{
boolean bool = Boolean.parseBoolean(entry.getValue());
optionsBuilder = optionsBuilder.alwaysUpperCaseTerms(bool);
break;
}
case TOKENIZATION_NORMALIZE_LOWERCASE:
{
boolean bool = Boolean.parseBoolean(entry.getValue());
optionsBuilder = optionsBuilder.alwaysLowerCaseTerms(bool);
break;
}
default:
{
}
}
}
return optionsBuilder.build();
}
public static StandardTokenizerOptions getDefaultOptions()
{
return new OptionsBuilder()
.ignoreStopTerms(true).alwaysLowerCaseTerms(true)
.stemTerms(false).useLocale(Locale.ENGLISH).build();
}
}

View File

@ -0,0 +1,76 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
import java.util.Locale;
/**
* Basic/General Token Filters
*/
public class BasicResultFilters
{
private static final Locale DEFAULT_LOCALE = Locale.getDefault();
public static class LowerCase extends FilterPipelineTask<String, String>
{
private Locale locale;
public LowerCase(Locale locale)
{
this.locale = locale;
}
public LowerCase()
{
this.locale = DEFAULT_LOCALE;
}
public String process(String input) throws Exception
{
return input.toLowerCase(locale);
}
}
public static class UpperCase extends FilterPipelineTask<String, String>
{
private Locale locale;
public UpperCase(Locale locale)
{
this.locale = locale;
}
public UpperCase()
{
this.locale = DEFAULT_LOCALE;
}
public String process(String input) throws Exception
{
return input.toUpperCase(locale);
}
}
public static class NoOperation extends FilterPipelineTask<Object, Object>
{
public Object process(Object input) throws Exception
{
return input;
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
/**
* Creates a Pipeline object for applying n pieces of logic
* from the provided methods to the builder in a guaranteed order
*/
public class FilterPipelineBuilder
{
private final FilterPipelineTask<?,?> parent;
private FilterPipelineTask<?,?> current;
public FilterPipelineBuilder(FilterPipelineTask<?, ?> first)
{
this(first, first);
}
private FilterPipelineBuilder(FilterPipelineTask<?, ?> first, FilterPipelineTask<?, ?> current)
{
this.parent = first;
this.current = current;
}
public FilterPipelineBuilder add(String name, FilterPipelineTask<?,?> nextTask)
{
this.current.setLast(name, nextTask);
this.current = nextTask;
return this;
}
public FilterPipelineTask<?,?> build()
{
return this.parent;
}
}

View File

@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Executes all linked Pipeline Tasks serially and returns
* output (if exists) from the executed logic
*/
public class FilterPipelineExecutor
{
private static final Logger logger = LoggerFactory.getLogger(FilterPipelineExecutor.class);
public static <F,T> T execute(FilterPipelineTask<F, T> task, T initialInput)
{
FilterPipelineTask<?, ?> taskPtr = task;
T result = initialInput;
try
{
while (true)
{
FilterPipelineTask<F,T> taskGeneric = (FilterPipelineTask<F,T>) taskPtr;
result = taskGeneric.process((F) result);
taskPtr = taskPtr.next;
if(taskPtr == null)
return result;
}
}
catch (Exception e)
{
logger.info("An unhandled exception to occurred while processing " +
"pipeline [{}]", task.getName(), e);
}
return null;
}
}

View File

@ -0,0 +1,52 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
/**
* A single task or set of work to process an input
* and return a single output. Maintains a link to the
* next task to be executed after itself
*/
public abstract class FilterPipelineTask<F, T>
{
private String name;
public FilterPipelineTask<?, ?> next;
protected <K, V> void setLast(String name, FilterPipelineTask<K, V> last)
{
if (last == this)
throw new IllegalArgumentException("provided last task [" + last.name + "] cannot be set to itself");
if (this.next == null)
{
this.next = last;
this.name = name;
}
else
{
this.next.setLast(name, last);
}
}
public abstract T process(F input) throws Exception;
public String getName()
{
return name;
}
}

View File

@ -0,0 +1,101 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.tartarus.snowball.SnowballStemmer;
import org.tartarus.snowball.ext.*;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Returns a SnowballStemmer instance appropriate for
* a given language
*/
public class StemmerFactory
{
private static final Logger logger = LoggerFactory.getLogger(StemmerFactory.class);
private static final LoadingCache<Class, Constructor<?>> STEMMER_CONSTRUCTOR_CACHE = CacheBuilder.newBuilder()
.build(new CacheLoader<Class, Constructor<?>>()
{
public Constructor<?> load(Class aClass) throws Exception
{
try
{
return aClass.getConstructor();
}
catch (Exception e) {
logger.error("Failed to get stemmer constructor", e);
}
return null;
}
});
private static final Map<String, Class> SUPPORTED_LANGUAGES;
static
{
SUPPORTED_LANGUAGES = new HashMap<>();
SUPPORTED_LANGUAGES.put("de", germanStemmer.class);
SUPPORTED_LANGUAGES.put("da", danishStemmer.class);
SUPPORTED_LANGUAGES.put("es", spanishStemmer.class);
SUPPORTED_LANGUAGES.put("en", englishStemmer.class);
SUPPORTED_LANGUAGES.put("fl", finnishStemmer.class);
SUPPORTED_LANGUAGES.put("fr", frenchStemmer.class);
SUPPORTED_LANGUAGES.put("hu", hungarianStemmer.class);
SUPPORTED_LANGUAGES.put("it", italianStemmer.class);
SUPPORTED_LANGUAGES.put("nl", dutchStemmer.class);
SUPPORTED_LANGUAGES.put("no", norwegianStemmer.class);
SUPPORTED_LANGUAGES.put("pt", portugueseStemmer.class);
SUPPORTED_LANGUAGES.put("ro", romanianStemmer.class);
SUPPORTED_LANGUAGES.put("ru", russianStemmer.class);
SUPPORTED_LANGUAGES.put("sv", swedishStemmer.class);
SUPPORTED_LANGUAGES.put("tr", turkishStemmer.class);
}
public static SnowballStemmer getStemmer(Locale locale)
{
if (locale == null)
return null;
String rootLang = locale.getLanguage().substring(0, 2);
try
{
Class clazz = SUPPORTED_LANGUAGES.get(rootLang);
if(clazz == null)
return null;
Constructor<?> ctor = STEMMER_CONSTRUCTOR_CACHE.get(clazz);
return (SnowballStemmer) ctor.newInstance();
}
catch (Exception e)
{
logger.debug("Failed to create new SnowballStemmer instance " +
"for language [{}]", locale.getLanguage(), e);
}
return null;
}
}

View File

@ -0,0 +1,46 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
import java.util.Locale;
import org.tartarus.snowball.SnowballStemmer;
/**
* Filters for performing Stemming on tokens
*/
public class StemmingFilters
{
public static class DefaultStemmingFilter extends FilterPipelineTask<String, String>
{
private SnowballStemmer stemmer;
public DefaultStemmingFilter(Locale locale)
{
stemmer = StemmerFactory.getStemmer(locale);
}
public String process(String input) throws Exception
{
if (stemmer == null)
return input;
stemmer.setCurrent(input);
return (stemmer.stem()) ? stemmer.getCurrent() : input;
}
}
}

View File

@ -0,0 +1,100 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides a list of Stop Words for a given language
*/
public class StopWordFactory
{
private static final Logger logger = LoggerFactory.getLogger(StopWordFactory.class);
private static final String DEFAULT_RESOURCE_EXT = "_ST.txt";
private static final String DEFAULT_RESOURCE_PREFIX = StopWordFactory.class.getPackage()
.getName().replace(".", File.separator);
private static final Set<String> SUPPORTED_LANGUAGES = new HashSet<>(
Arrays.asList("ar","bg","cs","de","en","es","fi","fr","hi","hu","it",
"pl","pt","ro","ru","sv"));
private static final LoadingCache<String, Set<String>> STOP_WORDS_CACHE = CacheBuilder.newBuilder()
.build(new CacheLoader<String, Set<String>>()
{
public Set<String> load(String s) throws Exception
{
return getStopWordsFromResource(s);
}
});
public static Set<String> getStopWordsForLanguage(Locale locale)
{
if (locale == null)
return null;
String rootLang = locale.getLanguage().substring(0, 2);
try
{
return (!SUPPORTED_LANGUAGES.contains(rootLang)) ? null : STOP_WORDS_CACHE.get(rootLang);
}
catch (ExecutionException e)
{
logger.error("Failed to populate Stop Words Cache for language [{}]", locale.getLanguage(), e);
return null;
}
}
private static Set<String> getStopWordsFromResource(String language)
{
Set<String> stopWords = new HashSet<>();
String resourceName = DEFAULT_RESOURCE_PREFIX + File.separator + language + DEFAULT_RESOURCE_EXT;
try (InputStream is = StopWordFactory.class.getClassLoader().getResourceAsStream(resourceName);
BufferedReader r = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)))
{
String line;
while ((line = r.readLine()) != null)
{
//skip comments (lines starting with # char)
if(line.charAt(0) == '#')
continue;
stopWords.add(line.trim());
}
}
catch (Exception e)
{
logger.error("Failed to retrieve Stop Terms resource for language [{}]", language, e);
}
return stopWords;
}
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.analyzer.filter;
import java.util.Locale;
import java.util.Set;
/**
* Filter implementations for input matching Stop Words
*/
public class StopWordFilters
{
public static class DefaultStopWordFilter extends FilterPipelineTask<String, String>
{
private Set<String> stopWords = null;
public DefaultStopWordFilter(Locale locale)
{
this.stopWords = StopWordFactory.getStopWordsForLanguage(locale);
}
public String process(String input) throws Exception
{
return (stopWords != null && stopWords.contains(input)) ? null : input;
}
}
}

View File

@ -0,0 +1,193 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.conf;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.AsciiType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.db.rows.Cell;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
import org.apache.cassandra.index.sasi.conf.view.View;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.memory.IndexMemtable;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.io.sstable.Component;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.schema.IndexMetadata;
import org.apache.cassandra.utils.FBUtilities;
public class ColumnIndex
{
private static final String FILE_NAME_FORMAT = "SI_%s.db";
private final AbstractType<?> keyValidator;
private final ColumnDefinition column;
private final Optional<IndexMetadata> config;
private final AtomicReference<IndexMemtable> memtable;
private final IndexMode mode;
private final Component component;
private final DataTracker tracker;
public ColumnIndex(AbstractType<?> keyValidator, ColumnDefinition column, IndexMetadata metadata)
{
this.keyValidator = keyValidator;
this.column = column;
this.config = metadata == null ? Optional.empty() : Optional.of(metadata);
this.mode = IndexMode.getMode(column, config);
this.memtable = new AtomicReference<>(new IndexMemtable(this));
this.tracker = new DataTracker(keyValidator, this);
this.component = new Component(Component.Type.SECONDARY_INDEX, String.format(FILE_NAME_FORMAT, getIndexName()));
}
public void validate() throws ConfigurationException
{
mode.validate(config);
}
/**
* Initialize this column index with specific set of SSTables.
*
* @param sstables The sstables to be used by index initially.
*
* @return A collection of sstables which don't have this specific index attached to them.
*/
public Iterable<SSTableReader> init(Set<SSTableReader> sstables)
{
return tracker.update(Collections.emptySet(), sstables);
}
public AbstractType<?> keyValidator()
{
return keyValidator;
}
public long index(DecoratedKey key, Row row)
{
return memtable.get().index(key, getValueOf(column, row, FBUtilities.nowInSeconds()));
}
public void switchMemtable()
{
memtable.set(new IndexMemtable(this));
}
public RangeIterator<Long, Token> searchMemtable(Expression e)
{
return memtable.get().search(e);
}
public void update(Collection<SSTableReader> oldSSTables, Collection<SSTableReader> newSSTables)
{
tracker.update(oldSSTables, newSSTables);
}
public ColumnDefinition getDefinition()
{
return column;
}
public AbstractType<?> getValidator()
{
return column.cellValueType();
}
public Component getComponent()
{
return component;
}
public IndexMode getMode()
{
return mode;
}
public String getColumnName()
{
return column.name.toString();
}
public String getIndexName()
{
return config.isPresent() ? config.get().name : "undefined";
}
public AbstractAnalyzer getAnalyzer()
{
AbstractAnalyzer analyzer = mode.getAnalyzer(getValidator());
analyzer.init(config.isPresent() ? config.get().options : Collections.emptyMap(), column.cellValueType());
return analyzer;
}
public View getView()
{
return tracker.getView();
}
public boolean hasSSTable(SSTableReader sstable)
{
return tracker.hasSSTable(sstable);
}
public void dropData(long truncateUntil)
{
switchMemtable();
tracker.dropData(truncateUntil);
}
public boolean isIndexed()
{
return mode != IndexMode.NOT_INDEXED;
}
public boolean isLiteral()
{
AbstractType<?> validator = getValidator();
return isIndexed() ? mode.isLiteral : (validator instanceof UTF8Type || validator instanceof AsciiType);
}
public static ByteBuffer getValueOf(ColumnDefinition column, Row row, int nowInSecs)
{
switch (column.kind)
{
case CLUSTERING:
return row.clustering().get(column.position());
case REGULAR:
Cell cell = row.getCell(column);
return cell == null || !cell.isLive(nowInSecs) ? null : cell.value();
default:
return null;
}
}
}

View File

@ -0,0 +1,162 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.conf;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.index.sasi.SSTableIndex;
import org.apache.cassandra.index.sasi.conf.view.View;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** a pared-down version of DataTracker and DT.View. need one for each index of each column family */
public class DataTracker
{
private static final Logger logger = LoggerFactory.getLogger(DataTracker.class);
private final AbstractType<?> keyValidator;
private final ColumnIndex columnIndex;
private final AtomicReference<View> view = new AtomicReference<>();
public DataTracker(AbstractType<?> keyValidator, ColumnIndex index)
{
this.keyValidator = keyValidator;
this.columnIndex = index;
this.view.set(new View(index, Collections.<SSTableIndex>emptySet()));
}
public View getView()
{
return view.get();
}
/**
* Replaces old SSTables with new by creating new immutable tracker.
*
* @param oldSSTables A set of SSTables to remove.
* @param newSSTables A set of SSTables to add to tracker.
*
* @return A collection of SSTables which don't have component attached for current index.
*/
public Iterable<SSTableReader> update(Collection<SSTableReader> oldSSTables, Collection<SSTableReader> newSSTables)
{
final Set<SSTableIndex> newIndexes = getIndexes(newSSTables);
final Set<SSTableReader> indexedSSTables = getSSTables(newIndexes);
View currentView, newView;
do
{
currentView = view.get();
newView = new View(columnIndex, currentView.getIndexes(), oldSSTables, newIndexes);
}
while (!view.compareAndSet(currentView, newView));
return newSSTables.stream().filter(sstable -> !indexedSSTables.contains(sstable)).collect(Collectors.toList());
}
public boolean hasSSTable(SSTableReader sstable)
{
View currentView = view.get();
for (SSTableIndex index : currentView)
{
if (index.getSSTable().equals(sstable))
return true;
}
return false;
}
public void dropData(long truncateUntil)
{
View currentView = view.get();
if (currentView == null)
return;
Set<SSTableReader> toRemove = new HashSet<>();
for (SSTableIndex index : currentView)
{
SSTableReader sstable = index.getSSTable();
if (sstable.getMaxTimestamp() > truncateUntil)
continue;
index.markObsolete();
toRemove.add(sstable);
}
update(toRemove, Collections.<SSTableReader>emptyList());
}
private Set<SSTableIndex> getIndexes(Collection<SSTableReader> sstables)
{
Set<SSTableIndex> indexes = new HashSet<>(sstables.size());
for (SSTableReader sstable : sstables)
{
if (sstable.isMarkedCompacted())
continue;
File indexFile = new File(sstable.descriptor.filenameFor(columnIndex.getComponent()));
if (!indexFile.exists())
continue;
SSTableIndex index = null;
try
{
index = new SSTableIndex(columnIndex, indexFile, sstable);
logger.info("SSTableIndex.open(column: {}, minTerm: {}, maxTerm: {}, minKey: {}, maxKey: {}, sstable: {})",
columnIndex.getColumnName(),
columnIndex.getValidator().getString(index.minTerm()),
columnIndex.getValidator().getString(index.maxTerm()),
keyValidator.getString(index.minKey()),
keyValidator.getString(index.maxKey()),
index.getSSTable());
// Try to add new index to the set, if set already has such index, we'll simply release and move on.
// This covers situation when sstable collection has the same sstable multiple
// times because we don't know what kind of collection it actually is.
if (!indexes.add(index))
index.release();
}
catch (Throwable t)
{
logger.error("Can't open index file at " + indexFile.getAbsolutePath() + ", skipping.", t);
if (index != null)
index.release();
}
}
return indexes;
}
private Set<SSTableReader> getSSTables(Set<SSTableIndex> indexes)
{
return Sets.newHashSet(indexes.stream().map(SSTableIndex::getSSTable).collect(Collectors.toList()));
}
}

View File

@ -0,0 +1,169 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.conf;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
import org.apache.cassandra.index.sasi.analyzer.NoOpAnalyzer;
import org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer;
import org.apache.cassandra.index.sasi.analyzer.StandardAnalyzer;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.AsciiType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.exceptions.ConfigurationException;
import org.apache.cassandra.schema.IndexMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IndexMode
{
private static final Logger logger = LoggerFactory.getLogger(IndexMode.class);
public static final IndexMode NOT_INDEXED = new IndexMode(Mode.PREFIX, true, false, NonTokenizingAnalyzer.class, 0);
private static final Set<AbstractType<?>> TOKENIZABLE_TYPES = new HashSet<AbstractType<?>>()
{{
add(UTF8Type.instance);
add(AsciiType.instance);
}};
private static final String INDEX_MODE_OPTION = "mode";
private static final String INDEX_ANALYZED_OPTION = "analyzed";
private static final String INDEX_ANALYZER_CLASS_OPTION = "analyzer_class";
private static final String INDEX_IS_LITERAL_OPTION = "is_literal";
private static final String INDEX_MAX_FLUSH_MEMORY_OPTION = "max_compaction_flush_memory_in_mb";
private static final double INDEX_MAX_FLUSH_DEFAULT_MULTIPLIER = 0.15;
public final Mode mode;
public final boolean isAnalyzed, isLiteral;
public final Class analyzerClass;
public final long maxCompactionFlushMemoryInMb;
private IndexMode(Mode mode, boolean isLiteral, boolean isAnalyzed, Class analyzerClass, long maxFlushMemMb)
{
this.mode = mode;
this.isLiteral = isLiteral;
this.isAnalyzed = isAnalyzed;
this.analyzerClass = analyzerClass;
this.maxCompactionFlushMemoryInMb = maxFlushMemMb;
}
public void validate(Optional<IndexMetadata> config) throws ConfigurationException
{
if (!config.isPresent())
return;
Map<String, String> indexOptions = config.get().options;
// validate that a valid analyzer class was provided if specified
if (indexOptions.containsKey(INDEX_ANALYZER_CLASS_OPTION))
{
try
{
Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION));
}
catch (ClassNotFoundException e)
{
throw new ConfigurationException(String.format("Invalid analyzer class option specified [%s]",
indexOptions.get(INDEX_ANALYZER_CLASS_OPTION)));
}
}
}
public AbstractAnalyzer getAnalyzer(AbstractType<?> validator)
{
AbstractAnalyzer analyzer = new NoOpAnalyzer();
try
{
if (isAnalyzed)
{
if (analyzerClass != null)
analyzer = (AbstractAnalyzer) analyzerClass.newInstance();
else if (TOKENIZABLE_TYPES.contains(validator))
analyzer = new StandardAnalyzer();
}
}
catch (InstantiationException | IllegalAccessException e)
{
logger.error("Failed to create new instance of analyzer with class [{}]", analyzerClass.getName(), e);
}
return analyzer;
}
public static IndexMode getMode(ColumnDefinition column, Optional<IndexMetadata> config)
{
Map<String, String> indexOptions = config.isPresent() ? config.get().options : null;
if (indexOptions == null || indexOptions.isEmpty())
return IndexMode.NOT_INDEXED;
Mode mode = indexOptions.get(INDEX_MODE_OPTION) == null
? Mode.PREFIX
: Mode.mode(indexOptions.get(INDEX_MODE_OPTION));
boolean isAnalyzed = false;
Class analyzerClass = null;
try
{
if (indexOptions.get(INDEX_ANALYZER_CLASS_OPTION) != null)
{
analyzerClass = Class.forName(indexOptions.get(INDEX_ANALYZER_CLASS_OPTION));
isAnalyzed = indexOptions.get(INDEX_ANALYZED_OPTION) == null
? true : Boolean.valueOf(indexOptions.get(INDEX_ANALYZED_OPTION));
}
else if (indexOptions.get(INDEX_ANALYZED_OPTION) != null)
{
isAnalyzed = Boolean.valueOf(indexOptions.get(INDEX_ANALYZED_OPTION));
}
}
catch (ClassNotFoundException e)
{
// should not happen as we already validated we could instantiate an instance in validateOptions()
logger.error("Failed to find specified analyzer class [{}]. Falling back to default analyzer",
indexOptions.get(INDEX_ANALYZER_CLASS_OPTION));
}
boolean isLiteral = false;
try
{
String literalOption = indexOptions.get(INDEX_IS_LITERAL_OPTION);
AbstractType<?> validator = column.cellValueType();
isLiteral = literalOption == null
? (validator instanceof UTF8Type || validator instanceof AsciiType)
: Boolean.valueOf(literalOption);
}
catch (Exception e)
{
logger.error("failed to parse {} option, defaulting to 'false' for {} index.", INDEX_IS_LITERAL_OPTION, config.get().name);
}
Long maxMemMb = indexOptions.get(INDEX_MAX_FLUSH_MEMORY_OPTION) == null
? (long) (1073741824 * INDEX_MAX_FLUSH_DEFAULT_MULTIPLIER) // 1G default for memtable
: Long.parseLong(indexOptions.get(INDEX_MAX_FLUSH_MEMORY_OPTION));
return new IndexMode(mode, isLiteral, isAnalyzed, analyzerClass, maxMemMb);
}
}

View File

@ -0,0 +1,194 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.conf.view;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.cassandra.index.sasi.SSTableIndex;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.utils.trie.KeyAnalyzer;
import org.apache.cassandra.index.sasi.utils.trie.PatriciaTrie;
import org.apache.cassandra.index.sasi.utils.trie.Trie;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.utils.Interval;
import org.apache.cassandra.utils.IntervalTree;
import com.google.common.collect.Sets;
/**
* This class is an extension over RangeTermTree for string terms,
* it is required because interval tree can't handle matching if search is on the
* prefix of min/max of the range, so for ascii/utf8 fields we build an additional
* prefix trie (including both min/max terms of the index) and do union of the results
* of the prefix tree search and results from the interval tree lookup.
*/
public class PrefixTermTree extends RangeTermTree
{
private final OnDiskIndexBuilder.Mode mode;
private final Trie<ByteBuffer, Set<SSTableIndex>> trie;
public PrefixTermTree(ByteBuffer min, ByteBuffer max,
Trie<ByteBuffer, Set<SSTableIndex>> trie,
IntervalTree<ByteBuffer, SSTableIndex, Interval<ByteBuffer, SSTableIndex>> ranges,
OnDiskIndexBuilder.Mode mode)
{
super(min, max, ranges);
this.mode = mode;
this.trie = trie;
}
public Set<SSTableIndex> search(Expression e)
{
Map<ByteBuffer, Set<SSTableIndex>> indexes = (e == null || e.lower == null || mode == OnDiskIndexBuilder.Mode.CONTAINS)
? trie : trie.prefixMap(e.lower.value);
Set<SSTableIndex> view = new HashSet<>(indexes.size());
indexes.values().forEach(view::addAll);
return Sets.union(view, super.search(e));
}
public static class Builder extends RangeTermTree.Builder
{
private final PatriciaTrie<ByteBuffer, Set<SSTableIndex>> trie;
protected Builder(OnDiskIndexBuilder.Mode mode, final AbstractType<?> comparator)
{
super(mode, comparator);
trie = new PatriciaTrie<>(new ByteBufferKeyAnalyzer(comparator));
}
public void addIndex(SSTableIndex index)
{
super.addIndex(index);
addTerm(index.minTerm(), index);
addTerm(index.maxTerm(), index);
}
public TermTree build()
{
return new PrefixTermTree(min, max, trie, IntervalTree.build(intervals), mode);
}
private void addTerm(ByteBuffer term, SSTableIndex index)
{
Set<SSTableIndex> indexes = trie.get(term);
if (indexes == null)
trie.put(term, (indexes = new HashSet<>()));
indexes.add(index);
}
}
private static class ByteBufferKeyAnalyzer implements KeyAnalyzer<ByteBuffer>
{
private final AbstractType<?> comparator;
public ByteBufferKeyAnalyzer(AbstractType<?> comparator)
{
this.comparator = comparator;
}
/**
* A bit mask where the first bit is 1 and the others are zero
*/
private static final int MSB = 1 << Byte.SIZE-1;
public int compare(ByteBuffer a, ByteBuffer b)
{
return comparator.compare(a, b);
}
public int lengthInBits(ByteBuffer o)
{
return o.remaining() * Byte.SIZE;
}
public boolean isBitSet(ByteBuffer key, int bitIndex)
{
if (bitIndex >= lengthInBits(key))
return false;
int index = bitIndex / Byte.SIZE;
int bit = bitIndex % Byte.SIZE;
return (key.get(index) & mask(bit)) != 0;
}
public int bitIndex(ByteBuffer key, ByteBuffer otherKey)
{
int length = Math.max(key.remaining(), otherKey.remaining());
boolean allNull = true;
for (int i = 0; i < length; i++)
{
byte b1 = valueAt(key, i);
byte b2 = valueAt(otherKey, i);
if (b1 != b2)
{
int xor = b1 ^ b2;
for (int j = 0; j < Byte.SIZE; j++)
{
if ((xor & mask(j)) != 0)
return (i * Byte.SIZE) + j;
}
}
if (b1 != 0)
allNull = false;
}
return allNull ? KeyAnalyzer.NULL_BIT_KEY : KeyAnalyzer.EQUAL_BIT_KEY;
}
public boolean isPrefix(ByteBuffer key, ByteBuffer prefix)
{
if (key.remaining() < prefix.remaining())
return false;
for (int i = 0; i < prefix.remaining(); i++)
{
if (key.get(i) != prefix.get(i))
return false;
}
return true;
}
/**
* Returns the {@code byte} value at the given index.
*/
private byte valueAt(ByteBuffer value, int index)
{
return index >= 0 && index < value.remaining() ? value.get(index) : 0;
}
/**
* Returns a bit mask where the given bit is set
*/
private int mask(int bit)
{
return MSB >>> bit;
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.conf.view;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.cassandra.index.sasi.SSTableIndex;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.utils.Interval;
import org.apache.cassandra.utils.IntervalTree;
public class RangeTermTree implements TermTree
{
protected final ByteBuffer min, max;
protected final IntervalTree<ByteBuffer, SSTableIndex, Interval<ByteBuffer, SSTableIndex>> rangeTree;
public RangeTermTree(ByteBuffer min, ByteBuffer max, IntervalTree<ByteBuffer, SSTableIndex, Interval<ByteBuffer, SSTableIndex>> rangeTree)
{
this.min = min;
this.max = max;
this.rangeTree = rangeTree;
}
public Set<SSTableIndex> search(Expression e)
{
ByteBuffer minTerm = e.lower == null ? min : e.lower.value;
ByteBuffer maxTerm = e.upper == null ? max : e.upper.value;
return new HashSet<>(rangeTree.search(Interval.create(minTerm, maxTerm, (SSTableIndex) null)));
}
public int intervalCount()
{
return rangeTree.intervalCount();
}
static class Builder extends TermTree.Builder
{
protected final List<Interval<ByteBuffer, SSTableIndex>> intervals = new ArrayList<>();
protected Builder(OnDiskIndexBuilder.Mode mode, AbstractType<?> comparator)
{
super(mode, comparator);
}
public void addIndex(SSTableIndex index)
{
intervals.add(Interval.create(index.minTerm(), index.maxTerm(), index));
}
public TermTree build()
{
return new RangeTermTree(min, max, IntervalTree.build(intervals));
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.conf.view;
import java.nio.ByteBuffer;
import java.util.Set;
import org.apache.cassandra.index.sasi.SSTableIndex;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.db.marshal.AbstractType;
public interface TermTree
{
Set<SSTableIndex> search(Expression e);
int intervalCount();
abstract class Builder
{
protected final OnDiskIndexBuilder.Mode mode;
protected final AbstractType<?> comparator;
protected ByteBuffer min, max;
protected Builder(OnDiskIndexBuilder.Mode mode, AbstractType<?> comparator)
{
this.mode = mode;
this.comparator = comparator;
}
public final void add(SSTableIndex index)
{
addIndex(index);
min = min == null || comparator.compare(min, index.minTerm()) > 0 ? index.minTerm() : min;
max = max == null || comparator.compare(max, index.maxTerm()) < 0 ? index.maxTerm() : max;
}
protected abstract void addIndex(SSTableIndex index);
public abstract TermTree build();
}
}

View File

@ -0,0 +1,104 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.conf.view;
import java.nio.ByteBuffer;
import java.util.*;
import org.apache.cassandra.index.sasi.SSTableIndex;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.AsciiType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.utils.Interval;
import org.apache.cassandra.utils.IntervalTree;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
public class View implements Iterable<SSTableIndex>
{
private final Map<Descriptor, SSTableIndex> view;
private final TermTree termTree;
private final IntervalTree<ByteBuffer, SSTableIndex, Interval<ByteBuffer, SSTableIndex>> keyIntervalTree;
public View(ColumnIndex index, Set<SSTableIndex> indexes)
{
this(index, Collections.<SSTableIndex>emptyList(), Collections.<SSTableReader>emptyList(), indexes);
}
public View(ColumnIndex index,
Collection<SSTableIndex> currentView,
Collection<SSTableReader> oldSSTables,
Set<SSTableIndex> newIndexes)
{
Map<Descriptor, SSTableIndex> newView = new HashMap<>();
AbstractType<?> validator = index.getValidator();
TermTree.Builder termTreeBuilder = (validator instanceof AsciiType || validator instanceof UTF8Type)
? new PrefixTermTree.Builder(index.getMode().mode, validator)
: new RangeTermTree.Builder(index.getMode().mode, validator);
List<Interval<ByteBuffer, SSTableIndex>> keyIntervals = new ArrayList<>();
for (SSTableIndex sstableIndex : Iterables.concat(currentView, newIndexes))
{
SSTableReader sstable = sstableIndex.getSSTable();
if (oldSSTables.contains(sstable) || sstable.isMarkedCompacted() || newView.containsKey(sstable.descriptor))
{
sstableIndex.release();
continue;
}
newView.put(sstable.descriptor, sstableIndex);
termTreeBuilder.add(sstableIndex);
keyIntervals.add(Interval.create(sstableIndex.minKey(), sstableIndex.maxKey(), sstableIndex));
}
this.view = newView;
this.termTree = termTreeBuilder.build();
this.keyIntervalTree = IntervalTree.build(keyIntervals);
if (keyIntervalTree.intervalCount() != termTree.intervalCount())
throw new IllegalStateException(String.format("mismatched sizes for intervals tree for keys vs terms: %d != %d", keyIntervalTree.intervalCount(), termTree.intervalCount()));
}
public Set<SSTableIndex> match(final Set<SSTableReader> scope, Expression expression)
{
return Sets.filter(termTree.search(expression), index -> scope.contains(index.getSSTable()));
}
public List<SSTableIndex> match(ByteBuffer minKey, ByteBuffer maxKey)
{
return keyIntervalTree.search(Interval.create(minKey, maxKey, (SSTableIndex) null));
}
public Iterator<SSTableIndex> iterator()
{
return view.values().iterator();
}
public Collection<SSTableIndex> getIndexes()
{
return view.values();
}
}

View File

@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
/**
* Object descriptor for SSTableAttachedSecondaryIndex files. Similar to, and based upon, the sstable descriptor.
*/
public class Descriptor
{
public static final String VERSION_AA = "aa";
public static final String VERSION_AB = "ab";
public static final String CURRENT_VERSION = VERSION_AB;
public static final Descriptor CURRENT = new Descriptor(CURRENT_VERSION);
public static class Version
{
public final String version;
public Version(String version)
{
this.version = version;
}
public String toString()
{
return version;
}
}
public final Version version;
public Descriptor(String v)
{
this.version = new Version(v);
}
}

View File

@ -0,0 +1,142 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
import java.nio.ByteBuffer;
import org.apache.cassandra.index.sasi.Term;
import org.apache.cassandra.index.sasi.utils.MappedBuffer;
import org.apache.cassandra.db.marshal.AbstractType;
public abstract class OnDiskBlock<T extends Term>
{
public enum BlockType
{
POINTER, DATA
}
// this contains offsets of the terms and term data
protected final MappedBuffer blockIndex;
protected final int blockIndexSize;
protected final boolean hasCombinedIndex;
protected final TokenTree combinedIndex;
public OnDiskBlock(Descriptor descriptor, MappedBuffer block, BlockType blockType)
{
blockIndex = block;
if (blockType == BlockType.POINTER)
{
hasCombinedIndex = false;
combinedIndex = null;
blockIndexSize = block.getInt() << 1; // num terms * sizeof(short)
return;
}
long blockOffset = block.position();
int combinedIndexOffset = block.getInt(blockOffset + OnDiskIndexBuilder.BLOCK_SIZE);
hasCombinedIndex = (combinedIndexOffset >= 0);
long blockIndexOffset = blockOffset + OnDiskIndexBuilder.BLOCK_SIZE + 4 + combinedIndexOffset;
combinedIndex = hasCombinedIndex ? new TokenTree(descriptor, blockIndex.duplicate().position(blockIndexOffset)) : null;
blockIndexSize = block.getInt() * 2;
}
public SearchResult<T> search(AbstractType<?> comparator, ByteBuffer query)
{
int cmp = -1, start = 0, end = termCount() - 1, middle = 0;
T element = null;
while (start <= end)
{
middle = start + ((end - start) >> 1);
element = getTerm(middle);
cmp = element.compareTo(comparator, query);
if (cmp == 0)
return new SearchResult<>(element, cmp, middle);
else if (cmp < 0)
start = middle + 1;
else
end = middle - 1;
}
return new SearchResult<>(element, cmp, middle);
}
protected T getTerm(int index)
{
MappedBuffer dup = blockIndex.duplicate();
long startsAt = getTermPosition(index);
if (termCount() - 1 == index) // last element
dup.position(startsAt);
else
dup.position(startsAt).limit(getTermPosition(index + 1));
return cast(dup);
}
protected long getTermPosition(int idx)
{
return getTermPosition(blockIndex, idx, blockIndexSize);
}
protected int termCount()
{
return blockIndexSize >> 1;
}
protected abstract T cast(MappedBuffer data);
static long getTermPosition(MappedBuffer data, int idx, int indexSize)
{
idx <<= 1;
assert idx < indexSize;
return data.position() + indexSize + data.getShort(data.position() + idx);
}
public TokenTree getBlockIndex()
{
return combinedIndex;
}
public int minOffset(OnDiskIndex.IteratorOrder order)
{
return order == OnDiskIndex.IteratorOrder.DESC ? 0 : termCount() - 1;
}
public int maxOffset(OnDiskIndex.IteratorOrder order)
{
return minOffset(order) == 0 ? termCount() - 1 : 0;
}
public static class SearchResult<T>
{
public final T result;
public final int index, cmp;
public SearchResult(T result, int cmp, int index)
{
this.result = result;
this.index = index;
this.cmp = cmp;
}
}
}

View File

@ -0,0 +1,773 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.Term;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.plan.Expression.Op;
import org.apache.cassandra.index.sasi.utils.MappedBuffer;
import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
import org.apache.cassandra.index.sasi.utils.AbstractIterator;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.FSReadError;
import org.apache.cassandra.io.util.ChannelProxy;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
import com.google.common.base.Function;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import static org.apache.cassandra.index.sasi.disk.OnDiskBlock.SearchResult;
public class OnDiskIndex implements Iterable<OnDiskIndex.DataTerm>, Closeable
{
public enum IteratorOrder
{
DESC(1), ASC(-1);
public final int step;
IteratorOrder(int step)
{
this.step = step;
}
public int startAt(OnDiskBlock<DataTerm> block, Expression e)
{
switch (this)
{
case DESC:
return e.lower == null
? 0
: startAt(block.search(e.validator, e.lower.value), e.lower.inclusive);
case ASC:
return e.upper == null
? block.termCount() - 1
: startAt(block.search(e.validator, e.upper.value), e.upper.inclusive);
default:
throw new IllegalArgumentException("Unknown order: " + this);
}
}
public int startAt(SearchResult<DataTerm> found, boolean inclusive)
{
switch (this)
{
case DESC:
if (found.cmp < 0)
return found.index + 1;
return inclusive || found.cmp != 0 ? found.index : found.index + 1;
case ASC:
if (found.cmp < 0) // search term was bigger then whole data set
return found.index;
return inclusive && (found.cmp == 0 || found.cmp < 0) ? found.index : found.index - 1;
default:
throw new IllegalArgumentException("Unknown order: " + this);
}
}
}
public final Descriptor descriptor;
protected final OnDiskIndexBuilder.Mode mode;
protected final OnDiskIndexBuilder.TermSize termSize;
protected final AbstractType<?> comparator;
protected final MappedBuffer indexFile;
protected final long indexSize;
protected final Function<Long, DecoratedKey> keyFetcher;
protected final String indexPath;
protected final PointerLevel[] levels;
protected final DataLevel dataLevel;
protected final ByteBuffer minTerm, maxTerm, minKey, maxKey;
public OnDiskIndex(File index, AbstractType<?> cmp, Function<Long, DecoratedKey> keyReader)
{
keyFetcher = keyReader;
comparator = cmp;
indexPath = index.getAbsolutePath();
RandomAccessFile backingFile = null;
try
{
backingFile = new RandomAccessFile(index, "r");
descriptor = new Descriptor(backingFile.readUTF());
termSize = OnDiskIndexBuilder.TermSize.of(backingFile.readShort());
minTerm = ByteBufferUtil.readWithShortLength(backingFile);
maxTerm = ByteBufferUtil.readWithShortLength(backingFile);
minKey = ByteBufferUtil.readWithShortLength(backingFile);
maxKey = ByteBufferUtil.readWithShortLength(backingFile);
mode = OnDiskIndexBuilder.Mode.mode(backingFile.readUTF());
indexSize = backingFile.length();
indexFile = new MappedBuffer(new ChannelProxy(indexPath, backingFile.getChannel()));
// start of the levels
indexFile.position(indexFile.getLong(indexSize - 8));
int numLevels = indexFile.getInt();
levels = new PointerLevel[numLevels];
for (int i = 0; i < levels.length; i++)
{
int blockCount = indexFile.getInt();
levels[i] = new PointerLevel(indexFile.position(), blockCount);
indexFile.position(indexFile.position() + blockCount * 8);
}
int blockCount = indexFile.getInt();
dataLevel = new DataLevel(indexFile.position(), blockCount);
}
catch (IOException e)
{
throw new FSReadError(e, index);
}
finally
{
FileUtils.closeQuietly(backingFile);
}
}
public ByteBuffer minTerm()
{
return minTerm;
}
public ByteBuffer maxTerm()
{
return maxTerm;
}
public ByteBuffer minKey()
{
return minKey;
}
public ByteBuffer maxKey()
{
return maxKey;
}
public DataTerm min()
{
return dataLevel.getBlock(0).getTerm(0);
}
public DataTerm max()
{
DataBlock block = dataLevel.getBlock(dataLevel.blockCount - 1);
return block.getTerm(block.termCount() - 1);
}
/**
* Search for rows which match all of the terms inside the given expression in the index file.
*
* @param exp The expression to use for the query.
*
* @return Iterator which contains rows for all of the terms from the given range.
*/
public RangeIterator<Long, Token> search(Expression exp)
{
// convert single NOT_EQ to range with exclusion
final Expression expression = (exp.getOp() != Op.NOT_EQ)
? exp
: new Expression(exp).setOp(Op.RANGE)
.setLower(new Expression.Bound(minTerm, true))
.setUpper(new Expression.Bound(maxTerm, true))
.addExclusion(exp.lower.value);
List<ByteBuffer> exclusions = new ArrayList<>(expression.exclusions.size());
Iterables.addAll(exclusions, expression.exclusions.stream().filter(exclusion -> {
// accept only exclusions which are in the bounds of lower/upper
return !(expression.lower != null && comparator.compare(exclusion, expression.lower.value) < 0)
&& !(expression.upper != null && comparator.compare(exclusion, expression.upper.value) > 0);
}).collect(Collectors.toList()));
Collections.sort(exclusions, comparator);
if (exclusions.size() == 0)
return searchRange(expression);
List<Expression> ranges = new ArrayList<>(exclusions.size());
// calculate range splits based on the sorted exclusions
Iterator<ByteBuffer> exclusionsIterator = exclusions.iterator();
Expression.Bound min = expression.lower, max = null;
while (exclusionsIterator.hasNext())
{
max = new Expression.Bound(exclusionsIterator.next(), false);
ranges.add(new Expression(expression).setOp(Op.RANGE).setLower(min).setUpper(max));
min = max;
}
assert max != null;
ranges.add(new Expression(expression).setOp(Op.RANGE).setLower(max).setUpper(expression.upper));
RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
for (Expression e : ranges)
{
RangeIterator<Long, Token> range = searchRange(e);
if (range != null)
builder.add(range);
}
return builder.build();
}
private RangeIterator<Long, Token> searchRange(Expression range)
{
Expression.Bound lower = range.lower;
Expression.Bound upper = range.upper;
int lowerBlock = lower == null ? 0 : getDataBlock(lower.value);
int upperBlock = upper == null
? dataLevel.blockCount - 1
// optimization so we don't have to fetch upperBlock when query has lower == upper
: (lower != null && comparator.compare(lower.value, upper.value) == 0) ? lowerBlock : getDataBlock(upper.value);
return (mode != OnDiskIndexBuilder.Mode.SPARSE || lowerBlock == upperBlock || upperBlock - lowerBlock <= 1)
? searchPoint(lowerBlock, range)
: searchRange(lowerBlock, lower, upperBlock, upper);
}
private RangeIterator<Long, Token> searchRange(int lowerBlock, Expression.Bound lower, int upperBlock, Expression.Bound upper)
{
// if lower is at the beginning of the block that means we can just do a single iterator per block
SearchResult<DataTerm> lowerPosition = (lower == null) ? null : searchIndex(lower.value, lowerBlock);
SearchResult<DataTerm> upperPosition = (upper == null) ? null : searchIndex(upper.value, upperBlock);
RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
// optimistically assume that first and last blocks are full block reads, saves at least 3 'else' conditions
int firstFullBlockIdx = lowerBlock, lastFullBlockIdx = upperBlock;
// 'lower' doesn't cover the whole block so we need to do a partial iteration
// Two reasons why that can happen:
// - 'lower' is not the first element of the block
// - 'lower' is first element but it's not inclusive in the query
if (lowerPosition != null && (lowerPosition.index > 0 || !lower.inclusive))
{
DataBlock block = dataLevel.getBlock(lowerBlock);
int start = (lower.inclusive || lowerPosition.cmp != 0) ? lowerPosition.index : lowerPosition.index + 1;
builder.add(block.getRange(start, block.termCount()));
firstFullBlockIdx = lowerBlock + 1;
}
if (upperPosition != null)
{
DataBlock block = dataLevel.getBlock(upperBlock);
int lastIndex = block.termCount() - 1;
// The save as with 'lower' but here we need to check if the upper is the last element of the block,
// which means that we only have to get individual results if:
// - if it *is not* the last element, or
// - it *is* but shouldn't be included (dictated by upperInclusive)
if (upperPosition.index != lastIndex || !upper.inclusive)
{
int end = (upperPosition.cmp < 0 || (upperPosition.cmp == 0 && upper.inclusive))
? upperPosition.index + 1 : upperPosition.index;
builder.add(block.getRange(0, end));
lastFullBlockIdx = upperBlock - 1;
}
}
int totalSuperBlocks = (lastFullBlockIdx - firstFullBlockIdx) / OnDiskIndexBuilder.SUPER_BLOCK_SIZE;
// if there are no super-blocks, we can simply read all of the block iterators in sequence
if (totalSuperBlocks == 0)
{
for (int i = firstFullBlockIdx; i <= lastFullBlockIdx; i++)
builder.add(dataLevel.getBlock(i).getBlockIndex().iterator(keyFetcher));
return builder.build();
}
// first get all of the blocks which are aligned before the first super-block in the sequence,
// e.g. if the block range was (1, 9) and super-block-size = 4, we need to read 1, 2, 3, 4 - 7 is covered by
// super-block, 8, 9 is a remainder.
int superBlockAlignedStart = firstFullBlockIdx == 0 ? 0 : (int) FBUtilities.align(firstFullBlockIdx, OnDiskIndexBuilder.SUPER_BLOCK_SIZE);
for (int blockIdx = firstFullBlockIdx; blockIdx < Math.min(superBlockAlignedStart, lastFullBlockIdx); blockIdx++)
builder.add(getBlockIterator(blockIdx));
// now read all of the super-blocks matched by the request, from the previous comment
// it's a block with index 1 (which covers everything from 4 to 7)
int superBlockIdx = superBlockAlignedStart / OnDiskIndexBuilder.SUPER_BLOCK_SIZE;
for (int offset = 0; offset < totalSuperBlocks - 1; offset++)
builder.add(dataLevel.getSuperBlock(superBlockIdx++).iterator());
// now it's time for a remainder read, again from the previous example it's 8, 9 because
// we have over-shot previous block but didn't request enough to cover next super-block.
int lastCoveredBlock = superBlockIdx * OnDiskIndexBuilder.SUPER_BLOCK_SIZE;
for (int offset = 0; offset <= (lastFullBlockIdx - lastCoveredBlock); offset++)
builder.add(getBlockIterator(lastCoveredBlock + offset));
return builder.build();
}
private RangeIterator<Long, Token> searchPoint(int lowerBlock, Expression expression)
{
Iterator<DataTerm> terms = new TermIterator(lowerBlock, expression, IteratorOrder.DESC);
RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
while (terms.hasNext())
{
try
{
builder.add(terms.next().getTokens());
}
finally
{
expression.checkpoint();
}
}
return builder.build();
}
private RangeIterator<Long, Token> getBlockIterator(int blockIdx)
{
DataBlock block = dataLevel.getBlock(blockIdx);
return (block.hasCombinedIndex)
? block.getBlockIndex().iterator(keyFetcher)
: block.getRange(0, block.termCount());
}
public Iterator<DataTerm> iteratorAt(ByteBuffer query, IteratorOrder order, boolean inclusive)
{
Expression e = new Expression("", comparator);
Expression.Bound bound = new Expression.Bound(query, inclusive);
switch (order)
{
case DESC:
e.setLower(bound);
break;
case ASC:
e.setUpper(bound);
break;
default:
throw new IllegalArgumentException("Unknown order: " + order);
}
return new TermIterator(levels.length == 0 ? 0 : getBlockIdx(findPointer(query), query), e, order);
}
private int getDataBlock(ByteBuffer query)
{
return levels.length == 0 ? 0 : getBlockIdx(findPointer(query), query);
}
public Iterator<DataTerm> iterator()
{
return new TermIterator(0, new Expression("", comparator), IteratorOrder.DESC);
}
public void close() throws IOException
{
FileUtils.closeQuietly(indexFile);
}
private PointerTerm findPointer(ByteBuffer query)
{
PointerTerm ptr = null;
for (PointerLevel level : levels)
{
if ((ptr = level.getPointer(ptr, query)) == null)
return null;
}
return ptr;
}
private SearchResult<DataTerm> searchIndex(ByteBuffer query, int blockIdx)
{
return dataLevel.getBlock(blockIdx).search(comparator, query);
}
private int getBlockIdx(PointerTerm ptr, ByteBuffer query)
{
int blockIdx = 0;
if (ptr != null)
{
int cmp = ptr.compareTo(comparator, query);
blockIdx = (cmp == 0 || cmp > 0) ? ptr.getBlock() : ptr.getBlock() + 1;
}
return blockIdx;
}
protected class PointerLevel extends Level<PointerBlock>
{
public PointerLevel(long offset, int count)
{
super(offset, count);
}
public PointerTerm getPointer(PointerTerm parent, ByteBuffer query)
{
return getBlock(getBlockIdx(parent, query)).search(comparator, query).result;
}
protected PointerBlock cast(MappedBuffer block)
{
return new PointerBlock(block);
}
}
protected class DataLevel extends Level<DataBlock>
{
protected final int superBlockCnt;
protected final long superBlocksOffset;
public DataLevel(long offset, int count)
{
super(offset, count);
long baseOffset = blockOffsets + blockCount * 8;
superBlockCnt = indexFile.getInt(baseOffset);
superBlocksOffset = baseOffset + 4;
}
protected DataBlock cast(MappedBuffer block)
{
return new DataBlock(block);
}
public OnDiskSuperBlock getSuperBlock(int idx)
{
assert idx < superBlockCnt : String.format("requested index %d is greater than super block count %d", idx, superBlockCnt);
long blockOffset = indexFile.getLong(superBlocksOffset + idx * 8);
return new OnDiskSuperBlock(indexFile.duplicate().position(blockOffset));
}
}
protected class OnDiskSuperBlock
{
private final TokenTree tokenTree;
public OnDiskSuperBlock(MappedBuffer buffer)
{
tokenTree = new TokenTree(descriptor, buffer);
}
public RangeIterator<Long, Token> iterator()
{
return tokenTree.iterator(keyFetcher);
}
}
protected abstract class Level<T extends OnDiskBlock>
{
protected final long blockOffsets;
protected final int blockCount;
public Level(long offsets, int count)
{
this.blockOffsets = offsets;
this.blockCount = count;
}
public T getBlock(int idx) throws FSReadError
{
assert idx >= 0 && idx < blockCount;
// calculate block offset and move there
// (long is intentional, we'll just need mmap implementation which supports long positions)
long blockOffset = indexFile.getLong(blockOffsets + idx * 8);
return cast(indexFile.duplicate().position(blockOffset));
}
protected abstract T cast(MappedBuffer block);
}
protected class DataBlock extends OnDiskBlock<DataTerm>
{
public DataBlock(MappedBuffer data)
{
super(descriptor, data, BlockType.DATA);
}
protected DataTerm cast(MappedBuffer data)
{
return new DataTerm(data, termSize, getBlockIndex());
}
public RangeIterator<Long, Token> getRange(int start, int end)
{
RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
NavigableMap<Long, Token> sparse = new TreeMap<>();
for (int i = start; i < end; i++)
{
DataTerm term = getTerm(i);
if (term.isSparse())
{
NavigableMap<Long, Token> tokens = term.getSparseTokens();
for (Map.Entry<Long, Token> t : tokens.entrySet())
{
Token token = sparse.get(t.getKey());
if (token == null)
sparse.put(t.getKey(), t.getValue());
else
token.merge(t.getValue());
}
}
else
{
builder.add(term.getTokens());
}
}
PrefetchedTokensIterator prefetched = sparse.isEmpty() ? null : new PrefetchedTokensIterator(sparse);
if (builder.rangeCount() == 0)
return prefetched;
builder.add(prefetched);
return builder.build();
}
}
protected class PointerBlock extends OnDiskBlock<PointerTerm>
{
public PointerBlock(MappedBuffer block)
{
super(descriptor, block, BlockType.POINTER);
}
protected PointerTerm cast(MappedBuffer data)
{
return new PointerTerm(data, termSize);
}
}
public class DataTerm extends Term implements Comparable<DataTerm>
{
private final TokenTree perBlockIndex;
protected DataTerm(MappedBuffer content, OnDiskIndexBuilder.TermSize size, TokenTree perBlockIndex)
{
super(content, size);
this.perBlockIndex = perBlockIndex;
}
public RangeIterator<Long, Token> getTokens()
{
final long blockEnd = FBUtilities.align(content.position(), OnDiskIndexBuilder.BLOCK_SIZE);
if (isSparse())
return new PrefetchedTokensIterator(getSparseTokens());
long offset = blockEnd + 4 + content.getInt(getDataOffset() + 1);
return new TokenTree(descriptor, indexFile.duplicate().position(offset)).iterator(keyFetcher);
}
public boolean isSparse()
{
return content.get(getDataOffset()) > 0;
}
public NavigableMap<Long, Token> getSparseTokens()
{
long ptrOffset = getDataOffset();
byte size = content.get(ptrOffset);
assert size > 0;
NavigableMap<Long, Token> individualTokens = new TreeMap<>();
for (int i = 0; i < size; i++)
{
Token token = perBlockIndex.get(content.getLong(ptrOffset + 1 + (8 * i)), keyFetcher);
assert token != null;
individualTokens.put(token.get(), token);
}
return individualTokens;
}
public int compareTo(DataTerm other)
{
return other == null ? 1 : compareTo(comparator, other.getTerm());
}
}
protected static class PointerTerm extends Term
{
public PointerTerm(MappedBuffer content, OnDiskIndexBuilder.TermSize size)
{
super(content, size);
}
public int getBlock()
{
return content.getInt(getDataOffset());
}
}
private static class PrefetchedTokensIterator extends RangeIterator<Long, Token>
{
private final NavigableMap<Long, Token> tokens;
private PeekingIterator<Token> currentIterator;
public PrefetchedTokensIterator(NavigableMap<Long, Token> tokens)
{
super(tokens.firstKey(), tokens.lastKey(), tokens.size());
this.tokens = tokens;
this.currentIterator = Iterators.peekingIterator(tokens.values().iterator());
}
protected Token computeNext()
{
return currentIterator != null && currentIterator.hasNext()
? currentIterator.next()
: endOfData();
}
protected void performSkipTo(Long nextToken)
{
currentIterator = Iterators.peekingIterator(tokens.tailMap(nextToken, true).values().iterator());
}
public void close() throws IOException
{
endOfData();
}
}
public AbstractType<?> getComparator()
{
return comparator;
}
public String getIndexPath()
{
return indexPath;
}
private class TermIterator extends AbstractIterator<DataTerm>
{
private final Expression e;
private final IteratorOrder order;
protected OnDiskBlock<DataTerm> currentBlock;
protected int blockIndex, offset;
private boolean checkLower = true, checkUpper = true;
public TermIterator(int startBlock, Expression expression, IteratorOrder order)
{
this.e = expression;
this.order = order;
this.blockIndex = startBlock;
nextBlock();
}
protected DataTerm computeNext()
{
for (;;)
{
if (currentBlock == null)
return endOfData();
if (offset >= 0 && offset < currentBlock.termCount())
{
DataTerm currentTerm = currentBlock.getTerm(nextOffset());
if (checkLower && !e.isLowerSatisfiedBy(currentTerm))
continue;
// flip the flag right on the first bounds match
// to avoid expensive comparisons
checkLower = false;
if (checkUpper && !e.isUpperSatisfiedBy(currentTerm))
return endOfData();
return currentTerm;
}
nextBlock();
}
}
protected void nextBlock()
{
currentBlock = null;
if (blockIndex < 0 || blockIndex >= dataLevel.blockCount)
return;
currentBlock = dataLevel.getBlock(nextBlockIndex());
offset = checkLower ? order.startAt(currentBlock, e) : currentBlock.minOffset(order);
// let's check the last term of the new block right away
// if expression's upper bound is satisfied by it such means that we can avoid
// doing any expensive upper bound checks for that block.
checkUpper = e.hasUpper() && !e.isUpperSatisfiedBy(currentBlock.getTerm(currentBlock.maxOffset(order)));
}
protected int nextBlockIndex()
{
int current = blockIndex;
blockIndex += order.step;
return current;
}
protected int nextOffset()
{
int current = offset;
offset += order.step;
return current;
}
}
}

View File

@ -0,0 +1,627 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.sa.IntegralSA;
import org.apache.cassandra.index.sasi.sa.SA;
import org.apache.cassandra.index.sasi.sa.TermIterator;
import org.apache.cassandra.index.sasi.sa.SuffixSA;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.io.FSWriteError;
import org.apache.cassandra.io.compress.BufferType;
import org.apache.cassandra.io.util.*;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
import com.carrotsearch.hppc.LongArrayList;
import com.carrotsearch.hppc.LongSet;
import com.carrotsearch.hppc.ShortArrayList;
import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OnDiskIndexBuilder
{
private static final Logger logger = LoggerFactory.getLogger(OnDiskIndexBuilder.class);
public enum Mode
{
PREFIX, CONTAINS, SPARSE;
public static Mode mode(String mode)
{
return Mode.valueOf(mode.toUpperCase());
}
}
public enum TermSize
{
INT(4), LONG(8), UUID(16), VARIABLE(-1);
public final int size;
TermSize(int size)
{
this.size = size;
}
public boolean isConstant()
{
return this != VARIABLE;
}
public static TermSize of(int size)
{
switch (size)
{
case -1:
return VARIABLE;
case 4:
return INT;
case 8:
return LONG;
case 16:
return UUID;
default:
throw new IllegalStateException("unknown state: " + size);
}
}
public static TermSize sizeOf(AbstractType<?> comparator)
{
if (comparator instanceof Int32Type || comparator instanceof FloatType)
return INT;
if (comparator instanceof LongType || comparator instanceof DoubleType
|| comparator instanceof TimestampType || comparator instanceof DateType)
return LONG;
if (comparator instanceof TimeUUIDType || comparator instanceof UUIDType)
return UUID;
return VARIABLE;
}
}
public static final int BLOCK_SIZE = 4096;
public static final int MAX_TERM_SIZE = 1024;
public static final int SUPER_BLOCK_SIZE = 64;
private final List<MutableLevel<InMemoryPointerTerm>> levels = new ArrayList<>();
private MutableLevel<InMemoryDataTerm> dataLevel;
private final TermSize termSize;
private final AbstractType<?> keyComparator, termComparator;
private final Map<ByteBuffer, TokenTreeBuilder> terms;
private final Mode mode;
private ByteBuffer minKey, maxKey;
private long estimatedBytes;
public OnDiskIndexBuilder(AbstractType<?> keyComparator, AbstractType<?> comparator, Mode mode)
{
this.keyComparator = keyComparator;
this.termComparator = comparator;
this.terms = new HashMap<>();
this.termSize = TermSize.sizeOf(comparator);
this.mode = mode;
}
public OnDiskIndexBuilder add(ByteBuffer term, DecoratedKey key, long keyPosition)
{
if (term.remaining() >= MAX_TERM_SIZE)
{
logger.error("Rejecting value (value size {}, maximum size {} bytes).", term.remaining(), Short.MAX_VALUE);
return this;
}
TokenTreeBuilder tokens = terms.get(term);
if (tokens == null)
{
terms.put(term, (tokens = new TokenTreeBuilder()));
// on-heap size estimates from jol
// 64 bytes for TTB + 48 bytes for TreeMap in TTB + size bytes for the term (map key)
estimatedBytes += 64 + 48 + term.remaining();
}
tokens.add((Long) key.getToken().getTokenValue(), keyPosition);
// calculate key range (based on actual key values) for current index
minKey = (minKey == null || keyComparator.compare(minKey, key.getKey()) > 0) ? key.getKey() : minKey;
maxKey = (maxKey == null || keyComparator.compare(maxKey, key.getKey()) < 0) ? key.getKey() : maxKey;
// 60 ((boolean(1)*4) + (long(8)*4) + 24) bytes for the LongOpenHashSet created when the keyPosition was added
// + 40 bytes for the TreeMap.Entry + 8 bytes for the token (key).
// in the case of hash collision for the token we may overestimate but this is extremely rare
estimatedBytes += 60 + 40 + 8;
return this;
}
public long estimatedMemoryUse()
{
return estimatedBytes;
}
private void addTerm(InMemoryDataTerm term, SequentialWriter out) throws IOException
{
InMemoryPointerTerm ptr = dataLevel.add(term);
if (ptr == null)
return;
int levelIdx = 0;
for (;;)
{
MutableLevel<InMemoryPointerTerm> level = getIndexLevel(levelIdx++, out);
if ((ptr = level.add(ptr)) == null)
break;
}
}
public boolean isEmpty()
{
return terms.isEmpty();
}
public void finish(Pair<ByteBuffer, ByteBuffer> range, File file, TermIterator terms)
{
finish(Descriptor.CURRENT, range, file, terms);
}
/**
* Finishes up index building process by creating/populating index file.
*
* @param indexFile The file to write index contents to.
*
* @return true if index was written successfully, false otherwise (e.g. if index was empty).
*
* @throws FSWriteError on I/O error.
*/
public boolean finish(File indexFile) throws FSWriteError
{
return finish(Descriptor.CURRENT, indexFile);
}
@VisibleForTesting
protected boolean finish(Descriptor descriptor, File file) throws FSWriteError
{
// no terms means there is nothing to build
if (terms.isEmpty())
return false;
// split terms into suffixes only if it's text, otherwise (even if CONTAINS is set) use terms in original form
SA sa = ((termComparator instanceof UTF8Type || termComparator instanceof AsciiType) && mode == Mode.CONTAINS)
? new SuffixSA(termComparator, mode) : new IntegralSA(termComparator, mode);
for (Map.Entry<ByteBuffer, TokenTreeBuilder> term : terms.entrySet())
sa.add(term.getKey(), term.getValue());
finish(descriptor, Pair.create(minKey, maxKey), file, sa.finish());
return true;
}
protected void finish(Descriptor descriptor, Pair<ByteBuffer, ByteBuffer> range, File file, TermIterator terms)
{
SequentialWriter out = null;
try
{
out = new SequentialWriter(file, BLOCK_SIZE, BufferType.ON_HEAP);
out.writeUTF(descriptor.version.toString());
out.writeShort(termSize.size);
// min, max term (useful to find initial scan range from search expressions)
ByteBufferUtil.writeWithShortLength(terms.minTerm(), out);
ByteBufferUtil.writeWithShortLength(terms.maxTerm(), out);
// min, max keys covered by index (useful when searching across multiple indexes)
ByteBufferUtil.writeWithShortLength(range.left, out);
ByteBufferUtil.writeWithShortLength(range.right, out);
out.writeUTF(mode.toString());
out.skipBytes((int) (BLOCK_SIZE - out.position()));
dataLevel = mode == Mode.SPARSE ? new DataBuilderLevel(out, new MutableDataBlock(mode))
: new MutableLevel<>(out, new MutableDataBlock(mode));
while (terms.hasNext())
{
Pair<ByteBuffer, TokenTreeBuilder> term = terms.next();
addTerm(new InMemoryDataTerm(term.left, term.right), out);
}
dataLevel.finalFlush();
for (MutableLevel l : levels)
l.flush(); // flush all of the buffers
// and finally write levels index
final long levelIndexPosition = out.position();
out.writeInt(levels.size());
for (int i = levels.size() - 1; i >= 0; i--)
levels.get(i).flushMetadata();
dataLevel.flushMetadata();
out.writeLong(levelIndexPosition);
// sync contents of the output and disk,
// since it's not done implicitly on close
out.sync();
}
catch (IOException e)
{
throw new FSWriteError(e, file);
}
finally
{
FileUtils.closeQuietly(out);
}
}
private MutableLevel<InMemoryPointerTerm> getIndexLevel(int idx, SequentialWriter out)
{
if (levels.size() == 0)
levels.add(new MutableLevel<>(out, new MutableBlock<>()));
if (levels.size() - 1 < idx)
{
int toAdd = idx - (levels.size() - 1);
for (int i = 0; i < toAdd; i++)
levels.add(new MutableLevel<>(out, new MutableBlock<>()));
}
return levels.get(idx);
}
protected static void alignToBlock(SequentialWriter out) throws IOException
{
long endOfBlock = out.position();
if ((endOfBlock & (BLOCK_SIZE - 1)) != 0) // align on the block boundary if needed
out.skipBytes((int) (FBUtilities.align(endOfBlock, BLOCK_SIZE) - endOfBlock));
}
private class InMemoryTerm
{
protected final ByteBuffer term;
public InMemoryTerm(ByteBuffer term)
{
this.term = term;
}
public int serializedSize()
{
return (termSize.isConstant() ? 0 : 2) + term.remaining();
}
public void serialize(DataOutputPlus out) throws IOException
{
if (termSize.isConstant())
out.write(term);
else
ByteBufferUtil.writeWithShortLength(term, out);
}
}
private class InMemoryPointerTerm extends InMemoryTerm
{
protected final int blockCnt;
public InMemoryPointerTerm(ByteBuffer term, int blockCnt)
{
super(term);
this.blockCnt = blockCnt;
}
public int serializedSize()
{
return super.serializedSize() + 4;
}
public void serialize(DataOutputPlus out) throws IOException
{
super.serialize(out);
out.writeInt(blockCnt);
}
}
private class InMemoryDataTerm extends InMemoryTerm
{
private final TokenTreeBuilder keys;
public InMemoryDataTerm(ByteBuffer term, TokenTreeBuilder keys)
{
super(term);
this.keys = keys;
}
}
private class MutableLevel<T extends InMemoryTerm>
{
private final LongArrayList blockOffsets = new LongArrayList();
protected final SequentialWriter out;
private final MutableBlock<T> inProcessBlock;
private InMemoryPointerTerm lastTerm;
public MutableLevel(SequentialWriter out, MutableBlock<T> block)
{
this.out = out;
this.inProcessBlock = block;
}
/**
* @return If we flushed a block, return the last term of that block; else, null.
*/
public InMemoryPointerTerm add(T term) throws IOException
{
InMemoryPointerTerm toPromote = null;
if (!inProcessBlock.hasSpaceFor(term))
{
flush();
toPromote = lastTerm;
}
inProcessBlock.add(term);
lastTerm = new InMemoryPointerTerm(term.term, blockOffsets.size());
return toPromote;
}
public void flush() throws IOException
{
blockOffsets.add(out.position());
inProcessBlock.flushAndClear(out);
}
public void finalFlush() throws IOException
{
flush();
}
public void flushMetadata() throws IOException
{
flushMetadata(blockOffsets);
}
protected void flushMetadata(LongArrayList longArrayList) throws IOException
{
out.writeInt(longArrayList.size());
for (int i = 0; i < longArrayList.size(); i++)
out.writeLong(longArrayList.get(i));
}
}
/** builds standard data blocks and super blocks, as well */
private class DataBuilderLevel extends MutableLevel<InMemoryDataTerm>
{
private final LongArrayList superBlockOffsets = new LongArrayList();
/** count of regular data blocks written since current super block was init'd */
private int dataBlocksCnt;
private TokenTreeBuilder superBlockTree;
public DataBuilderLevel(SequentialWriter out, MutableBlock<InMemoryDataTerm> block)
{
super(out, block);
superBlockTree = new TokenTreeBuilder();
}
public InMemoryPointerTerm add(InMemoryDataTerm term) throws IOException
{
InMemoryPointerTerm ptr = super.add(term);
if (ptr != null)
{
dataBlocksCnt++;
flushSuperBlock(false);
}
superBlockTree.add(term.keys.getTokens());
return ptr;
}
public void flushSuperBlock(boolean force) throws IOException
{
if (dataBlocksCnt == SUPER_BLOCK_SIZE || (force && !superBlockTree.getTokens().isEmpty()))
{
superBlockOffsets.add(out.position());
superBlockTree.finish().write(out);
alignToBlock(out);
dataBlocksCnt = 0;
superBlockTree = new TokenTreeBuilder();
}
}
public void finalFlush() throws IOException
{
super.flush();
flushSuperBlock(true);
}
public void flushMetadata() throws IOException
{
super.flushMetadata();
flushMetadata(superBlockOffsets);
}
}
private static class MutableBlock<T extends InMemoryTerm>
{
protected final DataOutputBufferFixed buffer;
protected final ShortArrayList offsets;
public MutableBlock()
{
buffer = new DataOutputBufferFixed(BLOCK_SIZE);
offsets = new ShortArrayList();
}
public final void add(T term) throws IOException
{
offsets.add((short) buffer.position());
addInternal(term);
}
protected void addInternal(T term) throws IOException
{
term.serialize(buffer);
}
public boolean hasSpaceFor(T element)
{
return sizeAfter(element) < BLOCK_SIZE;
}
protected int sizeAfter(T element)
{
return getWatermark() + 4 + element.serializedSize();
}
protected int getWatermark()
{
return 4 + offsets.size() * 2 + (int) buffer.position();
}
public void flushAndClear(SequentialWriter out) throws IOException
{
out.writeInt(offsets.size());
for (int i = 0; i < offsets.size(); i++)
out.writeShort(offsets.get(i));
out.write(buffer.buffer());
alignToBlock(out);
offsets.clear();
buffer.clear();
}
}
private static class MutableDataBlock extends MutableBlock<InMemoryDataTerm>
{
private final Mode mode;
private int offset = 0;
private int sparseValueTerms = 0;
private final List<TokenTreeBuilder> containers = new ArrayList<>();
private TokenTreeBuilder combinedIndex;
public MutableDataBlock(Mode mode)
{
this.mode = mode;
this.combinedIndex = new TokenTreeBuilder();
}
protected void addInternal(InMemoryDataTerm term) throws IOException
{
TokenTreeBuilder keys = term.keys;
if (mode == Mode.SPARSE && keys.getTokenCount() <= 5)
{
writeTerm(term, keys);
sparseValueTerms++;
}
else
{
writeTerm(term, offset);
offset += keys.serializedSize();
containers.add(keys);
}
if (mode == Mode.SPARSE)
combinedIndex.add(keys.getTokens());
}
protected int sizeAfter(InMemoryDataTerm element)
{
return super.sizeAfter(element) + ptrLength(element);
}
public void flushAndClear(SequentialWriter out) throws IOException
{
super.flushAndClear(out);
out.writeInt((sparseValueTerms == 0) ? -1 : offset);
if (containers.size() > 0)
{
for (TokenTreeBuilder tokens : containers)
tokens.write(out);
}
if (sparseValueTerms > 0)
{
combinedIndex.finish().write(out);
}
alignToBlock(out);
containers.clear();
combinedIndex = new TokenTreeBuilder();
offset = 0;
sparseValueTerms = 0;
}
private int ptrLength(InMemoryDataTerm term)
{
return (term.keys.getTokenCount() > 5)
? 5 // 1 byte type + 4 byte offset to the tree
: 1 + (8 * (int) term.keys.getTokenCount()); // 1 byte size + n 8 byte tokens
}
private void writeTerm(InMemoryTerm term, TokenTreeBuilder keys) throws IOException
{
term.serialize(buffer);
buffer.writeByte((byte) keys.getTokenCount());
Iterator<Pair<Long, LongSet>> tokens = keys.iterator();
while (tokens.hasNext())
buffer.writeLong(tokens.next().left);
}
private void writeTerm(InMemoryTerm term, int offset) throws IOException
{
term.serialize(buffer);
buffer.writeByte(0x0);
buffer.writeInt(offset);
}
}
}

View File

@ -0,0 +1,361 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
import org.apache.cassandra.concurrent.JMXEnabledThreadPoolExecutor;
import org.apache.cassandra.concurrent.NamedThreadFactory;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.compaction.OperationType;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.utils.CombinedTermIterator;
import org.apache.cassandra.index.sasi.utils.TypeUtil;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.sstable.Descriptor;
import org.apache.cassandra.io.sstable.format.SSTableFlushObserver;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.Uninterruptibles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PerSSTableIndexWriter implements SSTableFlushObserver
{
private static final Logger logger = LoggerFactory.getLogger(PerSSTableIndexWriter.class);
private static final ThreadPoolExecutor INDEX_FLUSHER_MEMTABLE;
private static final ThreadPoolExecutor INDEX_FLUSHER_GENERAL;
static
{
INDEX_FLUSHER_GENERAL = new JMXEnabledThreadPoolExecutor(1, 8, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new NamedThreadFactory("SASI-General"),
"internal");
INDEX_FLUSHER_GENERAL.allowCoreThreadTimeOut(true);
INDEX_FLUSHER_MEMTABLE = new JMXEnabledThreadPoolExecutor(1, 8, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new NamedThreadFactory("SASI-Memtable"),
"internal");
INDEX_FLUSHER_MEMTABLE.allowCoreThreadTimeOut(true);
}
private final int nowInSec = FBUtilities.nowInSeconds();
private final Descriptor descriptor;
private final OperationType source;
private final AbstractType<?> keyValidator;
private final Map<ColumnDefinition, ColumnIndex> supportedIndexes;
@VisibleForTesting
protected final Map<ColumnDefinition, Index> indexes;
private DecoratedKey currentKey;
private long currentKeyPosition;
private boolean isComplete;
public PerSSTableIndexWriter(AbstractType<?> keyValidator,
Descriptor descriptor,
OperationType source,
Map<ColumnDefinition, ColumnIndex> supportedIndexes)
{
this.keyValidator = keyValidator;
this.descriptor = descriptor;
this.source = source;
this.supportedIndexes = supportedIndexes;
this.indexes = new HashMap<>();
}
public void begin()
{}
public void startPartition(DecoratedKey key, long curPosition)
{
currentKey = key;
currentKeyPosition = curPosition;
}
public void nextUnfilteredCluster(Unfiltered unfiltered)
{
if (!unfiltered.isRow())
return;
Row row = (Row) unfiltered;
supportedIndexes.keySet().forEach((column) -> {
ByteBuffer value = ColumnIndex.getValueOf(column, row, nowInSec);
if (value == null)
return;
ColumnIndex columnIndex = supportedIndexes.get(column);
if (columnIndex == null)
return;
Index index = indexes.get(column);
if (index == null)
indexes.put(column, (index = new Index(columnIndex)));
index.add(value.duplicate(), currentKey, currentKeyPosition);
});
}
public void complete()
{
if (isComplete)
return;
currentKey = null;
try
{
CountDownLatch latch = new CountDownLatch(indexes.size());
for (Index index : indexes.values())
index.complete(latch);
Uninterruptibles.awaitUninterruptibly(latch);
}
finally
{
indexes.clear();
isComplete = true;
}
}
public Index getIndex(ColumnDefinition columnDef)
{
return indexes.get(columnDef);
}
public Descriptor getDescriptor()
{
return descriptor;
}
@VisibleForTesting
protected class Index
{
private final ColumnIndex columnIndex;
private final String outputFile;
private final AbstractAnalyzer analyzer;
private final long maxMemorySize;
@VisibleForTesting
protected final Set<Future<OnDiskIndex>> segments;
private int segmentNumber = 0;
private OnDiskIndexBuilder currentBuilder;
public Index(ColumnIndex columnIndex)
{
this.columnIndex = columnIndex;
this.outputFile = descriptor.filenameFor(columnIndex.getComponent());
this.analyzer = columnIndex.getAnalyzer();
this.segments = new HashSet<>();
this.maxMemorySize = maxMemorySize(columnIndex);
this.currentBuilder = newIndexBuilder();
}
public void add(ByteBuffer term, DecoratedKey key, long keyPosition)
{
if (term.remaining() == 0)
return;
boolean isAdded = false;
analyzer.reset(term);
while (analyzer.hasNext())
{
ByteBuffer token = analyzer.next();
int size = token.remaining();
if (token.remaining() >= OnDiskIndexBuilder.MAX_TERM_SIZE)
{
logger.info("Rejecting value (size {}, maximum {} bytes) for column {} (analyzed {}) at {} SSTable.",
term.remaining(),
OnDiskIndexBuilder.MAX_TERM_SIZE,
columnIndex.getColumnName(),
columnIndex.getMode().isAnalyzed,
descriptor);
continue;
}
if (!TypeUtil.isValid(token, columnIndex.getValidator()))
{
if ((token = TypeUtil.tryUpcast(token, columnIndex.getValidator())) == null)
{
logger.info("({}) Failed to add {} to index for key: {}, value size was {} bytes, validator is {}.",
outputFile,
columnIndex.getColumnName(),
keyValidator.getString(key.getKey()),
size,
columnIndex.getValidator());
continue;
}
}
currentBuilder.add(token, key, keyPosition);
isAdded = true;
}
if (!isAdded || currentBuilder.estimatedMemoryUse() < maxMemorySize)
return; // non of the generated tokens were added to the index or memory size wasn't reached
segments.add(getExecutor().submit(scheduleSegmentFlush(false)));
}
@VisibleForTesting
protected Callable<OnDiskIndex> scheduleSegmentFlush(final boolean isFinal)
{
final OnDiskIndexBuilder builder = currentBuilder;
currentBuilder = newIndexBuilder();
final String segmentFile = filename(isFinal);
return () -> {
long start1 = System.nanoTime();
try
{
File index = new File(segmentFile);
return builder.finish(index) ? new OnDiskIndex(index, columnIndex.getValidator(), null) : null;
}
finally
{
if (!isFinal)
logger.info("Flushed index segment {}, took {} ms.", segmentFile, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start1));
}
};
}
public void complete(final CountDownLatch latch)
{
logger.info("Scheduling index flush to {}", outputFile);
getExecutor().submit((Runnable) () -> {
long start1 = System.nanoTime();
OnDiskIndex[] parts = new OnDiskIndex[segments.size() + 1];
try
{
// no parts present, build entire index from memory
if (segments.isEmpty())
{
scheduleSegmentFlush(true).call();
return;
}
// parts are present but there is something still in memory, let's flush that inline
if (!currentBuilder.isEmpty())
{
OnDiskIndex last = scheduleSegmentFlush(false).call();
segments.add(Futures.immediateFuture(last));
}
int index = 0;
ByteBuffer combinedMin = null, combinedMax = null;
for (Future<OnDiskIndex> f : segments)
{
OnDiskIndex part = Futures.getUnchecked(f);
if (part == null)
continue;
parts[index++] = part;
combinedMin = (combinedMin == null || keyValidator.compare(combinedMin, part.minKey()) > 0) ? part.minKey() : combinedMin;
combinedMax = (combinedMax == null || keyValidator.compare(combinedMax, part.maxKey()) < 0) ? part.maxKey() : combinedMax;
}
OnDiskIndexBuilder builder = newIndexBuilder();
builder.finish(Pair.create(combinedMin, combinedMax),
new File(outputFile),
new CombinedTermIterator(parts));
}
catch (Exception e)
{
logger.error("Failed to flush index {}.", outputFile, e);
FileUtils.delete(outputFile);
}
finally
{
logger.info("Index flush to {} took {} ms.", outputFile, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start1));
for (OnDiskIndex part : parts)
{
if (part == null)
continue;
FileUtils.closeQuietly(part);
FileUtils.delete(part.getIndexPath());
}
latch.countDown();
}
});
}
private ExecutorService getExecutor()
{
return source == OperationType.FLUSH ? INDEX_FLUSHER_MEMTABLE : INDEX_FLUSHER_GENERAL;
}
private OnDiskIndexBuilder newIndexBuilder()
{
return new OnDiskIndexBuilder(keyValidator, columnIndex.getValidator(), columnIndex.getMode().mode);
}
public String filename(boolean isFinal)
{
return outputFile + (isFinal ? "" : "_" + segmentNumber++);
}
}
protected long maxMemorySize(ColumnIndex columnIndex)
{
// 1G for memtable and configuration for compaction
return source == OperationType.FLUSH ? 1073741824L : columnIndex.getMode().maxCompactionFlushMemoryInMb;
}
public int hashCode()
{
return descriptor.hashCode();
}
public boolean equals(Object o)
{
return !(o == null || !(o instanceof PerSSTableIndexWriter)) && descriptor.equals(((PerSSTableIndexWriter) o).descriptor);
}
}

View File

@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
import com.google.common.primitives.Longs;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.utils.CombinedValue;
public abstract class Token implements CombinedValue<Long>, Iterable<DecoratedKey>
{
protected final long token;
public Token(long token)
{
this.token = token;
}
public Long get()
{
return token;
}
public int compareTo(CombinedValue<Long> o)
{
return Longs.compare(token, ((Token) o).token);
}
}

View File

@ -0,0 +1,519 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
import java.io.IOException;
import java.util.*;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.utils.AbstractIterator;
import org.apache.cassandra.index.sasi.utils.CombinedValue;
import org.apache.cassandra.index.sasi.utils.MappedBuffer;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.utils.MergeIterator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.Iterators;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import static org.apache.cassandra.index.sasi.disk.TokenTreeBuilder.EntryType;
// Note: all of the seek-able offsets contained in TokenTree should be sizeof(long)
// even if currently only lower int portion of them if used, because that makes
// it possible to switch to mmap implementation which supports long positions
// without any on-disk format changes and/or re-indexing if one day we'll have a need to.
public class TokenTree
{
private static final int LONG_BYTES = Long.SIZE / 8;
private static final int SHORT_BYTES = Short.SIZE / 8;
private final Descriptor descriptor;
private final MappedBuffer file;
private final long startPos;
private final long treeMinToken;
private final long treeMaxToken;
private final long tokenCount;
@VisibleForTesting
protected TokenTree(MappedBuffer tokenTree)
{
this(Descriptor.CURRENT, tokenTree);
}
public TokenTree(Descriptor d, MappedBuffer tokenTree)
{
descriptor = d;
file = tokenTree;
startPos = file.position();
file.position(startPos + TokenTreeBuilder.SHARED_HEADER_BYTES);
if (!validateMagic())
throw new IllegalArgumentException("invalid token tree");
tokenCount = file.getLong();
treeMinToken = file.getLong();
treeMaxToken = file.getLong();
}
public long getCount()
{
return tokenCount;
}
public RangeIterator<Long, Token> iterator(Function<Long, DecoratedKey> keyFetcher)
{
return new TokenTreeIterator(file.duplicate(), keyFetcher);
}
public OnDiskToken get(final long searchToken, Function<Long, DecoratedKey> keyFetcher)
{
seekToLeaf(searchToken, file);
long leafStart = file.position();
short leafSize = file.getShort(leafStart + 1); // skip the info byte
file.position(leafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES); // skip to tokens
short tokenIndex = searchLeaf(searchToken, leafSize);
file.position(leafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES);
OnDiskToken token = OnDiskToken.getTokenAt(file, tokenIndex, leafSize, keyFetcher);
return token.get().equals(searchToken) ? token : null;
}
private boolean validateMagic()
{
switch (descriptor.version.toString())
{
case Descriptor.VERSION_AA:
return true;
case Descriptor.VERSION_AB:
return TokenTreeBuilder.AB_MAGIC == file.getShort();
default:
return false;
}
}
// finds leaf that *could* contain token
private void seekToLeaf(long token, MappedBuffer file)
{
// this loop always seeks forward except for the first iteration
// where it may seek back to the root
long blockStart = startPos;
while (true)
{
file.position(blockStart);
byte info = file.get();
boolean isLeaf = (info & 1) == 1;
if (isLeaf)
{
file.position(blockStart);
break;
}
short tokenCount = file.getShort();
long minToken = file.getLong();
long maxToken = file.getLong();
long seekBase = blockStart + TokenTreeBuilder.BLOCK_HEADER_BYTES;
if (minToken > token)
{
// seek to beginning of child offsets to locate first child
file.position(seekBase + tokenCount * LONG_BYTES);
blockStart = (startPos + (int) file.getLong());
}
else if (maxToken < token)
{
// seek to end of child offsets to locate last child
file.position(seekBase + (2 * tokenCount) * LONG_BYTES);
blockStart = (startPos + (int) file.getLong());
}
else
{
// skip to end of block header/start of interior block tokens
file.position(seekBase);
short offsetIndex = searchBlock(token, tokenCount, file);
// file pointer is now at beginning of offsets
if (offsetIndex == tokenCount)
file.position(file.position() + (offsetIndex * LONG_BYTES));
else
file.position(file.position() + ((tokenCount - offsetIndex - 1) + offsetIndex) * LONG_BYTES);
blockStart = (startPos + (int) file.getLong());
}
}
}
private short searchBlock(long searchToken, short tokenCount, MappedBuffer file)
{
short offsetIndex = 0;
for (int i = 0; i < tokenCount; i++)
{
long readToken = file.getLong();
if (searchToken < readToken)
break;
offsetIndex++;
}
return offsetIndex;
}
private short searchLeaf(long searchToken, short tokenCount)
{
long base = file.position();
int start = 0;
int end = tokenCount;
int middle = 0;
while (start <= end)
{
middle = start + ((end - start) >> 1);
// each entry is 16 bytes wide, token is in bytes 4-11
long token = file.getLong(base + (middle * (2 * LONG_BYTES) + 4));
if (token == searchToken)
break;
if (token < searchToken)
start = middle + 1;
else
end = middle - 1;
}
return (short) middle;
}
public class TokenTreeIterator extends RangeIterator<Long, Token>
{
private final Function<Long, DecoratedKey> keyFetcher;
private final MappedBuffer file;
private long currentLeafStart;
private int currentTokenIndex;
private long leafMinToken;
private long leafMaxToken;
private short leafSize;
protected boolean firstIteration = true;
private boolean lastLeaf;
TokenTreeIterator(MappedBuffer file, Function<Long, DecoratedKey> keyFetcher)
{
super(treeMinToken, treeMaxToken, tokenCount);
this.file = file;
this.keyFetcher = keyFetcher;
}
protected Token computeNext()
{
maybeFirstIteration();
if (currentTokenIndex >= leafSize && lastLeaf)
return endOfData();
if (currentTokenIndex < leafSize) // tokens remaining in this leaf
{
return getTokenAt(currentTokenIndex++);
}
else // no more tokens remaining in this leaf
{
assert !lastLeaf;
seekToNextLeaf();
setupBlock();
return computeNext();
}
}
protected void performSkipTo(Long nextToken)
{
maybeFirstIteration();
if (nextToken <= leafMaxToken) // next is in this leaf block
{
searchLeaf(nextToken);
}
else // next is in a leaf block that needs to be found
{
seekToLeaf(nextToken, file);
setupBlock();
findNearest(nextToken);
}
}
private void setupBlock()
{
currentLeafStart = file.position();
currentTokenIndex = 0;
lastLeaf = (file.get() & (1 << TokenTreeBuilder.LAST_LEAF_SHIFT)) > 0;
leafSize = file.getShort();
leafMinToken = file.getLong();
leafMaxToken = file.getLong();
// seek to end of leaf header/start of data
file.position(currentLeafStart + TokenTreeBuilder.BLOCK_HEADER_BYTES);
}
private void findNearest(Long next)
{
if (next > leafMaxToken && !lastLeaf)
{
seekToNextLeaf();
setupBlock();
findNearest(next);
}
else if (next > leafMinToken)
searchLeaf(next);
}
private void searchLeaf(long next)
{
for (int i = currentTokenIndex; i < leafSize; i++)
{
if (compareTokenAt(currentTokenIndex, next) >= 0)
break;
currentTokenIndex++;
}
}
private int compareTokenAt(int idx, long toToken)
{
return Long.compare(file.getLong(getTokenPosition(idx)), toToken);
}
private Token getTokenAt(int idx)
{
return OnDiskToken.getTokenAt(file, idx, leafSize, keyFetcher);
}
private long getTokenPosition(int idx)
{
// skip 4 byte entry header to get position pointing directly at the entry's token
return OnDiskToken.getEntryPosition(idx, file) + (2 * SHORT_BYTES);
}
private void seekToNextLeaf()
{
file.position(currentLeafStart + TokenTreeBuilder.BLOCK_BYTES);
}
public void close() throws IOException
{
// nothing to do here
}
private void maybeFirstIteration()
{
// seek to the first token only when requested for the first time,
// highly predictable branch and saves us a lot by not traversing the tree
// on creation time because it's not at all required.
if (!firstIteration)
return;
seekToLeaf(treeMinToken, file);
setupBlock();
firstIteration = false;
}
}
public static class OnDiskToken extends Token
{
private final Set<TokenInfo> info = new HashSet<>(2);
private final Set<DecoratedKey> loadedKeys = new TreeSet<>(DecoratedKey.comparator);
public OnDiskToken(MappedBuffer buffer, long position, short leafSize, Function<Long, DecoratedKey> keyFetcher)
{
super(buffer.getLong(position + (2 * SHORT_BYTES)));
info.add(new TokenInfo(buffer, position, leafSize, keyFetcher));
}
public void merge(CombinedValue<Long> other)
{
if (!(other instanceof Token))
return;
Token o = (Token) other;
if (token != o.token)
throw new IllegalArgumentException(String.format("%s != %s", token, o.token));
if (o instanceof OnDiskToken)
{
info.addAll(((OnDiskToken) other).info);
}
else
{
Iterators.addAll(loadedKeys, o.iterator());
}
}
public Iterator<DecoratedKey> iterator()
{
List<Iterator<DecoratedKey>> keys = new ArrayList<>(info.size());
for (TokenInfo i : info)
keys.add(i.iterator());
if (!loadedKeys.isEmpty())
keys.add(loadedKeys.iterator());
return MergeIterator.get(keys, DecoratedKey.comparator, new MergeIterator.Reducer<DecoratedKey, DecoratedKey>()
{
DecoratedKey reduced = null;
public boolean trivialReduceIsTrivial()
{
return true;
}
public void reduce(int idx, DecoratedKey current)
{
reduced = current;
}
protected DecoratedKey getReduced()
{
return reduced;
}
});
}
public Set<Long> getOffsets()
{
Set<Long> offsets = new HashSet<>();
for (TokenInfo i : info)
{
for (long offset : i.fetchOffsets())
offsets.add(offset);
}
return offsets;
}
public static OnDiskToken getTokenAt(MappedBuffer buffer, int idx, short leafSize, Function<Long, DecoratedKey> keyFetcher)
{
return new OnDiskToken(buffer, getEntryPosition(idx, buffer), leafSize, keyFetcher);
}
private static long getEntryPosition(int idx, MappedBuffer file)
{
// info (4 bytes) + token (8 bytes) + offset (4 bytes) = 16 bytes
return file.position() + (idx * (2 * LONG_BYTES));
}
}
private static class TokenInfo
{
private final MappedBuffer buffer;
private final Function<Long, DecoratedKey> keyFetcher;
private final long position;
private final short leafSize;
public TokenInfo(MappedBuffer buffer, long position, short leafSize, Function<Long, DecoratedKey> keyFetcher)
{
this.keyFetcher = keyFetcher;
this.buffer = buffer;
this.position = position;
this.leafSize = leafSize;
}
public Iterator<DecoratedKey> iterator()
{
return new KeyIterator(keyFetcher, fetchOffsets());
}
public int hashCode()
{
return new HashCodeBuilder().append(keyFetcher).append(position).append(leafSize).build();
}
public boolean equals(Object other)
{
if (!(other instanceof TokenInfo))
return false;
TokenInfo o = (TokenInfo) other;
return keyFetcher == o.keyFetcher && position == o.position;
}
private long[] fetchOffsets()
{
short info = buffer.getShort(position);
short offsetShort = buffer.getShort(position + SHORT_BYTES);
int offsetInt = buffer.getInt(position + (2 * SHORT_BYTES) + LONG_BYTES);
EntryType type = EntryType.of(info & TokenTreeBuilder.ENTRY_TYPE_MASK);
switch (type)
{
case SIMPLE:
return new long[] { offsetInt };
case OVERFLOW:
long[] offsets = new long[offsetShort]; // offsetShort contains count of tokens
long offsetPos = (buffer.position() + (2 * (leafSize * LONG_BYTES)) + (offsetInt * LONG_BYTES));
for (int i = 0; i < offsetShort; i++)
offsets[i] = buffer.getLong(offsetPos + (i * LONG_BYTES));
return offsets;
case FACTORED:
return new long[] { (((long) offsetInt) << Short.SIZE) + offsetShort };
case PACKED:
return new long[] { offsetShort, offsetInt };
default:
throw new IllegalStateException("Unknown entry type: " + type);
}
}
}
private static class KeyIterator extends AbstractIterator<DecoratedKey>
{
private final Function<Long, DecoratedKey> keyFetcher;
private final long[] offsets;
private int index = 0;
public KeyIterator(Function<Long, DecoratedKey> keyFetcher, long[] offsets)
{
this.keyFetcher = keyFetcher;
this.offsets = offsets;
}
public DecoratedKey computeNext()
{
return index < offsets.length ? keyFetcher.apply(offsets[index++]) : endOfData();
}
}
}

View File

@ -0,0 +1,839 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.disk;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import org.apache.cassandra.io.util.DataOutputPlus;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.cassandra.utils.Pair;
import com.carrotsearch.hppc.LongArrayList;
import com.carrotsearch.hppc.LongSet;
import com.carrotsearch.hppc.cursors.LongCursor;
import com.carrotsearch.hppc.LongOpenHashSet;
import com.google.common.collect.AbstractIterator;
public class TokenTreeBuilder
{
// note: ordinal positions are used here, do not change order
enum EntryType
{
SIMPLE, FACTORED, PACKED, OVERFLOW;
public static EntryType of(int ordinal)
{
if (ordinal == SIMPLE.ordinal())
return SIMPLE;
if (ordinal == FACTORED.ordinal())
return FACTORED;
if (ordinal == PACKED.ordinal())
return PACKED;
if (ordinal == OVERFLOW.ordinal())
return OVERFLOW;
throw new IllegalArgumentException("Unknown ordinal: " + ordinal);
}
}
public static final int BLOCK_BYTES = 4096;
public static final int BLOCK_HEADER_BYTES = 64;
public static final int OVERFLOW_TRAILER_BYTES = 64;
public static final int OVERFLOW_TRAILER_CAPACITY = OVERFLOW_TRAILER_BYTES / 8;
public static final int TOKENS_PER_BLOCK = (BLOCK_BYTES - BLOCK_HEADER_BYTES - OVERFLOW_TRAILER_BYTES) / 16;
public static final long MAX_OFFSET = (1L << 47) - 1; // 48 bits for (signed) offset
public static final byte LAST_LEAF_SHIFT = 1;
public static final byte SHARED_HEADER_BYTES = 19;
public static final byte ENTRY_TYPE_MASK = 0x03;
public static final short AB_MAGIC = 0x5A51;
private final SortedMap<Long, LongSet> tokens = new TreeMap<>();
private int numBlocks;
private Node root;
private InteriorNode rightmostParent;
private Leaf leftmostLeaf;
private Leaf rightmostLeaf;
private long tokenCount = 0;
private long treeMinToken;
private long treeMaxToken;
public TokenTreeBuilder()
{}
public TokenTreeBuilder(SortedMap<Long, LongSet> data)
{
add(data);
}
public void add(Long token, long keyPosition)
{
LongSet found = tokens.get(token);
if (found == null)
tokens.put(token, (found = new LongOpenHashSet(2)));
found.add(keyPosition);
}
public void add(SortedMap<Long, LongSet> data)
{
for (Map.Entry<Long, LongSet> newEntry : data.entrySet())
{
LongSet found = tokens.get(newEntry.getKey());
if (found == null)
tokens.put(newEntry.getKey(), (found = new LongOpenHashSet(4)));
for (LongCursor offset : newEntry.getValue())
found.add(offset.value);
}
}
public TokenTreeBuilder finish()
{
maybeBulkLoad();
return this;
}
public SortedMap<Long, LongSet> getTokens()
{
return tokens;
}
public long getTokenCount()
{
return tokenCount;
}
public int serializedSize()
{
if (numBlocks == 1)
return (BLOCK_HEADER_BYTES + ((int) tokenCount * 16));
else
return numBlocks * BLOCK_BYTES;
}
public void write(DataOutputPlus out) throws IOException
{
ByteBuffer blockBuffer = ByteBuffer.allocate(BLOCK_BYTES);
Iterator<Node> levelIterator = root.levelIterator();
long childBlockIndex = 1;
while (levelIterator != null)
{
Node firstChild = null;
while (levelIterator.hasNext())
{
Node block = levelIterator.next();
if (firstChild == null && !block.isLeaf())
firstChild = ((InteriorNode) block).children.get(0);
block.serialize(childBlockIndex, blockBuffer);
flushBuffer(blockBuffer, out, numBlocks != 1);
childBlockIndex += block.childCount();
}
levelIterator = (firstChild == null) ? null : firstChild.levelIterator();
}
}
public Iterator<Pair<Long, LongSet>> iterator()
{
return new TokenIterator(leftmostLeaf.levelIterator());
}
private void maybeBulkLoad()
{
if (root == null)
bulkLoad();
}
private void flushBuffer(ByteBuffer buffer, DataOutputPlus o, boolean align) throws IOException
{
// seek to end of last block before flushing
if (align)
alignBuffer(buffer, BLOCK_BYTES);
buffer.flip();
o.write(buffer);
buffer.clear();
}
private static void alignBuffer(ByteBuffer buffer, int blockSize)
{
long curPos = buffer.position();
if ((curPos & (blockSize - 1)) != 0) // align on the block boundary if needed
buffer.position((int) FBUtilities.align(curPos, blockSize));
}
private void bulkLoad()
{
tokenCount = tokens.size();
treeMinToken = tokens.firstKey();
treeMaxToken = tokens.lastKey();
numBlocks = 1;
// special case the tree that only has a single block in it (so we don't create a useless root)
if (tokenCount <= TOKENS_PER_BLOCK)
{
leftmostLeaf = new Leaf(tokens);
rightmostLeaf = leftmostLeaf;
root = leftmostLeaf;
}
else
{
root = new InteriorNode();
rightmostParent = (InteriorNode) root;
int i = 0;
Leaf lastLeaf = null;
Long firstToken = tokens.firstKey();
Long finalToken = tokens.lastKey();
Long lastToken;
for (Long token : tokens.keySet())
{
if (i == 0 || (i % TOKENS_PER_BLOCK != 0 && i != (tokenCount - 1)))
{
i++;
continue;
}
lastToken = token;
Leaf leaf = (i != (tokenCount - 1) || token.equals(finalToken)) ?
new Leaf(tokens.subMap(firstToken, lastToken)) : new Leaf(tokens.tailMap(firstToken));
if (i == TOKENS_PER_BLOCK)
leftmostLeaf = leaf;
else
lastLeaf.next = leaf;
rightmostParent.add(leaf);
lastLeaf = leaf;
rightmostLeaf = leaf;
firstToken = lastToken;
i++;
numBlocks++;
if (token.equals(finalToken))
{
Leaf finalLeaf = new Leaf(tokens.tailMap(token));
lastLeaf.next = finalLeaf;
rightmostParent.add(finalLeaf);
rightmostLeaf = finalLeaf;
numBlocks++;
}
}
}
}
private abstract class Node
{
protected InteriorNode parent;
protected Node next;
protected Long nodeMinToken, nodeMaxToken;
public abstract void serialize(long childBlockIndex, ByteBuffer buf);
public abstract int childCount();
public abstract int tokenCount();
public abstract Long smallestToken();
public Iterator<Node> levelIterator()
{
return new LevelIterator(this);
}
public boolean isLeaf()
{
return (this instanceof Leaf);
}
protected boolean isLastLeaf()
{
return this == rightmostLeaf;
}
protected boolean isRoot()
{
return this == root;
}
protected void updateTokenRange(long token)
{
nodeMinToken = nodeMinToken == null ? token : Math.min(nodeMinToken, token);
nodeMaxToken = nodeMaxToken == null ? token : Math.max(nodeMaxToken, token);
}
protected void serializeHeader(ByteBuffer buf)
{
Header header;
if (isRoot())
header = new RootHeader();
else if (!isLeaf())
header = new InteriorNodeHeader();
else
header = new LeafHeader();
header.serialize(buf);
alignBuffer(buf, BLOCK_HEADER_BYTES);
}
private abstract class Header
{
public void serialize(ByteBuffer buf)
{
buf.put(infoByte())
.putShort((short) (tokenCount()))
.putLong(nodeMinToken)
.putLong(nodeMaxToken);
}
protected abstract byte infoByte();
}
private class RootHeader extends Header
{
public void serialize(ByteBuffer buf)
{
super.serialize(buf);
writeMagic(buf);
buf.putLong(tokenCount)
.putLong(treeMinToken)
.putLong(treeMaxToken);
}
protected byte infoByte()
{
// if leaf, set leaf indicator and last leaf indicator (bits 0 & 1)
// if not leaf, clear both bits
return (byte) ((isLeaf()) ? 3 : 0);
}
protected void writeMagic(ByteBuffer buf)
{
switch (Descriptor.CURRENT_VERSION)
{
case Descriptor.VERSION_AB:
buf.putShort(AB_MAGIC);
break;
default:
break;
}
}
}
private class InteriorNodeHeader extends Header
{
// bit 0 (leaf indicator) & bit 1 (last leaf indicator) cleared
protected byte infoByte()
{
return 0;
}
}
private class LeafHeader extends Header
{
// bit 0 set as leaf indicator
// bit 1 set if this is last leaf of data
protected byte infoByte()
{
byte infoByte = 1;
infoByte |= (isLastLeaf()) ? (1 << LAST_LEAF_SHIFT) : 0;
return infoByte;
}
}
}
private class Leaf extends Node
{
private final SortedMap<Long, LongSet> tokens;
private LongArrayList overflowCollisions;
Leaf(SortedMap<Long, LongSet> data)
{
nodeMinToken = data.firstKey();
nodeMaxToken = data.lastKey();
tokens = data;
}
public Long largestToken()
{
return nodeMaxToken;
}
public void serialize(long childBlockIndex, ByteBuffer buf)
{
serializeHeader(buf);
serializeData(buf);
serializeOverflowCollisions(buf);
}
public int childCount()
{
return 0;
}
public int tokenCount()
{
return tokens.size();
}
public Long smallestToken()
{
return nodeMinToken;
}
public Iterator<Map.Entry<Long, LongSet>> tokenIterator()
{
return tokens.entrySet().iterator();
}
private void serializeData(ByteBuffer buf)
{
for (Map.Entry<Long, LongSet> entry : tokens.entrySet())
createEntry(entry.getKey(), entry.getValue()).serialize(buf);
}
private void serializeOverflowCollisions(ByteBuffer buf)
{
if (overflowCollisions != null)
for (LongCursor offset : overflowCollisions)
buf.putLong(offset.value);
}
private LeafEntry createEntry(final long tok, final LongSet offsets)
{
int offsetCount = offsets.size();
switch (offsetCount)
{
case 0:
throw new AssertionError("no offsets for token " + tok);
case 1:
long offset = offsets.toArray()[0];
if (offset > MAX_OFFSET)
throw new AssertionError("offset " + offset + " cannot be greater than " + MAX_OFFSET);
else if (offset <= Integer.MAX_VALUE)
return new SimpleLeafEntry(tok, offset);
else
return new FactoredOffsetLeafEntry(tok, offset);
case 2:
long[] rawOffsets = offsets.toArray();
if (rawOffsets[0] <= Integer.MAX_VALUE && rawOffsets[1] <= Integer.MAX_VALUE &&
(rawOffsets[0] <= Short.MAX_VALUE || rawOffsets[1] <= Short.MAX_VALUE))
return new PackedCollisionLeafEntry(tok, rawOffsets);
else
return createOverflowEntry(tok, offsetCount, offsets);
default:
return createOverflowEntry(tok, offsetCount, offsets);
}
}
private LeafEntry createOverflowEntry(final long tok, final int offsetCount, final LongSet offsets)
{
if (overflowCollisions == null)
overflowCollisions = new LongArrayList();
LeafEntry entry = new OverflowCollisionLeafEntry(tok, (short) overflowCollisions.size(), (short) offsetCount);
for (LongCursor o : offsets) {
if (overflowCollisions.size() == OVERFLOW_TRAILER_CAPACITY)
throw new AssertionError("cannot have more than " + OVERFLOW_TRAILER_CAPACITY + " overflow collisions per leaf");
else
overflowCollisions.add(o.value);
}
return entry;
}
private abstract class LeafEntry
{
protected final long token;
abstract public EntryType type();
abstract public int offsetData();
abstract public short offsetExtra();
public LeafEntry(final long tok)
{
token = tok;
}
public void serialize(ByteBuffer buf)
{
buf.putShort((short) type().ordinal())
.putShort(offsetExtra())
.putLong(token)
.putInt(offsetData());
}
}
// assumes there is a single offset and the offset is <= Integer.MAX_VALUE
private class SimpleLeafEntry extends LeafEntry
{
private final long offset;
public SimpleLeafEntry(final long tok, final long off)
{
super(tok);
offset = off;
}
public EntryType type()
{
return EntryType.SIMPLE;
}
public int offsetData()
{
return (int) offset;
}
public short offsetExtra()
{
return 0;
}
}
// assumes there is a single offset and Integer.MAX_VALUE < offset <= MAX_OFFSET
// take the middle 32 bits of offset (or the top 32 when considering offset is max 48 bits)
// and store where offset is normally stored. take bottom 16 bits of offset and store in entry header
private class FactoredOffsetLeafEntry extends LeafEntry
{
private final long offset;
public FactoredOffsetLeafEntry(final long tok, final long off)
{
super(tok);
offset = off;
}
public EntryType type()
{
return EntryType.FACTORED;
}
public int offsetData()
{
return (int) (offset >>> Short.SIZE);
}
public short offsetExtra()
{
return (short) offset;
}
}
// holds an entry with two offsets that can be packed in an int & a short
// the int offset is stored where offset is normally stored. short offset is
// stored in entry header
private class PackedCollisionLeafEntry extends LeafEntry
{
private short smallerOffset;
private int largerOffset;
public PackedCollisionLeafEntry(final long tok, final long[] offs)
{
super(tok);
smallerOffset = (short) Math.min(offs[0], offs[1]);
largerOffset = (int) Math.max(offs[0], offs[1]);
}
public EntryType type()
{
return EntryType.PACKED;
}
public int offsetData()
{
return largerOffset;
}
public short offsetExtra()
{
return smallerOffset;
}
}
// holds an entry with three or more offsets, or two offsets that cannot
// be packed into an int & a short. the index into the overflow list
// is stored where the offset is normally stored. the number of overflowed offsets
// for the entry is stored in the entry header
private class OverflowCollisionLeafEntry extends LeafEntry
{
private final short startIndex;
private final short count;
public OverflowCollisionLeafEntry(final long tok, final short collisionStartIndex, final short collisionCount)
{
super(tok);
startIndex = collisionStartIndex;
count = collisionCount;
}
public EntryType type()
{
return EntryType.OVERFLOW;
}
public int offsetData()
{
return startIndex;
}
public short offsetExtra()
{
return count;
}
}
}
private class InteriorNode extends Node
{
private List<Long> tokens = new ArrayList<>(TOKENS_PER_BLOCK);
private List<Node> children = new ArrayList<>(TOKENS_PER_BLOCK + 1);
private int position = 0; // TODO (jwest): can get rid of this and use array size
public void serialize(long childBlockIndex, ByteBuffer buf)
{
serializeHeader(buf);
serializeTokens(buf);
serializeChildOffsets(childBlockIndex, buf);
}
public int childCount()
{
return children.size();
}
public int tokenCount()
{
return tokens.size();
}
public Long smallestToken()
{
return tokens.get(0);
}
protected void add(Long token, InteriorNode leftChild, InteriorNode rightChild)
{
int pos = tokens.size();
if (pos == TOKENS_PER_BLOCK)
{
InteriorNode sibling = split();
sibling.add(token, leftChild, rightChild);
}
else {
if (leftChild != null)
children.add(pos, leftChild);
if (rightChild != null)
{
children.add(pos + 1, rightChild);
rightChild.parent = this;
}
updateTokenRange(token);
tokens.add(pos, token);
}
}
protected void add(Leaf node)
{
if (position == (TOKENS_PER_BLOCK + 1))
{
rightmostParent = split();
rightmostParent.add(node);
}
else
{
node.parent = this;
children.add(position, node);
position++;
// the first child is referenced only during bulk load. we don't take a value
// to store into the tree, one is subtracted since position has already been incremented
// for the next node to be added
if (position - 1 == 0)
return;
// tokens are inserted one behind the current position, but 2 is subtracted because
// position has already been incremented for the next add
Long smallestToken = node.smallestToken();
updateTokenRange(smallestToken);
tokens.add(position - 2, smallestToken);
}
}
protected InteriorNode split()
{
Pair<Long, InteriorNode> splitResult = splitBlock();
Long middleValue = splitResult.left;
InteriorNode sibling = splitResult.right;
InteriorNode leftChild = null;
// create a new root if necessary
if (parent == null)
{
parent = new InteriorNode();
root = parent;
sibling.parent = parent;
leftChild = this;
numBlocks++;
}
parent.add(middleValue, leftChild, sibling);
return sibling;
}
protected Pair<Long, InteriorNode> splitBlock()
{
final int splitPosition = TOKENS_PER_BLOCK - 2;
InteriorNode sibling = new InteriorNode();
sibling.parent = parent;
next = sibling;
Long middleValue = tokens.get(splitPosition);
for (int i = splitPosition; i < TOKENS_PER_BLOCK; i++)
{
if (i != TOKENS_PER_BLOCK && i != splitPosition)
{
long token = tokens.get(i);
sibling.updateTokenRange(token);
sibling.tokens.add(token);
}
Node child = children.get(i + 1);
child.parent = sibling;
sibling.children.add(child);
sibling.position++;
}
for (int i = TOKENS_PER_BLOCK; i >= splitPosition; i--)
{
if (i != TOKENS_PER_BLOCK)
tokens.remove(i);
if (i != splitPosition)
children.remove(i);
}
nodeMinToken = smallestToken();
nodeMaxToken = tokens.get(tokens.size() - 1);
numBlocks++;
return Pair.create(middleValue, sibling);
}
protected boolean isFull()
{
return (position >= TOKENS_PER_BLOCK + 1);
}
private void serializeTokens(ByteBuffer buf)
{
for (Long token : tokens)
buf.putLong(token);
}
private void serializeChildOffsets(long childBlockIndex, ByteBuffer buf)
{
for (int i = 0; i < children.size(); i++)
buf.putLong((childBlockIndex + i) * BLOCK_BYTES);
}
}
public static class LevelIterator extends AbstractIterator<Node>
{
private Node currentNode;
LevelIterator(Node first)
{
currentNode = first;
}
public Node computeNext()
{
if (currentNode == null)
return endOfData();
Node returnNode = currentNode;
currentNode = returnNode.next;
return returnNode;
}
}
public static class TokenIterator extends AbstractIterator<Pair<Long, LongSet>>
{
private Iterator<Node> levelIterator;
private Iterator<Map.Entry<Long, LongSet>> currentIterator;
TokenIterator(Iterator<Node> level)
{
levelIterator = level;
if (levelIterator.hasNext())
currentIterator = ((Leaf) levelIterator.next()).tokenIterator();
}
public Pair<Long, LongSet> computeNext()
{
if (currentIterator != null && currentIterator.hasNext())
{
Map.Entry<Long, LongSet> next = currentIterator.next();
return Pair.create(next.getKey(), next.getValue());
}
else
{
if (!levelIterator.hasNext())
return endOfData();
else
{
currentIterator = ((Leaf) levelIterator.next()).tokenIterator();
return computeNext();
}
}
}
}
}

View File

@ -0,0 +1,21 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.exceptions;
public class TimeQuotaExceededException extends RuntimeException
{}

View File

@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.memory;
import java.nio.ByteBuffer;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.index.sasi.utils.TypeUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IndexMemtable
{
private static final Logger logger = LoggerFactory.getLogger(IndexMemtable.class);
private final MemIndex index;
public IndexMemtable(ColumnIndex columnIndex)
{
this.index = MemIndex.forColumn(columnIndex.keyValidator(), columnIndex);
}
public long index(DecoratedKey key, ByteBuffer value)
{
if (value == null || value.remaining() == 0)
return 0;
AbstractType<?> validator = index.columnIndex.getValidator();
if (!TypeUtil.isValid(value, validator))
{
int size = value.remaining();
if ((value = TypeUtil.tryUpcast(value, validator)) == null)
{
logger.error("Can't add column {} to index for key: {}, value size {} bytes, validator: {}.",
index.columnIndex.getColumnName(),
index.columnIndex.keyValidator().getString(key.getKey()),
size,
validator);
return 0;
}
}
return index.add(key, value);
}
public RangeIterator<Long, Token> search(Expression expression)
{
return index == null ? null : index.search(expression);
}
}

View File

@ -0,0 +1,118 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.memory;
import java.io.IOException;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentSkipListSet;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.utils.AbstractIterator;
import org.apache.cassandra.index.sasi.utils.CombinedValue;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import com.google.common.collect.PeekingIterator;
public class KeyRangeIterator extends RangeIterator<Long, Token>
{
private final DKIterator iterator;
public KeyRangeIterator(ConcurrentSkipListSet<DecoratedKey> keys)
{
super((Long) keys.first().getToken().getTokenValue(), (Long) keys.last().getToken().getTokenValue(), keys.size());
this.iterator = new DKIterator(keys.iterator());
}
protected Token computeNext()
{
return iterator.hasNext() ? new DKToken(iterator.next()) : endOfData();
}
protected void performSkipTo(Long nextToken)
{
while (iterator.hasNext())
{
DecoratedKey key = iterator.peek();
if (Long.compare((long) key.getToken().getTokenValue(), nextToken) >= 0)
break;
// consume smaller key
iterator.next();
}
}
public void close() throws IOException
{}
private static class DKIterator extends AbstractIterator<DecoratedKey> implements PeekingIterator<DecoratedKey>
{
private final Iterator<DecoratedKey> keys;
public DKIterator(Iterator<DecoratedKey> keys)
{
this.keys = keys;
}
protected DecoratedKey computeNext()
{
return keys.hasNext() ? keys.next() : endOfData();
}
}
private static class DKToken extends Token
{
private final SortedSet<DecoratedKey> keys;
public DKToken(final DecoratedKey key)
{
super((long) key.getToken().getTokenValue());
keys = new TreeSet<DecoratedKey>(DecoratedKey.comparator)
{{
add(key);
}};
}
public void merge(CombinedValue<Long> other)
{
if (!(other instanceof Token))
return;
Token o = (Token) other;
assert o.get().equals(token);
if (o instanceof DKToken)
{
keys.addAll(((DKToken) o).keys);
}
else
{
for (DecoratedKey key : o)
keys.add(key);
}
}
public Iterator<DecoratedKey> iterator()
{
return keys.iterator();
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.memory;
import java.nio.ByteBuffer;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.db.marshal.AbstractType;
import org.github.jamm.MemoryMeter;
public abstract class MemIndex
{
protected final AbstractType<?> keyValidator;
protected final ColumnIndex columnIndex;
protected MemIndex(AbstractType<?> keyValidator, ColumnIndex columnIndex)
{
this.keyValidator = keyValidator;
this.columnIndex = columnIndex;
}
public abstract long add(DecoratedKey key, ByteBuffer value);
public abstract RangeIterator<Long, Token> search(Expression expression);
public static MemIndex forColumn(AbstractType<?> keyValidator, ColumnIndex columnIndex)
{
return columnIndex.isLiteral()
? new TrieMemIndex(keyValidator, columnIndex)
: new SkipListMemIndex(keyValidator, columnIndex);
}
}

View File

@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.memory;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.ConcurrentSkipListSet;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.db.marshal.AbstractType;
public class SkipListMemIndex extends MemIndex
{
public static final int CSLM_OVERHEAD = 128; // average overhead of CSLM
private final ConcurrentSkipListMap<ByteBuffer, ConcurrentSkipListSet<DecoratedKey>> index;
public SkipListMemIndex(AbstractType<?> keyValidator, ColumnIndex columnIndex)
{
super(keyValidator, columnIndex);
index = new ConcurrentSkipListMap<>(columnIndex.getValidator());
}
public long add(DecoratedKey key, ByteBuffer value)
{
long overhead = CSLM_OVERHEAD; // DKs are shared
ConcurrentSkipListSet<DecoratedKey> keys = index.get(value);
if (keys == null)
{
ConcurrentSkipListSet<DecoratedKey> newKeys = new ConcurrentSkipListSet<>(DecoratedKey.comparator);
keys = index.putIfAbsent(value, newKeys);
if (keys == null)
{
overhead += CSLM_OVERHEAD + value.remaining();
keys = newKeys;
}
}
keys.add(key);
return overhead;
}
public RangeIterator<Long, Token> search(Expression expression)
{
ByteBuffer min = expression.lower == null ? null : expression.lower.value;
ByteBuffer max = expression.upper == null ? null : expression.upper.value;
SortedMap<ByteBuffer, ConcurrentSkipListSet<DecoratedKey>> search;
if (min == null && max == null)
{
throw new IllegalArgumentException();
}
if (min != null && max != null)
{
search = index.subMap(min, expression.lower.inclusive, max, expression.upper.inclusive);
}
else if (min == null)
{
search = index.headMap(max, expression.upper.inclusive);
}
else
{
search = index.tailMap(min, expression.lower.inclusive);
}
RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
search.values().stream()
.filter(keys -> !keys.isEmpty())
.forEach(keys -> builder.add(new KeyRangeIterator(keys)));
return builder.build();
}
}

View File

@ -0,0 +1,254 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.memory;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.ConcurrentSkipListSet;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Expression;
import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.db.marshal.AbstractType;
import com.googlecode.concurrenttrees.radix.ConcurrentRadixTree;
import com.googlecode.concurrenttrees.suffix.ConcurrentSuffixTree;
import com.googlecode.concurrenttrees.radix.node.concrete.SmartArrayBasedNodeFactory;
import com.googlecode.concurrenttrees.radix.node.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.cassandra.index.sasi.memory.SkipListMemIndex.CSLM_OVERHEAD;
public class TrieMemIndex extends MemIndex
{
private static final Logger logger = LoggerFactory.getLogger(TrieMemIndex.class);
private final ConcurrentTrie index;
public TrieMemIndex(AbstractType<?> keyValidator, ColumnIndex columnIndex)
{
super(keyValidator, columnIndex);
switch (columnIndex.getMode().mode)
{
case CONTAINS:
index = new ConcurrentSuffixTrie(columnIndex.getDefinition());
break;
case PREFIX:
index = new ConcurrentPrefixTrie(columnIndex.getDefinition());
break;
default:
throw new IllegalStateException("Unsupported mode: " + columnIndex.getMode().mode);
}
}
public long add(DecoratedKey key, ByteBuffer value)
{
AbstractAnalyzer analyzer = columnIndex.getAnalyzer();
analyzer.reset(value.duplicate());
long size = 0;
while (analyzer.hasNext())
{
ByteBuffer term = analyzer.next();
if (term.remaining() >= OnDiskIndexBuilder.MAX_TERM_SIZE)
{
logger.info("Can't add term of column {} to index for key: {}, term size {} bytes, max allowed size {} bytes, use analyzed = true (if not yet set) for that column.",
columnIndex.getColumnName(),
keyValidator.getString(key.getKey()),
term.remaining(),
OnDiskIndexBuilder.MAX_TERM_SIZE);
continue;
}
size += index.add(columnIndex.getValidator().getString(term), key);
}
return size;
}
public RangeIterator<Long, Token> search(Expression expression)
{
return index.search(expression);
}
private static abstract class ConcurrentTrie
{
public static final SizeEstimatingNodeFactory NODE_FACTORY = new SizeEstimatingNodeFactory();
protected final ColumnDefinition definition;
public ConcurrentTrie(ColumnDefinition column)
{
definition = column;
}
public long add(String value, DecoratedKey key)
{
long overhead = CSLM_OVERHEAD;
ConcurrentSkipListSet<DecoratedKey> keys = get(value);
if (keys == null)
{
ConcurrentSkipListSet<DecoratedKey> newKeys = new ConcurrentSkipListSet<>(DecoratedKey.comparator);
keys = putIfAbsent(value, newKeys);
if (keys == null)
{
overhead += CSLM_OVERHEAD + value.length();
keys = newKeys;
}
}
keys.add(key);
// get and reset new memory size allocated by current thread
overhead += NODE_FACTORY.currentUpdateSize();
NODE_FACTORY.reset();
return overhead;
}
public RangeIterator<Long, Token> search(Expression expression)
{
assert expression.getOp() == Expression.Op.EQ; // means that min == max
ByteBuffer prefix = expression.lower == null ? null : expression.lower.value;
Iterable<ConcurrentSkipListSet<DecoratedKey>> search = search(definition.cellValueType().getString(prefix));
RangeUnionIterator.Builder<Long, Token> builder = RangeUnionIterator.builder();
for (ConcurrentSkipListSet<DecoratedKey> keys : search)
{
if (!keys.isEmpty())
builder.add(new KeyRangeIterator(keys));
}
return builder.build();
}
protected abstract ConcurrentSkipListSet<DecoratedKey> get(String value);
protected abstract Iterable<ConcurrentSkipListSet<DecoratedKey>> search(String value);
protected abstract ConcurrentSkipListSet<DecoratedKey> putIfAbsent(String value, ConcurrentSkipListSet<DecoratedKey> key);
}
protected static class ConcurrentPrefixTrie extends ConcurrentTrie
{
private final ConcurrentRadixTree<ConcurrentSkipListSet<DecoratedKey>> trie;
private ConcurrentPrefixTrie(ColumnDefinition column)
{
super(column);
trie = new ConcurrentRadixTree<>(NODE_FACTORY);
}
public ConcurrentSkipListSet<DecoratedKey> get(String value)
{
return trie.getValueForExactKey(value);
}
public ConcurrentSkipListSet<DecoratedKey> putIfAbsent(String value, ConcurrentSkipListSet<DecoratedKey> newKeys)
{
return trie.putIfAbsent(value, newKeys);
}
public Iterable<ConcurrentSkipListSet<DecoratedKey>> search(String value)
{
return trie.getValuesForKeysStartingWith(value);
}
}
protected static class ConcurrentSuffixTrie extends ConcurrentTrie
{
private final ConcurrentSuffixTree<ConcurrentSkipListSet<DecoratedKey>> trie;
private ConcurrentSuffixTrie(ColumnDefinition column)
{
super(column);
trie = new ConcurrentSuffixTree<>(NODE_FACTORY);
}
public ConcurrentSkipListSet<DecoratedKey> get(String value)
{
return trie.getValueForExactKey(value);
}
public ConcurrentSkipListSet<DecoratedKey> putIfAbsent(String value, ConcurrentSkipListSet<DecoratedKey> newKeys)
{
return trie.putIfAbsent(value, newKeys);
}
public Iterable<ConcurrentSkipListSet<DecoratedKey>> search(String value)
{
return trie.getValuesForKeysContaining(value);
}
}
// This relies on the fact that all of the tree updates are done under exclusive write lock,
// method would overestimate in certain circumstances e.g. when nodes are replaced in place,
// but it's still better comparing to underestimate since it gives more breathing room for other memory users.
private static class SizeEstimatingNodeFactory extends SmartArrayBasedNodeFactory
{
private final ThreadLocal<Long> updateSize = ThreadLocal.withInitial(() -> 0L);
public Node createNode(CharSequence edgeCharacters, Object value, List<Node> childNodes, boolean isRoot)
{
Node node = super.createNode(edgeCharacters, value, childNodes, isRoot);
updateSize.set(updateSize.get() + measure(node));
return node;
}
public long currentUpdateSize()
{
return updateSize.get();
}
public void reset()
{
updateSize.set(0L);
}
private long measure(Node node)
{
// node with max overhead is CharArrayNodeLeafWithValue = 24B
long overhead = 24;
// array of chars (2 bytes) + CharSequence overhead
overhead += 24 + node.getIncomingEdge().length() * 2;
if (node.getOutgoingEdges() != null)
{
// 16 bytes for AtomicReferenceArray
overhead += 16;
overhead += 24 * node.getOutgoingEdges().size();
}
return overhead;
}
}
}

View File

@ -0,0 +1,340 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.plan;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
import org.apache.cassandra.index.sasi.utils.TypeUtil;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Iterators;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Expression
{
private static final Logger logger = LoggerFactory.getLogger(Expression.class);
public enum Op
{
EQ, NOT_EQ, RANGE
}
private final QueryController controller;
public final AbstractAnalyzer analyzer;
public final ColumnIndex index;
public final AbstractType<?> validator;
public final boolean isLiteral;
@VisibleForTesting
protected Op operation;
public Bound lower, upper;
public List<ByteBuffer> exclusions = new ArrayList<>();
public Expression(Expression other)
{
this(other.controller, other.index);
operation = other.operation;
}
public Expression(QueryController controller, ColumnIndex columnIndex)
{
this.controller = controller;
this.index = columnIndex;
this.analyzer = columnIndex.getAnalyzer();
this.validator = columnIndex.getValidator();
this.isLiteral = columnIndex.isLiteral();
}
@VisibleForTesting
public Expression(String name, AbstractType<?> validator)
{
this(null, new ColumnIndex(UTF8Type.instance, ColumnDefinition.regularDef("sasi", "internal", name, validator), null));
}
public Expression setLower(Bound newLower)
{
lower = newLower == null ? null : new Bound(newLower.value, newLower.inclusive);
return this;
}
public Expression setUpper(Bound newUpper)
{
upper = newUpper == null ? null : new Bound(newUpper.value, newUpper.inclusive);
return this;
}
public Expression setOp(Op op)
{
this.operation = op;
return this;
}
public Expression add(Operator op, ByteBuffer value)
{
boolean lowerInclusive = false, upperInclusive = false;
switch (op)
{
case EQ:
lower = new Bound(value, true);
upper = lower;
operation = Op.EQ;
break;
case NEQ:
// index expressions are priority sorted
// and NOT_EQ is the lowest priority, which means that operation type
// is always going to be set before reaching it in case of RANGE or EQ.
if (operation == null)
{
operation = Op.NOT_EQ;
lower = new Bound(value, true);
upper = lower;
}
else
exclusions.add(value);
break;
case LTE:
upperInclusive = true;
case LT:
operation = Op.RANGE;
upper = new Bound(value, upperInclusive);
break;
case GTE:
lowerInclusive = true;
case GT:
operation = Op.RANGE;
lower = new Bound(value, lowerInclusive);
break;
}
return this;
}
public Expression addExclusion(ByteBuffer value)
{
exclusions.add(value);
return this;
}
public boolean contains(ByteBuffer value)
{
if (!TypeUtil.isValid(value, validator))
{
int size = value.remaining();
if ((value = TypeUtil.tryUpcast(value, validator)) == null)
{
logger.error("Can't cast value for {} to size accepted by {}, value size is {} bytes.",
index.getColumnName(),
validator,
size);
return false;
}
}
if (lower != null)
{
// suffix check
if (isLiteral)
{
if (!validateStringValue(value, lower.value))
return false;
}
else
{
// range or (not-)equals - (mainly) for numeric values
int cmp = validator.compare(lower.value, value);
// in case of (NOT_)EQ lower == upper
if (operation == Op.EQ || operation == Op.NOT_EQ)
return cmp == 0;
if (cmp > 0 || (cmp == 0 && !lower.inclusive))
return false;
}
}
if (upper != null && lower != upper)
{
// string (prefix or suffix) check
if (isLiteral)
{
if (!validateStringValue(value, upper.value))
return false;
}
else
{
// range - mainly for numeric values
int cmp = validator.compare(upper.value, value);
if (cmp < 0 || (cmp == 0 && !upper.inclusive))
return false;
}
}
// as a last step let's check exclusions for the given field,
// this covers EQ/RANGE with exclusions.
for (ByteBuffer term : exclusions)
{
if (isLiteral && validateStringValue(value, term))
return false;
else if (validator.compare(term, value) == 0)
return false;
}
return true;
}
private boolean validateStringValue(ByteBuffer columnValue, ByteBuffer requestedValue)
{
analyzer.reset(columnValue.duplicate());
while (analyzer.hasNext())
{
ByteBuffer term = analyzer.next();
if (ByteBufferUtil.contains(term, requestedValue))
return true;
}
return false;
}
public Op getOp()
{
return operation;
}
public void checkpoint()
{
if (controller == null)
return;
controller.checkpoint();
}
public boolean hasLower()
{
return lower != null;
}
public boolean hasUpper()
{
return upper != null;
}
public boolean isLowerSatisfiedBy(OnDiskIndex.DataTerm term)
{
if (!hasLower())
return true;
int cmp = term.compareTo(validator, lower.value, false);
return cmp > 0 || cmp == 0 && lower.inclusive;
}
public boolean isUpperSatisfiedBy(OnDiskIndex.DataTerm term)
{
if (!hasUpper())
return true;
int cmp = term.compareTo(validator, upper.value, false);
return cmp < 0 || cmp == 0 && upper.inclusive;
}
public boolean isIndexed()
{
return index.isIndexed();
}
public String toString()
{
return String.format("Expression{name: %s, op: %s, lower: (%s, %s), upper: (%s, %s), exclusions: %s}",
index.getColumnName(),
operation,
lower == null ? "null" : validator.getString(lower.value),
lower != null && lower.inclusive,
upper == null ? "null" : validator.getString(upper.value),
upper != null && upper.inclusive,
Iterators.toString(Iterators.transform(exclusions.iterator(), validator::getString)));
}
public int hashCode()
{
return new HashCodeBuilder().append(index.getColumnName())
.append(operation)
.append(validator)
.append(lower).append(upper)
.append(exclusions).build();
}
public boolean equals(Object other)
{
if (!(other instanceof Expression))
return false;
if (this == other)
return true;
Expression o = (Expression) other;
return Objects.equals(index.getColumnName(), o.index.getColumnName())
&& validator.equals(o.validator)
&& operation == o.operation
&& Objects.equals(lower, o.lower)
&& Objects.equals(upper, o.upper)
&& exclusions.equals(o.exclusions);
}
public static class Bound
{
public final ByteBuffer value;
public final boolean inclusive;
public Bound(ByteBuffer value, boolean inclusive)
{
this.value = value;
this.inclusive = inclusive;
}
public boolean equals(Object other)
{
if (!(other instanceof Bound))
return false;
Bound o = (Bound) other;
return value.equals(o.value) && inclusive == o.inclusive;
}
}
}

View File

@ -0,0 +1,477 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.plan;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.*;
import org.apache.cassandra.config.ColumnDefinition;
import org.apache.cassandra.config.ColumnDefinition.Kind;
import org.apache.cassandra.cql3.Operator;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.rows.Row;
import org.apache.cassandra.db.rows.Unfiltered;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.analyzer.AbstractAnalyzer;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Expression.Op;
import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.*;
import org.apache.cassandra.utils.FBUtilities;
public class Operation extends RangeIterator<Long, Token>
{
public enum OperationType
{
AND, OR;
public boolean apply(boolean a, boolean b)
{
switch (this)
{
case OR:
return a | b;
case AND:
return a & b;
default:
throw new AssertionError();
}
}
}
private final QueryController controller;
protected final OperationType op;
protected final ListMultimap<ColumnDefinition, Expression> expressions;
protected final RangeIterator<Long, Token> range;
protected Operation left, right;
private Operation(OperationType operation,
QueryController controller,
ListMultimap<ColumnDefinition, Expression> expressions,
RangeIterator<Long, Token> range,
Operation left, Operation right)
{
super(range);
this.op = operation;
this.controller = controller;
this.expressions = expressions;
this.range = range;
this.left = left;
this.right = right;
}
/**
* Recursive "satisfies" checks based on operation
* and data from the lower level members using depth-first search
* and bubbling the results back to the top level caller.
*
* Most of the work here is done by {@link #localSatisfiedBy(Unfiltered, boolean)}
* see it's comment for details, if there are no local expressions
* assigned to Operation it will call satisfiedBy(Row) on it's children.
*
* Query: first_name = X AND (last_name = Y OR address = XYZ AND street = IL AND city = C) OR (state = 'CA' AND country = 'US')
* Row: key1: (first_name: X, last_name: Z, address: XYZ, street: IL, city: C, state: NY, country:US)
*
* #1 OR
* / \
* #2 (first_name) AND AND (state, country)
* \
* #3 (last_name) OR
* \
* #4 AND (address, street, city)
*
*
* Evaluation of the key1 is top-down depth-first search:
*
* --- going down ---
* Level #1 is evaluated, OR expression has to pull results from it's children which are at level #2 and OR them together,
* Level #2 AND (state, country) could be be evaluated right away, AND (first_name) refers to it's "right" child from level #3
* Level #3 OR (last_name) requests results from level #4
* Level #4 AND (address, street, city) does logical AND between it's 3 fields, returns result back to level #3.
* --- bubbling up ---
* Level #3 computes OR between AND (address, street, city) result and it's "last_name" expression
* Level #2 computes AND between "first_name" and result of level #3, AND (state, country) which is already computed
* Level #1 does OR between results of AND (first_name) and AND (state, country) and returns final result.
*
* @param row The row to check.
* @return true if give Row satisfied all of the expressions in the tree,
* false otherwise.
*/
public boolean satisfiedBy(Unfiltered row, boolean allowMissingColumns)
{
boolean sideL, sideR;
if (expressions == null || expressions.isEmpty())
{
sideL = left != null && left.satisfiedBy(row, allowMissingColumns);
sideR = right != null && right.satisfiedBy(row, allowMissingColumns);
// one of the expressions was skipped
// because it had no indexes attached
if (left == null)
return sideR;
}
else
{
sideL = localSatisfiedBy(row, allowMissingColumns);
// if there is no right it means that this expression
// is last in the sequence, we can just return result from local expressions
if (right == null)
return sideL;
sideR = right.satisfiedBy(row, allowMissingColumns);
}
return op.apply(sideL, sideR);
}
/**
* Check every expression in the analyzed list to figure out if the
* columns in the give row match all of the based on the operation
* set to the current operation node.
*
* The algorithm is as follows: for every given expression from analyzed
* list get corresponding column from the Row:
* - apply {@link Expression#contains(ByteBuffer)}
* method to figure out if it's satisfied;
* - apply logical operation between boolean accumulator and current boolean result;
* - if result == false and node's operation is AND return right away;
*
* After all of the expressions have been evaluated return resulting accumulator variable.
*
* Example:
*
* Operation = (op: AND, columns: [first_name = p, 5 < age < 7, last_name: y])
* Row = (first_name: pavel, last_name: y, age: 6, timestamp: 15)
*
* #1 get "first_name" = p (expressions)
* - row-get "first_name" => "pavel"
* - compare "pavel" against "p" => true (current)
* - set accumulator current => true (because this is expression #1)
*
* #2 get "last_name" = y (expressions)
* - row-get "last_name" => "y"
* - compare "y" against "y" => true (current)
* - set accumulator to accumulator & current => true
*
* #3 get 5 < "age" < 7 (expressions)
* - row-get "age" => "6"
* - compare 5 < 6 < 7 => true (current)
* - set accumulator to accumulator & current => true
*
* #4 return accumulator => true (row satisfied all of the conditions)
*
* @param row The row to check.
* @return true if give Row satisfied all of the analyzed expressions,
* false otherwise.
*/
private boolean localSatisfiedBy(Unfiltered row, boolean allowMissingColumns)
{
if (row == null || !row.isRow())
return false;
final int now = FBUtilities.nowInSeconds();
boolean result = false;
int idx = 0;
for (ColumnDefinition column : expressions.keySet())
{
if (column.kind == Kind.PARTITION_KEY)
continue;
ByteBuffer value = ColumnIndex.getValueOf(column, (Row) row, now);
boolean isMissingColumn = value == null;
if (!allowMissingColumns && isMissingColumn)
throw new IllegalStateException("All indexed columns should be included into the column slice, missing: " + column);
boolean isMatch = false;
// If there is a column with multiple expressions that effectively means an OR
// e.g. comment = 'x y z' could be split into 'comment' EQ 'x', 'comment' EQ 'y', 'comment' EQ 'z'
// by analyzer, in situation like that we only need to check if at least one of expressions matches,
// and there is no hit on the NOT_EQ (if any) which are always at the end of the filter list.
// Loop always starts from the end of the list, which makes it possible to break after the last
// NOT_EQ condition on first EQ/RANGE condition satisfied, instead of checking every
// single expression in the column filter list.
List<Expression> filters = expressions.get(column);
for (int i = filters.size() - 1; i >= 0; i--)
{
Expression expression = filters.get(i);
isMatch = !isMissingColumn && expression.contains(value);
if (expression.getOp() == Op.NOT_EQ)
{
// since this is NOT_EQ operation we have to
// inverse match flag (to check against other expressions),
// and break in case of negative inverse because that means
// that it's a positive hit on the not-eq clause.
isMatch = !isMatch;
if (!isMatch)
break;
} // if it was a match on EQ/RANGE or column is missing
else if (isMatch || isMissingColumn)
break;
}
if (idx++ == 0)
{
result = isMatch;
continue;
}
result = op.apply(result, isMatch);
// exit early because we already got a single false
if (op == OperationType.AND && !result)
return false;
}
return idx == 0 || result;
}
@VisibleForTesting
protected static ListMultimap<ColumnDefinition, Expression> analyzeGroup(QueryController controller,
OperationType op,
List<RowFilter.Expression> expressions)
{
ListMultimap<ColumnDefinition, Expression> analyzed = ArrayListMultimap.create();
// sort all of the expressions in the operation by name and priority of the logical operator
// this gives us an efficient way to handle inequality and combining into ranges without extra processing
// and converting expressions from one type to another.
Collections.sort(expressions, (a, b) -> {
int cmp = a.column().compareTo(b.column());
return cmp == 0 ? -Integer.compare(getPriority(a.operator()), getPriority(b.operator())) : cmp;
});
for (final RowFilter.Expression e : expressions)
{
ColumnIndex columnIndex = controller.getIndex(e);
List<Expression> perColumn = analyzed.get(e.column());
if (columnIndex == null)
columnIndex = new ColumnIndex(controller.getKeyValidator(), e.column(), null);
AbstractAnalyzer analyzer = columnIndex.getAnalyzer();
analyzer.reset(e.getIndexValue());
// EQ/NOT_EQ can have multiple expressions e.g. text = "Hello World",
// becomes text = "Hello" OR text = "World" because "space" is always interpreted as a split point (by analyzer),
// NOT_EQ is made an independent expression only in case of pre-existing multiple EQ expressions, or
// if there is no EQ operations and NOT_EQ is met or a single NOT_EQ expression present,
// in such case we know exactly that there would be no more EQ/RANGE expressions for given column
// since NOT_EQ has the lowest priority.
if (e.operator() == Operator.EQ
|| (e.operator() == Operator.NEQ
&& (perColumn.size() == 0 || perColumn.size() > 1
|| (perColumn.size() == 1 && perColumn.get(0).getOp() == Op.NOT_EQ))))
{
while (analyzer.hasNext())
{
final ByteBuffer token = analyzer.next();
perColumn.add(new Expression(controller, columnIndex).add(e.operator(), token));
}
}
else
// "range" or not-equals operator, combines both bounds together into the single expression,
// iff operation of the group is AND, otherwise we are forced to create separate expressions,
// not-equals is combined with the range iff operator is AND.
{
Expression range;
if (perColumn.size() == 0 || op != OperationType.AND)
perColumn.add((range = new Expression(controller, columnIndex)));
else
range = Iterables.getLast(perColumn);
while (analyzer.hasNext())
range.add(e.operator(), analyzer.next());
}
}
return analyzed;
}
private static int getPriority(Operator op)
{
switch (op)
{
case EQ:
return 4;
case GTE:
case GT:
return 3;
case LTE:
case LT:
return 2;
case NEQ:
return 1;
default:
return 0;
}
}
protected Token computeNext()
{
return range != null && range.hasNext() ? range.next() : endOfData();
}
protected void performSkipTo(Long nextToken)
{
if (range != null)
range.skipTo(nextToken);
}
public void close() throws IOException
{
controller.releaseIndexes(this);
}
public static class Builder
{
private final QueryController controller;
protected final OperationType op;
protected final List<RowFilter.Expression> expressions;
protected Builder left, right;
public Builder(OperationType operation, QueryController controller, RowFilter.Expression... columns)
{
this.op = operation;
this.controller = controller;
this.expressions = new ArrayList<>();
Collections.addAll(expressions, columns);
}
public Builder setRight(Builder operation)
{
this.right = operation;
return this;
}
public Builder setLeft(Builder operation)
{
this.left = operation;
return this;
}
public void add(RowFilter.Expression e)
{
expressions.add(e);
}
public void add(Collection<RowFilter.Expression> newExpressions)
{
if (expressions != null)
expressions.addAll(newExpressions);
}
public Operation complete()
{
if (!expressions.isEmpty())
{
ListMultimap<ColumnDefinition, Expression> analyzedExpressions = analyzeGroup(controller, op, expressions);
RangeIterator.Builder<Long, Token> range = controller.getIndexes(op, analyzedExpressions.values());
Operation rightOp = null;
if (right != null)
{
rightOp = right.complete();
range.add(rightOp);
}
return new Operation(op, controller, analyzedExpressions, range.build(), null, rightOp);
}
else
{
Operation leftOp = null, rightOp = null;
boolean leftIndexes = false, rightIndexes = false;
if (left != null)
{
leftOp = left.complete();
leftIndexes = leftOp != null && leftOp.range != null;
}
if (right != null)
{
rightOp = right.complete();
rightIndexes = rightOp != null && rightOp.range != null;
}
RangeIterator<Long, Token> join;
/**
* Operation should allow one of it's sub-trees to wrap no indexes, that is related to the fact that we
* have to accept defined-but-not-indexed columns as well as key range as IndexExpressions.
*
* Two cases are possible:
*
* only left child produced indexed iterators, that could happen when there are two columns
* or key range on the right:
*
* AND
* / \
* OR \
* / \ AND
* a b / \
* key key
*
* only right child produced indexed iterators:
*
* AND
* / \
* AND a
* / \
* key key
*/
if (leftIndexes && !rightIndexes)
join = leftOp;
else if (!leftIndexes && rightIndexes)
join = rightOp;
else if (leftIndexes)
{
RangeIterator.Builder<Long, Token> builder = op == OperationType.OR
? RangeUnionIterator.<Long, Token>builder()
: RangeIntersectionIterator.<Long, Token>builder();
join = builder.add(leftOp).add(rightOp).build();
}
else
throw new AssertionError("both sub-trees have 0 indexes.");
return new Operation(op, controller, null, join, leftOp, rightOp);
}
}
}
}

View File

@ -0,0 +1,261 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.plan;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.ColumnFamilyStore.RefViewFragment;
import org.apache.cassandra.db.filter.DataLimits;
import org.apache.cassandra.db.filter.RowFilter;
import org.apache.cassandra.db.lifecycle.SSTableSet;
import org.apache.cassandra.db.rows.UnfilteredRowIterator;
import org.apache.cassandra.index.Index;
import org.apache.cassandra.index.sasi.SASIIndex;
import org.apache.cassandra.index.sasi.SSTableIndex;
import org.apache.cassandra.index.sasi.TermIterator;
import org.apache.cassandra.index.sasi.conf.ColumnIndex;
import org.apache.cassandra.index.sasi.conf.view.View;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.exceptions.TimeQuotaExceededException;
import org.apache.cassandra.index.sasi.plan.Operation.OperationType;
import org.apache.cassandra.index.sasi.utils.RangeIntersectionIterator;
import org.apache.cassandra.index.sasi.utils.RangeIterator;
import org.apache.cassandra.index.sasi.utils.RangeUnionIterator;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.sstable.format.SSTableReader;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.utils.Pair;
public class QueryController
{
private final long executionQuota;
private final long executionStart;
private final ColumnFamilyStore cfs;
private final PartitionRangeReadCommand command;
private final Map<Collection<Expression>, List<RangeIterator<Long, Token>>> resources = new HashMap<>();
private final RefViewFragment scope;
private final Set<SSTableReader> sstables;
public QueryController(ColumnFamilyStore cfs, PartitionRangeReadCommand command, long timeQuotaMs)
{
this.cfs = cfs;
this.command = command;
this.executionQuota = TimeUnit.MILLISECONDS.toNanos(timeQuotaMs);
this.executionStart = System.nanoTime();
this.scope = getSSTableScope(cfs, command);
this.sstables = new HashSet<>(scope.sstables);
}
public boolean isForThrift()
{
return command.isForThrift();
}
public CFMetaData metadata()
{
return command.metadata();
}
public Collection<RowFilter.Expression> getExpressions()
{
return command.rowFilter().getExpressions();
}
public DataRange dataRange()
{
return command.dataRange();
}
public AbstractType<?> getKeyValidator()
{
return cfs.metadata.getKeyValidator();
}
public ColumnIndex getIndex(RowFilter.Expression expression)
{
Optional<Index> index = cfs.indexManager.getBestIndexFor(expression);
return index.isPresent() ? ((SASIIndex) index.get()).getIndex() : null;
}
public UnfilteredRowIterator getPartition(DecoratedKey key, ReadExecutionController executionController)
{
if (key == null)
throw new NullPointerException();
try
{
SinglePartitionReadCommand partition = SinglePartitionReadCommand.create(command.isForThrift(),
cfs.metadata,
command.nowInSec(),
command.columnFilter(),
command.rowFilter().withoutExpressions(),
DataLimits.NONE,
key,
command.clusteringIndexFilter(key));
return partition.queryMemtableAndDisk(cfs, executionController.baseReadOpOrderGroup());
}
finally
{
checkpoint();
}
}
/**
* Build a range iterator from the given list of expressions by applying given operation (OR/AND).
* Building of such iterator involves index search, results of which are persisted in the internal resources list
* and can be released later via {@link QueryController#releaseIndexes(Operation)}.
*
* @param op The operation type to coalesce expressions with.
* @param expressions The expressions to build range iterator from (expressions with not results are ignored).
*
* @return The range builder based on given expressions and operation type.
*/
public RangeIterator.Builder<Long, Token> getIndexes(OperationType op, Collection<Expression> expressions)
{
if (resources.containsKey(expressions))
throw new IllegalArgumentException("Can't process the same expressions multiple times.");
RangeIterator.Builder<Long, Token> builder = op == OperationType.OR
? RangeUnionIterator.<Long, Token>builder()
: RangeIntersectionIterator.<Long, Token>builder();
List<RangeIterator<Long, Token>> perIndexUnions = new ArrayList<>();
for (Map.Entry<Expression, Set<SSTableIndex>> e : getView(op, expressions).entrySet())
{
RangeIterator<Long, Token> index = TermIterator.build(e.getKey(), e.getValue());
if (index == null)
continue;
builder.add(index);
perIndexUnions.add(index);
}
resources.put(expressions, perIndexUnions);
return builder;
}
public void checkpoint()
{
if ((System.nanoTime() - executionStart) >= executionQuota)
throw new TimeQuotaExceededException();
}
public void releaseIndexes(Operation operation)
{
if (operation.expressions != null)
releaseIndexes(resources.remove(operation.expressions.values()));
}
private void releaseIndexes(List<RangeIterator<Long, Token>> indexes)
{
if (indexes == null)
return;
indexes.forEach(FileUtils::closeQuietly);
}
public void finish()
{
try
{
resources.values().forEach(this::releaseIndexes);
}
finally
{
scope.release();
}
}
private Map<Expression, Set<SSTableIndex>> getView(OperationType op, Collection<Expression> expressions)
{
// first let's determine the primary expression if op is AND
Pair<Expression, Set<SSTableIndex>> primary = (op == OperationType.AND) ? calculatePrimary(expressions) : null;
Map<Expression, Set<SSTableIndex>> indexes = new HashMap<>();
for (Expression e : expressions)
{
// NO_EQ and non-index column query should only act as FILTER BY for satisfiedBy(Row) method
// because otherwise it likely to go through the whole index.
if (!e.isIndexed() || e.getOp() == Expression.Op.NOT_EQ)
continue;
// primary expression, we'll have to add as is
if (primary != null && e.equals(primary.left))
{
indexes.put(primary.left, primary.right);
continue;
}
View view = e.index.getView();
if (view == null)
continue;
Set<SSTableIndex> readers = new HashSet<>();
if (primary != null && primary.right.size() > 0)
{
for (SSTableIndex index : primary.right)
readers.addAll(view.match(index.minKey(), index.maxKey()));
}
else
{
readers.addAll(view.match(sstables, e));
}
indexes.put(e, readers);
}
return indexes;
}
private Pair<Expression, Set<SSTableIndex>> calculatePrimary(Collection<Expression> expressions)
{
Expression expression = null;
Set<SSTableIndex> primaryIndexes = Collections.emptySet();
for (Expression e : expressions)
{
if (!e.isIndexed())
continue;
View view = e.index.getView();
if (view == null)
continue;
Set<SSTableIndex> indexes = view.match(sstables, e);
if (primaryIndexes.size() > indexes.size())
{
primaryIndexes = indexes;
expression = e;
}
}
return expression == null ? null : Pair.create(expression, primaryIndexes);
}
private static RefViewFragment getSSTableScope(ColumnFamilyStore cfs, PartitionRangeReadCommand command)
{
return cfs.selectAndReference(org.apache.cassandra.db.lifecycle.View.select(SSTableSet.CANONICAL, command.dataRange().keyRange()));
}
}

View File

@ -0,0 +1,170 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.plan;
import java.util.*;
import org.apache.cassandra.config.CFMetaData;
import org.apache.cassandra.db.*;
import org.apache.cassandra.db.partitions.UnfilteredPartitionIterator;
import org.apache.cassandra.db.rows.*;
import org.apache.cassandra.dht.AbstractBounds;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.plan.Operation.OperationType;
import org.apache.cassandra.exceptions.RequestTimeoutException;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.utils.AbstractIterator;
public class QueryPlan
{
private final QueryController controller;
public QueryPlan(ColumnFamilyStore cfs, ReadCommand command, long executionQuotaMs)
{
this.controller = new QueryController(cfs, (PartitionRangeReadCommand) command, executionQuotaMs);
}
/**
* Converts expressions into operation tree (which is currently just a single AND).
*
* Operation tree allows us to do a couple of important optimizations
* namely, group flattening for AND operations (query rewrite), expression bounds checks,
* "satisfies by" checks for resulting rows with an early exit.
*
* @return root of the operations tree.
*/
private Operation analyze()
{
try
{
Operation.Builder and = new Operation.Builder(OperationType.AND, controller);
controller.getExpressions().forEach(and::add);
return and.complete();
}
catch (Exception | Error e)
{
controller.finish();
throw e;
}
}
public UnfilteredPartitionIterator execute(ReadExecutionController executionController) throws RequestTimeoutException
{
return new ResultIterator(analyze(), controller, executionController);
}
private static class ResultIterator extends AbstractIterator<UnfilteredRowIterator> implements UnfilteredPartitionIterator
{
private final AbstractBounds<PartitionPosition> keyRange;
private final Operation operationTree;
private final QueryController controller;
private final ReadExecutionController executionController;
private Iterator<DecoratedKey> currentKeys = null;
public ResultIterator(Operation operationTree, QueryController controller, ReadExecutionController executionController)
{
this.keyRange = controller.dataRange().keyRange();
this.operationTree = operationTree;
this.controller = controller;
this.executionController = executionController;
if (operationTree != null)
operationTree.skipTo((Long) keyRange.left.getToken().getTokenValue());
}
protected UnfilteredRowIterator computeNext()
{
if (operationTree == null)
return endOfData();
for (;;)
{
if (currentKeys == null || !currentKeys.hasNext())
{
if (!operationTree.hasNext())
return endOfData();
Token token = operationTree.next();
currentKeys = token.iterator();
}
while (currentKeys.hasNext())
{
DecoratedKey key = currentKeys.next();
if (!keyRange.right.isMinimum() && keyRange.right.compareTo(key) < 0)
return endOfData();
try (UnfilteredRowIterator partition = controller.getPartition(key, executionController))
{
List<Unfiltered> clusters = new ArrayList<>();
while (partition.hasNext())
{
Unfiltered row = partition.next();
if (operationTree.satisfiedBy(row, true))
clusters.add(row);
}
if (!clusters.isEmpty())
return new PartitionIterator(partition, clusters);
}
}
}
}
private static class PartitionIterator extends AbstractUnfilteredRowIterator
{
private final Iterator<Unfiltered> rows;
public PartitionIterator(UnfilteredRowIterator partition, Collection<Unfiltered> content)
{
super(partition.metadata(),
partition.partitionKey(),
partition.partitionLevelDeletion(),
partition.columns(),
partition.staticRow(),
partition.isReverseOrder(),
partition.stats());
rows = content.iterator();
}
@Override
protected Unfiltered computeNext()
{
return rows.hasNext() ? rows.next() : endOfData();
}
}
public boolean isForThrift()
{
return controller.isForThrift();
}
public CFMetaData metadata()
{
return controller.metadata();
}
public void close()
{
FileUtils.closeQuietly(operationTree);
controller.finish();
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.sa;
import java.nio.ByteBuffer;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
public class ByteTerm extends Term<ByteBuffer>
{
public ByteTerm(int position, ByteBuffer value, TokenTreeBuilder tokens)
{
super(position, value, tokens);
}
public ByteBuffer getTerm()
{
return value.duplicate();
}
public ByteBuffer getSuffix(int start)
{
return (ByteBuffer) value.duplicate().position(value.position() + start);
}
public int compareTo(AbstractType<?> comparator, Term other)
{
return comparator.compare(value, (ByteBuffer) other.value);
}
public int length()
{
return value.remaining();
}
}

View File

@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.sa;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
import com.google.common.base.Charsets;
public class CharTerm extends Term<CharBuffer>
{
public CharTerm(int position, CharBuffer value, TokenTreeBuilder tokens)
{
super(position, value, tokens);
}
public ByteBuffer getTerm()
{
return Charsets.UTF_8.encode(value.duplicate());
}
public ByteBuffer getSuffix(int start)
{
return Charsets.UTF_8.encode(value.subSequence(value.position() + start, value.remaining()));
}
public int compareTo(AbstractType<?> comparator, Term other)
{
return value.compareTo((CharBuffer) other.value);
}
public int length()
{
return value.length();
}
}

View File

@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.sa;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.utils.Pair;
public class IntegralSA extends SA<ByteBuffer>
{
public IntegralSA(AbstractType<?> comparator, OnDiskIndexBuilder.Mode mode)
{
super(comparator, mode);
}
public Term<ByteBuffer> getTerm(ByteBuffer termValue, TokenTreeBuilder tokens)
{
return new ByteTerm(charCount, termValue, tokens);
}
public TermIterator finish()
{
return new IntegralSuffixIterator();
}
private class IntegralSuffixIterator extends TermIterator
{
private final Iterator<Term<ByteBuffer>> termIterator;
public IntegralSuffixIterator()
{
Collections.sort(terms, new Comparator<Term<?>>()
{
public int compare(Term<?> a, Term<?> b)
{
return a.compareTo(comparator, b);
}
});
termIterator = terms.iterator();
}
public ByteBuffer minTerm()
{
return terms.get(0).getTerm();
}
public ByteBuffer maxTerm()
{
return terms.get(terms.size() - 1).getTerm();
}
protected Pair<ByteBuffer, TokenTreeBuilder> computeNext()
{
if (!termIterator.hasNext())
return endOfData();
Term<ByteBuffer> term = termIterator.next();
return Pair.create(term.getTerm(), term.getTokens().finish());
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.sa;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder.Mode;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
public abstract class SA<T extends Buffer>
{
protected final AbstractType<?> comparator;
protected final Mode mode;
protected final List<Term<T>> terms = new ArrayList<>();
protected int charCount = 0;
public SA(AbstractType<?> comparator, Mode mode)
{
this.comparator = comparator;
this.mode = mode;
}
public Mode getMode()
{
return mode;
}
public void add(ByteBuffer termValue, TokenTreeBuilder tokens)
{
Term<T> term = getTerm(termValue, tokens);
terms.add(term);
charCount += term.length();
}
public abstract TermIterator finish();
protected abstract Term<T> getTerm(ByteBuffer termValue, TokenTreeBuilder tokens);
}

View File

@ -0,0 +1,143 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.sa;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import org.apache.cassandra.index.sasi.disk.OnDiskIndexBuilder;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.utils.Pair;
import com.google.common.base.Charsets;
import net.mintern.primitive.Primitive;
public class SuffixSA extends SA<CharBuffer>
{
public SuffixSA(AbstractType<?> comparator, OnDiskIndexBuilder.Mode mode)
{
super(comparator, mode);
}
protected Term<CharBuffer> getTerm(ByteBuffer termValue, TokenTreeBuilder tokens)
{
return new CharTerm(charCount, Charsets.UTF_8.decode(termValue.duplicate()), tokens);
}
public TermIterator finish()
{
return new SASuffixIterator();
}
private class SASuffixIterator extends TermIterator
{
private final long[] suffixes;
private int current = 0;
private ByteBuffer lastProcessedSuffix;
private TokenTreeBuilder container;
public SASuffixIterator()
{
// each element has term index and char position encoded as two 32-bit integers
// to avoid binary search per suffix while sorting suffix array.
suffixes = new long[charCount];
long termIndex = -1, currentTermLength = -1;
for (int i = 0; i < charCount; i++)
{
if (i >= currentTermLength || currentTermLength == -1)
{
Term currentTerm = terms.get((int) ++termIndex);
currentTermLength = currentTerm.getPosition() + currentTerm.length();
}
suffixes[i] = (termIndex << 32) | i;
}
Primitive.sort(suffixes, (a, b) -> {
Term aTerm = terms.get((int) (a >>> 32));
Term bTerm = terms.get((int) (b >>> 32));
return comparator.compare(aTerm.getSuffix(((int) a) - aTerm.getPosition()),
bTerm.getSuffix(((int) b) - bTerm.getPosition()));
});
}
private Pair<ByteBuffer, TokenTreeBuilder> suffixAt(int position)
{
long index = suffixes[position];
Term term = terms.get((int) (index >>> 32));
return Pair.create(term.getSuffix(((int) index) - term.getPosition()), term.getTokens());
}
public ByteBuffer minTerm()
{
return suffixAt(0).left;
}
public ByteBuffer maxTerm()
{
return suffixAt(suffixes.length - 1).left;
}
protected Pair<ByteBuffer, TokenTreeBuilder> computeNext()
{
while (true)
{
if (current >= suffixes.length)
{
if (lastProcessedSuffix == null)
return endOfData();
Pair<ByteBuffer, TokenTreeBuilder> result = finishSuffix();
lastProcessedSuffix = null;
return result;
}
Pair<ByteBuffer, TokenTreeBuilder> suffix = suffixAt(current++);
if (lastProcessedSuffix == null)
{
lastProcessedSuffix = suffix.left;
container = new TokenTreeBuilder(suffix.right.getTokens());
}
else if (comparator.compare(lastProcessedSuffix, suffix.left) == 0)
{
lastProcessedSuffix = suffix.left;
container.add(suffix.right.getTokens());
}
else
{
Pair<ByteBuffer, TokenTreeBuilder> result = finishSuffix();
lastProcessedSuffix = suffix.left;
container = new TokenTreeBuilder(suffix.right.getTokens());
return result;
}
}
}
private Pair<ByteBuffer, TokenTreeBuilder> finishSuffix()
{
return Pair.create(lastProcessedSuffix, container.finish());
}
}
}

View File

@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.sa;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
public abstract class Term<T extends Buffer>
{
protected final int position;
protected final T value;
protected TokenTreeBuilder tokens;
public Term(int position, T value, TokenTreeBuilder tokens)
{
this.position = position;
this.value = value;
this.tokens = tokens;
}
public int getPosition()
{
return position;
}
public abstract ByteBuffer getTerm();
public abstract ByteBuffer getSuffix(int start);
public TokenTreeBuilder getTokens()
{
return tokens;
}
public abstract int compareTo(AbstractType<?> comparator, Term other);
public abstract int length();
}

View File

@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.sa;
import java.nio.ByteBuffer;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.utils.Pair;
import com.google.common.collect.AbstractIterator;
public abstract class TermIterator extends AbstractIterator<Pair<ByteBuffer, TokenTreeBuilder>>
{
public abstract ByteBuffer minTerm();
public abstract ByteBuffer maxTerm();
}

View File

@ -0,0 +1,155 @@
/*
* Copyright (C) 2007 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.util.NoSuchElementException;
import com.google.common.collect.PeekingIterator;
import static com.google.common.base.Preconditions.checkState;
// This is fork of the Guava AbstractIterator, the only difference
// is that state & next variables are now protected, this was required
// for SkippableIterator.skipTo(..) to void all previous state.
public abstract class AbstractIterator<T> implements PeekingIterator<T>
{
protected State state = State.NOT_READY;
/** Constructor for use by subclasses. */
protected AbstractIterator() {}
protected enum State
{
/** We have computed the next element and haven't returned it yet. */
READY,
/** We haven't yet computed or have already returned the element. */
NOT_READY,
/** We have reached the end of the data and are finished. */
DONE,
/** We've suffered an exception and are kaput. */
FAILED,
}
protected T next;
/**
* Returns the next element. <b>Note:</b> the implementation must call {@link
* #endOfData()} when there are no elements left in the iteration. Failure to
* do so could result in an infinite loop.
*
* <p>The initial invocation of {@link #hasNext()} or {@link #next()} calls
* this method, as does the first invocation of {@code hasNext} or {@code
* next} following each successful call to {@code next}. Once the
* implementation either invokes {@code endOfData} or throws an exception,
* {@code computeNext} is guaranteed to never be called again.
*
* <p>If this method throws an exception, it will propagate outward to the
* {@code hasNext} or {@code next} invocation that invoked this method. Any
* further attempts to use the iterator will result in an {@link
* IllegalStateException}.
*
* <p>The implementation of this method may not invoke the {@code hasNext},
* {@code next}, or {@link #peek()} methods on this instance; if it does, an
* {@code IllegalStateException} will result.
*
* @return the next element if there was one. If {@code endOfData} was called
* during execution, the return value will be ignored.
* @throws RuntimeException if any unrecoverable error happens. This exception
* will propagate outward to the {@code hasNext()}, {@code next()}, or
* {@code peek()} invocation that invoked this method. Any further
* attempts to use the iterator will result in an
* {@link IllegalStateException}.
*/
protected abstract T computeNext();
/**
* Implementations of {@link #computeNext} <b>must</b> invoke this method when
* there are no elements left in the iteration.
*
* @return {@code null}; a convenience so your {@code computeNext}
* implementation can use the simple statement {@code return endOfData();}
*/
protected final T endOfData()
{
state = State.DONE;
return null;
}
public final boolean hasNext()
{
checkState(state != State.FAILED);
switch (state)
{
case DONE:
return false;
case READY:
return true;
default:
}
return tryToComputeNext();
}
protected boolean tryToComputeNext()
{
state = State.FAILED; // temporary pessimism
next = computeNext();
if (state != State.DONE)
{
state = State.READY;
return true;
}
return false;
}
public final T next()
{
if (!hasNext())
throw new NoSuchElementException();
state = State.NOT_READY;
return next;
}
public void remove()
{
throw new UnsupportedOperationException();
}
/**
* Returns the next element in the iteration without advancing the iteration,
* according to the contract of {@link PeekingIterator#peek()}.
*
* <p>Implementations of {@code AbstractIterator} that wish to expose this
* functionality should implement {@code PeekingIterator}.
*/
public final T peek()
{
if (!hasNext())
throw new NoSuchElementException();
return next;
}
}

View File

@ -0,0 +1,103 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.nio.ByteBuffer;
import java.util.Map;
import java.util.TreeMap;
import org.apache.cassandra.index.sasi.disk.OnDiskIndex.DataTerm;
import org.apache.cassandra.index.sasi.disk.Token;
import org.apache.cassandra.index.sasi.disk.TokenTree;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.db.marshal.AbstractType;
import com.carrotsearch.hppc.LongOpenHashSet;
import com.carrotsearch.hppc.LongSet;
import com.carrotsearch.hppc.cursors.LongCursor;
public class CombinedTerm implements CombinedValue<DataTerm>
{
private final AbstractType<?> comparator;
private final DataTerm term;
private final TreeMap<Long, LongSet> tokens;
public CombinedTerm(AbstractType<?> comparator, DataTerm term)
{
this.comparator = comparator;
this.term = term;
this.tokens = new TreeMap<>();
RangeIterator<Long, Token> tokens = term.getTokens();
while (tokens.hasNext())
{
Token current = tokens.next();
LongSet offsets = this.tokens.get(current.get());
if (offsets == null)
this.tokens.put(current.get(), (offsets = new LongOpenHashSet()));
for (Long offset : ((TokenTree.OnDiskToken) current).getOffsets())
offsets.add(offset);
}
}
public ByteBuffer getTerm()
{
return term.getTerm();
}
public Map<Long, LongSet> getTokens()
{
return tokens;
}
public TokenTreeBuilder getTokenTreeBuilder()
{
return new TokenTreeBuilder(tokens).finish();
}
public void merge(CombinedValue<DataTerm> other)
{
if (!(other instanceof CombinedTerm))
return;
CombinedTerm o = (CombinedTerm) other;
assert comparator == o.comparator;
for (Map.Entry<Long, LongSet> token : o.tokens.entrySet())
{
LongSet offsets = this.tokens.get(token.getKey());
if (offsets == null)
this.tokens.put(token.getKey(), (offsets = new LongOpenHashSet()));
for (LongCursor offset : token.getValue())
offsets.add(offset.value);
}
}
public DataTerm get()
{
return term;
}
public int compareTo(CombinedValue<DataTerm> o)
{
return term.compareTo(comparator, o.get().getTerm());
}
}

View File

@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.nio.ByteBuffer;
import org.apache.cassandra.index.sasi.disk.Descriptor;
import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
import org.apache.cassandra.index.sasi.disk.TokenTreeBuilder;
import org.apache.cassandra.index.sasi.sa.TermIterator;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.utils.Pair;
public class CombinedTermIterator extends TermIterator
{
final Descriptor descriptor;
final RangeIterator<OnDiskIndex.DataTerm, CombinedTerm> union;
final ByteBuffer min;
final ByteBuffer max;
public CombinedTermIterator(OnDiskIndex... sas)
{
this(Descriptor.CURRENT, sas);
}
public CombinedTermIterator(Descriptor d, OnDiskIndex... parts)
{
descriptor = d;
union = OnDiskIndexIterator.union(parts);
AbstractType<?> comparator = parts[0].getComparator(); // assumes all SAs have same comparator
ByteBuffer minimum = parts[0].minTerm();
ByteBuffer maximum = parts[0].maxTerm();
for (int i = 1; i < parts.length; i++)
{
OnDiskIndex part = parts[i];
if (part == null)
continue;
minimum = comparator.compare(minimum, part.minTerm()) > 0 ? part.minTerm() : minimum;
maximum = comparator.compare(maximum, part.maxTerm()) < 0 ? part.maxTerm() : maximum;
}
min = minimum;
max = maximum;
}
public ByteBuffer minTerm()
{
return min;
}
public ByteBuffer maxTerm()
{
return max;
}
protected Pair<ByteBuffer, TokenTreeBuilder> computeNext()
{
if (!union.hasNext())
{
return endOfData();
}
else
{
CombinedTerm term = union.next();
return Pair.create(term.getTerm(), term.getTokenTreeBuilder());
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
public interface CombinedValue<V> extends Comparable<CombinedValue<V>>
{
void merge(CombinedValue<V> other);
V get();
}

View File

@ -0,0 +1,253 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.io.Closeable;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel.MapMode;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.io.util.ChannelProxy;
import org.apache.cassandra.io.util.FileUtils;
import org.apache.cassandra.io.util.RandomAccessReader;
import com.google.common.annotations.VisibleForTesting;
public class MappedBuffer implements Closeable
{
private final MappedByteBuffer[] pages;
private long position, limit;
private final long capacity;
private final int pageSize, sizeBits;
private MappedBuffer(MappedBuffer other)
{
this.sizeBits = other.sizeBits;
this.pageSize = other.pageSize;
this.position = other.position;
this.limit = other.limit;
this.capacity = other.capacity;
this.pages = other.pages;
}
public MappedBuffer(RandomAccessReader file)
{
this(file.getChannel(), 30);
}
public MappedBuffer(ChannelProxy file)
{
this(file, 30);
}
@VisibleForTesting
protected MappedBuffer(ChannelProxy file, int numPageBits)
{
if (numPageBits > Integer.SIZE - 1)
throw new IllegalArgumentException("page size can't be bigger than 1G");
sizeBits = numPageBits;
pageSize = 1 << sizeBits;
position = 0;
limit = capacity = file.size();
pages = new MappedByteBuffer[(int) (file.size() / pageSize) + 1];
try
{
long offset = 0;
for (int i = 0; i < pages.length; i++)
{
long pageSize = Math.min(this.pageSize, (capacity - offset));
pages[i] = file.map(MapMode.READ_ONLY, offset, pageSize);
offset += pageSize;
}
}
finally
{
file.close();
}
}
public int comparePageTo(long offset, int length, AbstractType<?> comparator, ByteBuffer other)
{
return comparator.compare(getPageRegion(offset, length), other);
}
public long capacity()
{
return capacity;
}
public long position()
{
return position;
}
public MappedBuffer position(long newPosition)
{
if (newPosition < 0 || newPosition > limit)
throw new IllegalArgumentException("position: " + newPosition + ", limit: " + limit);
position = newPosition;
return this;
}
public long limit()
{
return limit;
}
public MappedBuffer limit(long newLimit)
{
if (newLimit < position || newLimit > capacity)
throw new IllegalArgumentException();
limit = newLimit;
return this;
}
public long remaining()
{
return limit - position;
}
public boolean hasRemaining()
{
return remaining() > 0;
}
public byte get()
{
return get(position++);
}
public byte get(long pos)
{
return pages[getPage(pos)].get(getPageOffset(pos));
}
public short getShort()
{
short value = getShort(position);
position += 2;
return value;
}
public short getShort(long pos)
{
if (isPageAligned(pos, 2))
return pages[getPage(pos)].getShort(getPageOffset(pos));
int ch1 = get(pos) & 0xff;
int ch2 = get(pos + 1) & 0xff;
return (short) ((ch1 << 8) + ch2);
}
public int getInt()
{
int value = getInt(position);
position += 4;
return value;
}
public int getInt(long pos)
{
if (isPageAligned(pos, 4))
return pages[getPage(pos)].getInt(getPageOffset(pos));
int ch1 = get(pos) & 0xff;
int ch2 = get(pos + 1) & 0xff;
int ch3 = get(pos + 2) & 0xff;
int ch4 = get(pos + 3) & 0xff;
return ((ch1 << 24) + (ch2 << 16) + (ch3 << 8) + ch4);
}
public long getLong()
{
long value = getLong(position);
position += 8;
return value;
}
public long getLong(long pos)
{
// fast path if the long could be retrieved from a single page
// that would avoid multiple expensive look-ups into page array.
return (isPageAligned(pos, 8))
? pages[getPage(pos)].getLong(getPageOffset(pos))
: ((long) (getInt(pos)) << 32) + (getInt(pos + 4) & 0xFFFFFFFFL);
}
public ByteBuffer getPageRegion(long position, int length)
{
if (!isPageAligned(position, length))
throw new IllegalArgumentException(String.format("range: %s-%s wraps more than one page", position, length));
ByteBuffer slice = pages[getPage(position)].duplicate();
int pageOffset = getPageOffset(position);
slice.position(pageOffset).limit(pageOffset + length);
return slice;
}
public MappedBuffer duplicate()
{
return new MappedBuffer(this);
}
public void close()
{
if (!FileUtils.isCleanerAvailable())
return;
/*
* Try forcing the unmapping of pages using undocumented unsafe sun APIs.
* If this fails (non Sun JVM), we'll have to wait for the GC to finalize the mapping.
* If this works and a thread tries to access any page, hell will unleash on earth.
*/
try
{
for (MappedByteBuffer segment : pages)
FileUtils.clean(segment);
}
catch (Exception e)
{
// This is not supposed to happen
}
}
private int getPage(long position)
{
return (int) (position >> sizeBits);
}
private int getPageOffset(long position)
{
return (int) (position & pageSize - 1);
}
private boolean isPageAligned(long position, int length)
{
return pageSize - (getPageOffset(position) + length) > 0;
}
}

View File

@ -0,0 +1,64 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.io.IOException;
import java.util.Iterator;
import org.apache.cassandra.index.sasi.disk.OnDiskIndex;
import org.apache.cassandra.index.sasi.disk.OnDiskIndex.DataTerm;
import org.apache.cassandra.db.marshal.AbstractType;
public class OnDiskIndexIterator extends RangeIterator<DataTerm, CombinedTerm>
{
private final AbstractType<?> comparator;
private final Iterator<DataTerm> terms;
public OnDiskIndexIterator(OnDiskIndex index)
{
super(index.min(), index.max(), Long.MAX_VALUE);
this.comparator = index.getComparator();
this.terms = index.iterator();
}
public static RangeIterator<DataTerm, CombinedTerm> union(OnDiskIndex... union)
{
RangeUnionIterator.Builder<DataTerm, CombinedTerm> builder = RangeUnionIterator.builder();
for (OnDiskIndex e : union)
{
if (e != null)
builder.add(new OnDiskIndexIterator(e));
}
return builder.build();
}
protected CombinedTerm computeNext()
{
return terms.hasNext() ? new CombinedTerm(comparator, terms.next()) : endOfData();
}
protected void performSkipTo(DataTerm nextToken)
{
throw new UnsupportedOperationException();
}
public void close() throws IOException
{}
}

View File

@ -0,0 +1,281 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import com.google.common.collect.Iterators;
import org.apache.cassandra.io.util.FileUtils;
import com.google.common.annotations.VisibleForTesting;
public class RangeIntersectionIterator
{
protected enum Strategy
{
BOUNCE, LOOKUP, ADAPTIVE
}
public static <K extends Comparable<K>, D extends CombinedValue<K>> Builder<K, D> builder()
{
return builder(Strategy.ADAPTIVE);
}
@VisibleForTesting
protected static <K extends Comparable<K>, D extends CombinedValue<K>> Builder<K, D> builder(Strategy strategy)
{
return new Builder<>(strategy);
}
public static class Builder<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator.Builder<K, D>
{
private final Strategy strategy;
public Builder(Strategy strategy)
{
super(IteratorType.INTERSECTION);
this.strategy = strategy;
}
protected RangeIterator<K, D> buildIterator()
{
// if the range is disjoint we can simply return empty
// iterator of any type, because it's not going to produce any results.
if (statistics.isDisjoint())
return new BounceIntersectionIterator<>(statistics, new PriorityQueue<RangeIterator<K, D>>(1));
switch (strategy)
{
case LOOKUP:
return new LookupIntersectionIterator<>(statistics, ranges);
case BOUNCE:
return new BounceIntersectionIterator<>(statistics, ranges);
case ADAPTIVE:
return statistics.sizeRatio() <= 0.01d
? new LookupIntersectionIterator<>(statistics, ranges)
: new BounceIntersectionIterator<>(statistics, ranges);
default:
throw new IllegalStateException("Unknown strategy: " + strategy);
}
}
}
private static abstract class AbstractIntersectionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator<K, D>
{
protected final PriorityQueue<RangeIterator<K, D>> ranges;
private AbstractIntersectionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
{
super(statistics);
this.ranges = ranges;
}
public void close() throws IOException
{
for (RangeIterator<K, D> range : ranges)
FileUtils.closeQuietly(range);
}
}
/**
* Iterator which performs intersection of multiple ranges by using bouncing (merge-join) technique to identify
* common elements in the given ranges. Aforementioned "bounce" works as follows: range queue is poll'ed for the
* range with the smallest current token (main loop), that token is used to {@link RangeIterator#skipTo(Comparable)}
* other ranges, if token produced by {@link RangeIterator#skipTo(Comparable)} is equal to current "candidate" token,
* both get merged together and the same operation is repeated for next range from the queue, if returned token
* is not equal than candidate, candidate's range gets put back into the queue and the main loop gets repeated until
* next intersection token is found or at least one iterator runs out of tokens.
*
* This technique is every efficient to jump over gaps in the ranges.
*
* @param <K> The type used to sort ranges.
* @param <D> The container type which is going to be returned by {@link Iterator#next()}.
*/
@VisibleForTesting
protected static class BounceIntersectionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends AbstractIntersectionIterator<K, D>
{
private BounceIntersectionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
{
super(statistics, ranges);
}
protected D computeNext()
{
while (!ranges.isEmpty())
{
RangeIterator<K, D> head = ranges.poll();
// jump right to the beginning of the intersection or return next element
if (head.getCurrent().compareTo(getMinimum()) < 0)
head.skipTo(getMinimum());
D candidate = head.hasNext() ? head.next() : null;
if (candidate == null || candidate.get().compareTo(getMaximum()) > 0)
{
ranges.add(head);
return endOfData();
}
List<RangeIterator<K, D>> processed = new ArrayList<>();
boolean intersectsAll = true, exhausted = false;
while (!ranges.isEmpty())
{
RangeIterator<K, D> range = ranges.poll();
// found a range which doesn't overlap with one (or possibly more) other range(s)
if (!isOverlapping(head, range))
{
exhausted = true;
intersectsAll = false;
break;
}
D point = range.skipTo(candidate.get());
if (point == null) // other range is exhausted
{
exhausted = true;
intersectsAll = false;
break;
}
processed.add(range);
if (candidate.get().equals(point.get()))
{
candidate.merge(point);
// advance skipped range to the next element if any
Iterators.getNext(range, null);
}
else
{
intersectsAll = false;
break;
}
}
ranges.add(head);
for (RangeIterator<K, D> range : processed)
ranges.add(range);
if (exhausted)
return endOfData();
if (intersectsAll)
return candidate;
}
return endOfData();
}
protected void performSkipTo(K nextToken)
{
List<RangeIterator<K, D>> skipped = new ArrayList<>();
while (!ranges.isEmpty())
{
RangeIterator<K, D> range = ranges.poll();
range.skipTo(nextToken);
skipped.add(range);
}
for (RangeIterator<K, D> range : skipped)
ranges.add(range);
}
}
/**
* Iterator which performs a linear scan over a primary range (the smallest of the ranges)
* and O(log(n)) lookup into secondary ranges using values from the primary iterator.
* This technique is efficient when one of the intersection ranges is smaller than others
* e.g. ratio 0.01d (default), in such situation scan + lookup is more efficient comparing
* to "bounce" merge because "bounce" distance is never going to be big.
*
* @param <K> The type used to sort ranges.
* @param <D> The container type which is going to be returned by {@link Iterator#next()}.
*/
@VisibleForTesting
protected static class LookupIntersectionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends AbstractIntersectionIterator<K, D>
{
private final RangeIterator<K, D> smallestIterator;
private LookupIntersectionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
{
super(statistics, ranges);
smallestIterator = statistics.minRange;
if (smallestIterator.getCurrent().compareTo(getMinimum()) < 0)
smallestIterator.skipTo(getMinimum());
}
protected D computeNext()
{
while (smallestIterator.hasNext())
{
D candidate = smallestIterator.next();
K token = candidate.get();
boolean intersectsAll = true;
for (RangeIterator<K, D> range : ranges)
{
// avoid checking against self, much cheaper than changing queue comparator
// to compare based on the size and re-populating such queue.
if (range.equals(smallestIterator))
continue;
// found a range which doesn't overlap with one (or possibly more) other range(s)
if (!isOverlapping(smallestIterator, range))
return endOfData();
D point = range.skipTo(token);
if (point == null) // one of the iterators is exhausted
return endOfData();
if (!point.get().equals(token))
{
intersectsAll = false;
break;
}
candidate.merge(point);
}
if (intersectsAll)
return candidate;
}
return endOfData();
}
protected void performSkipTo(K nextToken)
{
smallestIterator.skipTo(nextToken);
}
}
}

View File

@ -0,0 +1,279 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.io.Closeable;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;
import com.google.common.annotations.VisibleForTesting;
public abstract class RangeIterator<K extends Comparable<K>, T extends CombinedValue<K>> extends AbstractIterator<T> implements Closeable
{
private final K min, max;
private final long count;
private K current;
protected RangeIterator(Builder.Statistics<K, T> statistics)
{
this(statistics.min, statistics.max, statistics.tokenCount);
}
public RangeIterator(RangeIterator<K, T> range)
{
this(range == null ? null : range.min, range == null ? null : range.max, range == null ? -1 : range.count);
}
public RangeIterator(K min, K max, long count)
{
this.min = min;
this.current = min;
this.max = max;
this.count = count;
}
public final K getMinimum()
{
return min;
}
public final K getCurrent()
{
return current;
}
public final K getMaximum()
{
return max;
}
public final long getCount()
{
return count;
}
/**
* When called, this iterators current position should
* be skipped forwards until finding either:
* 1) an element equal to or bigger than next
* 2) the end of the iterator
*
* @param nextToken value to skip the iterator forward until matching
*
* @return The next current token after the skip was performed
*/
public final T skipTo(K nextToken)
{
if (min == null || max == null)
return endOfData();
if (current.compareTo(nextToken) >= 0)
return next == null ? recomputeNext() : next;
if (max.compareTo(nextToken) < 0)
return endOfData();
performSkipTo(nextToken);
return recomputeNext();
}
protected abstract void performSkipTo(K nextToken);
protected T recomputeNext()
{
return tryToComputeNext() ? peek() : endOfData();
}
protected boolean tryToComputeNext()
{
boolean hasNext = super.tryToComputeNext();
current = hasNext ? next.get() : getMaximum();
return hasNext;
}
public static abstract class Builder<K extends Comparable<K>, D extends CombinedValue<K>>
{
public enum IteratorType
{
UNION, INTERSECTION
}
@VisibleForTesting
protected final Statistics<K, D> statistics;
@VisibleForTesting
protected final PriorityQueue<RangeIterator<K, D>> ranges;
public Builder(IteratorType type)
{
statistics = new Statistics<>(type);
ranges = new PriorityQueue<>(16, (Comparator<RangeIterator<K, D>>) (a, b) -> a.getCurrent().compareTo(b.getCurrent()));
}
public K getMinimum()
{
return statistics.min;
}
public K getMaximum()
{
return statistics.max;
}
public long getTokenCount()
{
return statistics.tokenCount;
}
public int rangeCount()
{
return ranges.size();
}
public Builder<K, D> add(RangeIterator<K, D> range)
{
if (range == null || range.getMinimum() == null || range.getMaximum() == null)
return this;
ranges.add(range);
statistics.update(range);
return this;
}
public Builder<K, D> add(List<RangeIterator<K, D>> ranges)
{
if (ranges == null || ranges.isEmpty())
return this;
ranges.forEach(this::add);
return this;
}
public final RangeIterator<K, D> build()
{
switch (rangeCount())
{
case 0:
return null;
case 1:
return ranges.poll();
default:
return buildIterator();
}
}
protected abstract RangeIterator<K, D> buildIterator();
public static class Statistics<K extends Comparable<K>, D extends CombinedValue<K>>
{
protected final IteratorType iteratorType;
protected K min, max;
protected long tokenCount;
// iterator with the least number of items
protected RangeIterator<K, D> minRange;
// iterator with the most number of items
protected RangeIterator<K, D> maxRange;
// tracks if all of the added ranges overlap, which is useful in case of intersection,
// as it gives direct answer as to such iterator is going to produce any results.
protected boolean isOverlapping = true;
public Statistics(IteratorType iteratorType)
{
this.iteratorType = iteratorType;
}
/**
* Update statistics information with the given range.
*
* Updates min/max of the combined range, token count and
* tracks range with the least/most number of tokens.
*
* @param range The range to update statistics with.
*/
public void update(RangeIterator<K, D> range)
{
switch (iteratorType)
{
case UNION:
min = min == null || min.compareTo(range.getMinimum()) > 0 ? range.getMinimum() : min;
max = max == null || max.compareTo(range.getMaximum()) < 0 ? range.getMaximum() : max;
break;
case INTERSECTION:
// minimum of the intersection is the biggest minimum of individual iterators
min = min == null || min.compareTo(range.getMinimum()) < 0 ? range.getMinimum() : min;
// maximum of the intersection is the smallest maximum of individual iterators
max = max == null || max.compareTo(range.getMaximum()) > 0 ? range.getMaximum() : max;
break;
default:
throw new IllegalStateException("Unknown iterator type: " + iteratorType);
}
// check if new range is disjoint with already added ranges, which means that this intersection
// is not going to produce any results, so we can cleanup range storage and never added anything to it.
isOverlapping &= isOverlapping(min, max, range);
minRange = minRange == null ? range : min(minRange, range);
maxRange = maxRange == null ? range : max(maxRange, range);
tokenCount += range.getCount();
}
private RangeIterator<K, D> min(RangeIterator<K, D> a, RangeIterator<K, D> b)
{
return a.getCount() > b.getCount() ? b : a;
}
private RangeIterator<K, D> max(RangeIterator<K, D> a, RangeIterator<K, D> b)
{
return a.getCount() > b.getCount() ? a : b;
}
public boolean isDisjoint()
{
return !isOverlapping;
}
public double sizeRatio()
{
return minRange.getCount() * 1d / maxRange.getCount();
}
}
}
@VisibleForTesting
protected static <K extends Comparable<K>, D extends CombinedValue<K>> boolean isOverlapping(RangeIterator<K, D> a, RangeIterator<K, D> b)
{
return isOverlapping(a.getCurrent(), a.getMaximum(), b);
}
@VisibleForTesting
protected static <K extends Comparable<K>, D extends CombinedValue<K>> boolean isOverlapping(K min, K max, RangeIterator<K, D> b)
{
return min.compareTo(b.getMaximum()) <= 0 && b.getCurrent().compareTo(max) <= 0;
}
}

View File

@ -0,0 +1,158 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.cassandra.io.util.FileUtils;
/**
* Range Union Iterator is used to return sorted stream of elements from multiple RangeIterator instances.
*
* PriorityQueue is used as a sorting mechanism for the ranges, where each computeNext() operation would poll
* from the queue (and push when done), which returns range that contains the smallest element, because
* sorting is done on the moving window of range iteration {@link RangeIterator#getCurrent()}. Once retrieved
* the smallest element (return candidate) is attempted to be merged with other ranges, because there could
* be equal elements in adjacent ranges, such ranges are poll'ed only if their {@link RangeIterator#getCurrent()}
* equals to the return candidate.
*
* @param <K> The type used to sort ranges.
* @param <D> The container type which is going to be returned by {@link Iterator#next()}.
*/
public class RangeUnionIterator<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator<K, D>
{
private final PriorityQueue<RangeIterator<K, D>> ranges;
private RangeUnionIterator(Builder.Statistics<K, D> statistics, PriorityQueue<RangeIterator<K, D>> ranges)
{
super(statistics);
this.ranges = ranges;
}
public D computeNext()
{
RangeIterator<K, D> head = null;
while (!ranges.isEmpty())
{
head = ranges.poll();
if (head.hasNext())
break;
FileUtils.closeQuietly(head);
}
if (head == null || !head.hasNext())
return endOfData();
D candidate = head.next();
List<RangeIterator<K, D>> processedRanges = new ArrayList<>();
if (head.hasNext())
processedRanges.add(head);
else
FileUtils.closeQuietly(head);
while (!ranges.isEmpty())
{
// peek here instead of poll is an optimization
// so we can re-insert less ranges back if candidate
// is less than head of the current range.
RangeIterator<K, D> range = ranges.peek();
int cmp = candidate.get().compareTo(range.getCurrent());
assert cmp <= 0;
if (cmp < 0)
{
break; // candidate is smaller than next token, return immediately
}
else if (cmp == 0)
{
candidate.merge(range.next()); // consume and merge
range = ranges.poll();
// re-prioritize changed range
if (range.hasNext())
processedRanges.add(range);
else
FileUtils.closeQuietly(range);
}
}
ranges.addAll(processedRanges);
return candidate;
}
protected void performSkipTo(K nextToken)
{
List<RangeIterator<K, D>> changedRanges = new ArrayList<>();
while (!ranges.isEmpty())
{
if (ranges.peek().getCurrent().compareTo(nextToken) >= 0)
break;
RangeIterator<K, D> head = ranges.poll();
if (head.getMaximum().compareTo(nextToken) >= 0)
{
head.skipTo(nextToken);
changedRanges.add(head);
continue;
}
FileUtils.closeQuietly(head);
}
ranges.addAll(changedRanges.stream().collect(Collectors.toList()));
}
public void close() throws IOException
{
ranges.forEach(FileUtils::closeQuietly);
}
public static <K extends Comparable<K>, D extends CombinedValue<K>> Builder<K, D> builder()
{
return new Builder<>();
}
public static <K extends Comparable<K>, D extends CombinedValue<K>> RangeIterator<K, D> build(List<RangeIterator<K, D>> tokens)
{
return new Builder<K, D>().add(tokens).build();
}
public static class Builder<K extends Comparable<K>, D extends CombinedValue<K>> extends RangeIterator.Builder<K, D>
{
public Builder()
{
super(IteratorType.UNION);
}
protected RangeIterator<K, D> buildIterator()
{
return new RangeUnionIterator<>(statistics, ranges);
}
}
}

View File

@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils;
import java.nio.ByteBuffer;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.serializers.MarshalException;
public class TypeUtil
{
public static boolean isValid(ByteBuffer term, AbstractType<?> validator)
{
try
{
validator.validate(term);
return true;
}
catch (MarshalException e)
{
return false;
}
}
public static ByteBuffer tryUpcast(ByteBuffer term, AbstractType<?> validator)
{
if (term.remaining() == 0)
return null;
try
{
if (validator instanceof Int32Type && term.remaining() == 2)
{
return Int32Type.instance.decompose((int) term.getShort(term.position()));
}
else if (validator instanceof LongType)
{
long upcastToken;
switch (term.remaining())
{
case 2:
upcastToken = (long) term.getShort(term.position());
break;
case 4:
upcastToken = (long) Int32Type.instance.compose(term);
break;
default:
upcastToken = Long.valueOf(UTF8Type.instance.getString(term));
}
return LongType.instance.decompose(upcastToken);
}
else if (validator instanceof DoubleType && term.remaining() == 4)
{
return DoubleType.instance.decompose((double) FloatType.instance.compose(term));
}
// maybe it was a string after all
return validator.fromString(UTF8Type.instance.getString(term));
}
catch (Exception e)
{
return null;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,230 @@
/*
* Copyright 2005-2010 Roger Kapsi, Sam Berlin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils.trie;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.Map;
/**
* This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
* to correspond to Cassandra code style, as the only Patricia Trie implementation,
* which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
* on rkapsi/patricia-trie project) only supports String keys)
* but unfortunately is not deployed to the maven central as a downloadable artifact.
*/
/**
* This class provides some basic {@link Trie} functionality and
* utility methods for actual {@link Trie} implementations.
*/
abstract class AbstractTrie<K, V> extends AbstractMap<K, V> implements Serializable, Trie<K, V>
{
private static final long serialVersionUID = -6358111100045408883L;
/**
* The {@link KeyAnalyzer} that's being used to build the
* PATRICIA {@link Trie}
*/
protected final KeyAnalyzer<? super K> keyAnalyzer;
/**
* Constructs a new {@link Trie} using the given {@link KeyAnalyzer}
*/
public AbstractTrie(KeyAnalyzer<? super K> keyAnalyzer)
{
this.keyAnalyzer = Tries.notNull(keyAnalyzer, "keyAnalyzer");
}
@Override
public K selectKey(K key)
{
Map.Entry<K, V> entry = select(key);
return entry != null ? entry.getKey() : null;
}
@Override
public V selectValue(K key)
{
Map.Entry<K, V> entry = select(key);
return entry != null ? entry.getValue() : null;
}
@Override
public String toString()
{
StringBuilder buffer = new StringBuilder();
buffer.append("Trie[").append(size()).append("]={\n");
for (Map.Entry<K, V> entry : entrySet()) {
buffer.append(" ").append(entry).append("\n");
}
buffer.append("}\n");
return buffer.toString();
}
/**
* Returns the length of the given key in bits
*
* @see KeyAnalyzer#lengthInBits(Object)
*/
final int lengthInBits(K key)
{
return key == null ? 0 : keyAnalyzer.lengthInBits(key);
}
/**
* Returns whether or not the given bit on the
* key is set or false if the key is null.
*
* @see KeyAnalyzer#isBitSet(Object, int)
*/
final boolean isBitSet(K key, int bitIndex)
{
return key != null && keyAnalyzer.isBitSet(key, bitIndex);
}
/**
* Utility method for calling {@link KeyAnalyzer#bitIndex(Object, Object)}
*/
final int bitIndex(K key, K otherKey)
{
if (key != null && otherKey != null)
{
return keyAnalyzer.bitIndex(key, otherKey);
}
else if (key != null)
{
return bitIndex(key);
}
else if (otherKey != null)
{
return bitIndex(otherKey);
}
return KeyAnalyzer.NULL_BIT_KEY;
}
private int bitIndex(K key)
{
int lengthInBits = lengthInBits(key);
for (int i = 0; i < lengthInBits; i++)
{
if (isBitSet(key, i))
return i;
}
return KeyAnalyzer.NULL_BIT_KEY;
}
/**
* An utility method for calling {@link KeyAnalyzer#compare(Object, Object)}
*/
final boolean compareKeys(K key, K other)
{
if (key == null)
{
return (other == null);
}
else if (other == null)
{
return false;
}
return keyAnalyzer.compare(key, other) == 0;
}
/**
* A basic implementation of {@link Entry}
*/
abstract static class BasicEntry<K, V> implements Map.Entry<K, V>, Serializable
{
private static final long serialVersionUID = -944364551314110330L;
protected K key;
protected V value;
private transient int hashCode = 0;
public BasicEntry(K key, V value)
{
this.key = key;
this.value = value;
}
/**
* Replaces the current key and value with the provided
* key &amp; value
*/
public V setKeyValue(K key, V value)
{
this.key = key;
this.hashCode = 0;
return setValue(value);
}
@Override
public K getKey()
{
return key;
}
@Override
public V getValue()
{
return value;
}
@Override
public V setValue(V value)
{
V previous = this.value;
this.value = value;
return previous;
}
@Override
public int hashCode()
{
if (hashCode == 0)
hashCode = (key != null ? key.hashCode() : 0);
return hashCode;
}
@Override
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
else if (!(o instanceof Map.Entry<?, ?>))
{
return false;
}
Map.Entry<?, ?> other = (Map.Entry<?, ?>)o;
return Tries.areEqual(key, other.getKey()) && Tries.areEqual(value, other.getValue());
}
@Override
public String toString()
{
return key + "=" + value;
}
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2005-2010 Roger Kapsi, Sam Berlin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils.trie;
import java.util.Map;
import java.util.Map.Entry;
/**
* This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
* to correspond to Cassandra code style, as the only Patricia Trie implementation,
* which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
* on rkapsi/patricia-trie project) only supports String keys)
* but unfortunately is not deployed to the maven central as a downloadable artifact.
*/
/**
* A {@link Cursor} can be used to traverse a {@link Trie}, visit each node
* step by step and make {@link Decision}s on each step how to continue with
* traversing the {@link Trie}.
*/
public interface Cursor<K, V>
{
/**
* The {@link Decision} tells the {@link Cursor} what to do on each step
* while traversing the {@link Trie}.
*
* NOTE: Not all operations that work with a {@link Cursor} support all
* {@link Decision} types
*/
enum Decision
{
/**
* Exit the traverse operation
*/
EXIT,
/**
* Continue with the traverse operation
*/
CONTINUE,
/**
* Remove the previously returned element
* from the {@link Trie} and continue
*/
REMOVE,
/**
* Remove the previously returned element
* from the {@link Trie} and exit from the
* traverse operation
*/
REMOVE_AND_EXIT
}
/**
* Called for each {@link Entry} in the {@link Trie}. Return
* {@link Decision#EXIT} to finish the {@link Trie} operation,
* {@link Decision#CONTINUE} to go to the next {@link Entry},
* {@link Decision#REMOVE} to remove the {@link Entry} and
* continue iterating or {@link Decision#REMOVE_AND_EXIT} to
* remove the {@link Entry} and stop iterating.
*
* Note: Not all operations support {@link Decision#REMOVE}.
*/
Decision select(Map.Entry<? extends K, ? extends V> entry);
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2010 Roger Kapsi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils.trie;
import java.util.Comparator;
/**
* This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
* to correspond to Cassandra code style, as the only Patricia Trie implementation,
* which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
* on rkapsi/patricia-trie project) only supports String keys)
* but unfortunately is not deployed to the maven central as a downloadable artifact.
*/
/**
* The {@link KeyAnalyzer} provides bit-level access to keys
* for the {@link PatriciaTrie}.
*/
public interface KeyAnalyzer<K> extends Comparator<K>
{
/**
* Returned by {@link #bitIndex(Object, Object)} if a key's
* bits were all zero (0).
*/
int NULL_BIT_KEY = -1;
/**
* Returned by {@link #bitIndex(Object, Object)} if a the
* bits of two keys were all equal.
*/
int EQUAL_BIT_KEY = -2;
/**
* Returned by {@link #bitIndex(Object, Object)} if a keys
* indices are out of bounds.
*/
int OUT_OF_BOUNDS_BIT_KEY = -3;
/**
* Returns the key's length in bits.
*/
int lengthInBits(K key);
/**
* Returns {@code true} if a key's bit it set at the given index.
*/
boolean isBitSet(K key, int bitIndex);
/**
* Returns the index of the first bit that is different in the two keys.
*/
int bitIndex(K key, K otherKey);
/**
* Returns {@code true} if the second argument is a
* prefix of the first argument.
*/
boolean isPrefix(K key, K prefix);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,152 @@
/*
* Copyright 2005-2010 Roger Kapsi, Sam Berlin
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.index.sasi.utils.trie;
import java.util.Map;
import java.util.SortedMap;
import org.apache.cassandra.index.sasi.utils.trie.Cursor.Decision;
/**
* This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
* to correspond to Cassandra code style, as the only Patricia Trie implementation,
* which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
* on rkapsi/patricia-trie project) only supports String keys)
* but unfortunately is not deployed to the maven central as a downloadable artifact.
*/
/**
* Defines the interface for a prefix tree, an ordered tree data structure. For
* more information, see <a href="http://en.wikipedia.org/wiki/Trie">Tries</a>.
*
* @author Roger Kapsi
* @author Sam Berlin
*/
public interface Trie<K, V> extends SortedMap<K, V>
{
/**
* Returns the {@link Map.Entry} whose key is closest in a bitwise XOR
* metric to the given key. This is NOT lexicographic closeness.
* For example, given the keys:
*
* <ol>
* <li>D = 1000100
* <li>H = 1001000
* <li>L = 1001100
* </ol>
*
* If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would
* return 'L', because the XOR distance between D &amp; L is smaller
* than the XOR distance between D &amp; H.
*
* @return The {@link Map.Entry} whose key is closest in a bitwise XOR metric
* to the provided key.
*/
Map.Entry<K, V> select(K key);
/**
* Returns the key that is closest in a bitwise XOR metric to the
* provided key. This is NOT lexicographic closeness!
*
* For example, given the keys:
*
* <ol>
* <li>D = 1000100
* <li>H = 1001000
* <li>L = 1001100
* </ol>
*
* If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would
* return 'L', because the XOR distance between D &amp; L is smaller
* than the XOR distance between D &amp; H.
*
* @return The key that is closest in a bitwise XOR metric to the provided key.
*/
@SuppressWarnings("unused")
K selectKey(K key);
/**
* Returns the value whose key is closest in a bitwise XOR metric to
* the provided key. This is NOT lexicographic closeness!
*
* For example, given the keys:
*
* <ol>
* <li>D = 1000100
* <li>H = 1001000
* <li>L = 1001100
* </ol>
*
* If the {@link Trie} contained 'H' and 'L', a lookup of 'D' would
* return 'L', because the XOR distance between D &amp; L is smaller
* than the XOR distance between D &amp; H.
*
* @return The value whose key is closest in a bitwise XOR metric
* to the provided key.
*/
@SuppressWarnings("unused")
V selectValue(K key);
/**
* Iterates through the {@link Trie}, starting with the entry whose bitwise
* value is closest in an XOR metric to the given key. After the closest
* entry is found, the {@link Trie} will call select on that entry and continue
* calling select for each entry (traversing in order of XOR closeness,
* NOT lexicographically) until the cursor returns {@link Decision#EXIT}.
*
* <p>The cursor can return {@link Decision#CONTINUE} to continue traversing.
*
* <p>{@link Decision#REMOVE_AND_EXIT} is used to remove the current element
* and stop traversing.
*
* <p>Note: The {@link Decision#REMOVE} operation is not supported.
*
* @return The entry the cursor returned {@link Decision#EXIT} on, or null
* if it continued till the end.
*/
Map.Entry<K,V> select(K key, Cursor<? super K, ? super V> cursor);
/**
* Traverses the {@link Trie} in lexicographical order.
* {@link Cursor#select(java.util.Map.Entry)} will be called on each entry.
*
* <p>The traversal will stop when the cursor returns {@link Decision#EXIT},
* {@link Decision#CONTINUE} is used to continue traversing and
* {@link Decision#REMOVE} is used to remove the element that was selected
* and continue traversing.
*
* <p>{@link Decision#REMOVE_AND_EXIT} is used to remove the current element
* and stop traversing.
*
* @return The entry the cursor returned {@link Decision#EXIT} on, or null
* if it continued till the end.
*/
Map.Entry<K,V> traverse(Cursor<? super K, ? super V> cursor);
/**
* Returns a view of this {@link Trie} of all elements that are prefixed
* by the given key.
*
* <p>In a {@link Trie} with fixed size keys, this is essentially a
* {@link #get(Object)} operation.
*
* <p>For example, if the {@link Trie} contains 'Anna', 'Anael',
* 'Analu', 'Andreas', 'Andrea', 'Andres', and 'Anatole', then
* a lookup of 'And' would return 'Andreas', 'Andrea', and 'Andres'.
*/
SortedMap<K, V> prefixMap(K prefix);
}

View File

@ -0,0 +1,95 @@
/*
* Copyright 2005-2010 Roger Kapsi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This class is taken from https://github.com/rkapsi/patricia-trie (v0.6), and slightly modified
* to correspond to Cassandra code style, as the only Patricia Trie implementation,
* which supports pluggable key comparators (e.g. commons-collections PatriciaTrie (which is based
* on rkapsi/patricia-trie project) only supports String keys)
* but unfortunately is not deployed to the maven central as a downloadable artifact.
*/
package org.apache.cassandra.index.sasi.utils.trie;
/**
* A collection of {@link Trie} utilities
*/
public class Tries
{
/**
* Returns true if bitIndex is a {@link KeyAnalyzer#OUT_OF_BOUNDS_BIT_KEY}
*/
static boolean isOutOfBoundsIndex(int bitIndex)
{
return bitIndex == KeyAnalyzer.OUT_OF_BOUNDS_BIT_KEY;
}
/**
* Returns true if bitIndex is a {@link KeyAnalyzer#EQUAL_BIT_KEY}
*/
static boolean isEqualBitKey(int bitIndex)
{
return bitIndex == KeyAnalyzer.EQUAL_BIT_KEY;
}
/**
* Returns true if bitIndex is a {@link KeyAnalyzer#NULL_BIT_KEY}
*/
static boolean isNullBitKey(int bitIndex)
{
return bitIndex == KeyAnalyzer.NULL_BIT_KEY;
}
/**
* Returns true if the given bitIndex is valid. Indices
* are considered valid if they're between 0 and
* {@link Integer#MAX_VALUE}
*/
static boolean isValidBitIndex(int bitIndex)
{
return 0 <= bitIndex;
}
/**
* Returns true if both values are either null or equal
*/
static boolean areEqual(Object a, Object b)
{
return (a == null ? b == null : a.equals(b));
}
/**
* Throws a {@link NullPointerException} with the given message if
* the argument is null.
*/
static <T> T notNull(T o, String message)
{
if (o == null)
throw new NullPointerException(message);
return o;
}
/**
* A utility method to cast keys. It actually doesn't
* cast anything. It's just fooling the compiler!
*/
@SuppressWarnings("unchecked")
static <K> K cast(Object key)
{
return (K)key;
}
}

View File

@ -19,6 +19,7 @@ package org.apache.cassandra.io.sstable;
import java.io.File;
import java.util.EnumSet;
import java.util.regex.Pattern;
import com.google.common.base.Objects;
@ -57,6 +58,8 @@ public class Component
SUMMARY("Summary.db"),
// table of contents, stores the list of all components for the sstable
TOC("TOC.txt"),
// built-in secondary index (may be multiple per sstable)
SECONDARY_INDEX("SI_.*.db"),
// custom component, used by e.g. custom compaction strategy
CUSTOM(new String[] { null });
@ -74,9 +77,12 @@ public class Component
static Type fromRepresentation(String repr)
{
for (Type type : TYPES)
for (String representation : type.repr)
if (repr.equals(representation))
return type;
{
if (type.repr == null || type.repr.length == 0 || type.repr[0] == null)
continue;
if (Pattern.matches(type.repr[0], repr))
return type;
}
return CUSTOM;
}
}
@ -169,6 +175,7 @@ public class Component
case CRC: component = Component.CRC; break;
case SUMMARY: component = Component.SUMMARY; break;
case TOC: component = Component.TOC; break;
case SECONDARY_INDEX: component = new Component(Type.SECONDARY_INDEX, path.right); break;
case CUSTOM: component = new Component(Type.CUSTOM, path.right); break;
default:
throw new IllegalStateException();

View File

@ -84,6 +84,7 @@ public class KeyIterator extends AbstractIterator<DecoratedKey> implements Close
private final In in;
private final IPartitioner partitioner;
private long keyPosition;
public KeyIterator(Descriptor desc, CFMetaData metadata)
{
@ -99,6 +100,7 @@ public class KeyIterator extends AbstractIterator<DecoratedKey> implements Close
if (in.isEOF())
return endOfData();
keyPosition = in.getFilePointer();
DecoratedKey key = partitioner.decorateKey(ByteBufferUtil.readWithShortLength(in.get()));
RowIndexEntry.Serializer.skip(in.get(), desc.version); // skip remainder of the entry
return key;
@ -123,4 +125,9 @@ public class KeyIterator extends AbstractIterator<DecoratedKey> implements Close
{
return in.length();
}
public long getKeyPosition()
{
return keyPosition;
}
}

View File

@ -18,7 +18,7 @@
package org.apache.cassandra.io.sstable.format;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.rows.ColumnData;
import org.apache.cassandra.db.rows.Unfiltered;
/**
* Observer for events in the lifecycle of writing out an sstable.
@ -32,7 +32,7 @@ public interface SSTableFlushObserver
/**
* Called when a new partition in being written to the sstable,
* but before any cells are processed (see {@link #nextCell(ColumnData)}).
* but before any cells are processed (see {@link #nextUnfilteredCluster(Unfiltered)}).
*
* @param key The key being appended to SSTable.
* @param indexPosition The position of the key in the SSTable PRIMARY_INDEX file.
@ -40,13 +40,13 @@ public interface SSTableFlushObserver
void startPartition(DecoratedKey key, long indexPosition);
/**
* Called after the cell is written to the sstable.
* Called after the unfiltered cluster is written to the sstable.
* Will be preceded by a call to {@code startPartition(DecoratedKey, long)},
* and the cell should be assumed to belong to that row.
* and the cluster should be assumed to belong to that partition.
*
* @param cell The cell being added to the row.
* @param unfilteredCluster The unfiltered cluster being added to SSTable.
*/
void nextCell(ColumnData cell);
void nextUnfilteredCluster(Unfiltered unfilteredCluster);
/**
* Called when all data is written to the file and it's ready to be finished up.

View File

@ -1795,6 +1795,26 @@ public abstract class SSTableReader extends SSTable implements SelfRefCounted<SS
return sstableMetadata.repairedAt != ActiveRepairService.UNREPAIRED_SSTABLE;
}
public DecoratedKey keyAt(long indexPosition) throws IOException
{
DecoratedKey key;
try (FileDataInput in = ifile.createReader(indexPosition))
{
if (in.isEOF())
return null;
key = decorateKey(ByteBufferUtil.readWithShortLength(in));
// hint read path about key location if caching is enabled
// this saves index summary lookup and index file iteration which whould be pretty costly
// especially in presence of promoted column indexes
if (isKeyCacheSetup())
cacheKey(key, rowIndexEntrySerializer.deserialize(in));
}
return key;
}
/**
* TODO: Move someplace reusable
*/

View File

@ -277,7 +277,14 @@ public abstract class SSTableWriter extends SSTable implements Transactional
public final Throwable commit(Throwable accumulate)
{
return txnProxy.commit(accumulate);
try
{
return txnProxy.commit(accumulate);
}
finally
{
observers.forEach(SSTableFlushObserver::complete);
}
}
public final Throwable abort(Throwable accumulate)

View File

@ -62,4 +62,9 @@ public class DataOutputBufferFixed extends DataOutputBuffer
{
throw new BufferOverflowException();
}
public void clear()
{
buffer.clear();
}
}

View File

@ -169,6 +169,13 @@ public class SequentialWriter extends BufferedDataOutputStreamPlus implements Tr
return this;
}
public void skipBytes(int numBytes) throws IOException
{
flush();
fchannel.position(fchannel.position() + numBytes);
bufferOffset = fchannel.position();
}
/**
* Synchronize file contents with disk.
*/

View File

@ -669,4 +669,45 @@ public class ByteBufferUtil
return buf;
}
/**
* Check is the given buffer contains a given sub-buffer.
*
* @param buffer The buffer to search for sequence of bytes in.
* @param subBuffer The buffer to match.
*
* @return true if buffer contains sub-buffer, false otherwise.
*/
public static boolean contains(ByteBuffer buffer, ByteBuffer subBuffer)
{
int len = subBuffer.remaining();
if (buffer.remaining() - len < 0)
return false;
// adapted form the JDK's String.indexOf()
byte first = subBuffer.get(subBuffer.position());
int max = buffer.position() + (buffer.remaining() - len);
for (int i = buffer.position(); i <= max; i++)
{
/* Look for first character. */
if (buffer.get(i) != first)
{
while (++i <= max && buffer.get(i) != first)
{}
}
/* (maybe) Found first character, now look at the rest of v2 */
if (i <= max)
{
int j = i + 1;
int end = j + len - 1;
for (int k = 1 + subBuffer.position(); j < end && buffer.get(j) == subBuffer.get(k); j++, k++)
{}
if (j == end)
return true;
}
}
return false;
}
}

View File

@ -844,4 +844,9 @@ public class FBUtilities
throw new RuntimeException(e);
}
}
public static long align(long val, int boundary)
{
return (val + boundary) & ~(boundary - 1);
}
}

View File

@ -31,7 +31,7 @@ public abstract class MemoryUtil
private static final long UNSAFE_COPY_THRESHOLD = 1024 * 1024L; // copied from java.nio.Bits
private static final Unsafe unsafe;
private static final Class<?> DIRECT_BYTE_BUFFER_CLASS;
private static final Class<?> DIRECT_BYTE_BUFFER_CLASS, RO_DIRECT_BYTE_BUFFER_CLASS;
private static final long DIRECT_BYTE_BUFFER_ADDRESS_OFFSET;
private static final long DIRECT_BYTE_BUFFER_CAPACITY_OFFSET;
private static final long DIRECT_BYTE_BUFFER_LIMIT_OFFSET;
@ -65,6 +65,7 @@ public abstract class MemoryUtil
DIRECT_BYTE_BUFFER_POSITION_OFFSET = unsafe.objectFieldOffset(Buffer.class.getDeclaredField("position"));
DIRECT_BYTE_BUFFER_ATTACHMENT_OFFSET = unsafe.objectFieldOffset(clazz.getDeclaredField("att"));
DIRECT_BYTE_BUFFER_CLASS = clazz;
RO_DIRECT_BYTE_BUFFER_CLASS = ByteBuffer.allocateDirect(0).asReadOnlyBuffer().getClass();
clazz = ByteBuffer.allocate(0).getClass();
BYTE_BUFFER_OFFSET_OFFSET = unsafe.objectFieldOffset(ByteBuffer.class.getDeclaredField("offset"));
@ -204,7 +205,7 @@ public abstract class MemoryUtil
public static ByteBuffer duplicateDirectByteBuffer(ByteBuffer source, ByteBuffer hollowBuffer)
{
assert source.getClass() == DIRECT_BYTE_BUFFER_CLASS;
assert source.getClass() == DIRECT_BYTE_BUFFER_CLASS || source.getClass() == RO_DIRECT_BYTE_BUFFER_CLASS;
unsafe.putLong(hollowBuffer, DIRECT_BYTE_BUFFER_ADDRESS_OFFSET, unsafe.getLong(source, DIRECT_BYTE_BUFFER_ADDRESS_OFFSET));
unsafe.putInt(hollowBuffer, DIRECT_BYTE_BUFFER_POSITION_OFFSET, unsafe.getInt(source, DIRECT_BYTE_BUFFER_POSITION_OFFSET));
unsafe.putInt(hollowBuffer, DIRECT_BYTE_BUFFER_LIMIT_OFFSET, unsafe.getInt(source, DIRECT_BYTE_BUFFER_LIMIT_OFFSET));

View File

@ -0,0 +1,163 @@
# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html
ب
ا
أ
،
عشر
عدد
عدة
عشرة
عدم
عام
عاما
عن
عند
عندما
على
عليه
عليها
زيارة
سنة
سنوات
تم
ضد
بعد
بعض
اعادة
اعلنت
بسبب
حتى
اذا
احد
اثر
برس
باسم
غدا
شخصا
صباح
اطار
اربعة
اخرى
بان
اجل
غير
بشكل
حاليا
بن
به
ثم
اف
ان
او
اي
بها
صفر
حيث
اكد
الا
اما
امس
السابق
التى
التي
اكثر
ايار
ايضا
ثلاثة
الذاتي
الاخيرة
الثاني
الثانية
الذى
الذي
الان
امام
ايام
خلال
حوالى
الذين
الاول
الاولى
بين
ذلك
دون
حول
حين
الف
الى
انه
اول
ضمن
انها
جميع
الماضي
الوقت
المقبل
اليوم
ـ
ف
و
و6
قد
لا
ما
مع
مساء
هذا
واحد
واضاف
واضافت
فان
قبل
قال
كان
لدى
نحو
هذه
وان
واكد
كانت
واوضح
مايو
فى
في
كل
لم
لن
له
من
هو
هي
قوة
كما
لها
منذ
وقد
ولا
نفسه
لقاء
مقابل
هناك
وقال
وكان
نهاية
وقالت
وكانت
للامم
فيه
كلم
لكن
وفي
وقف
ولم
ومن
وهو
وهي
يوم
فيها
منها
مليار
لوكالة
يكون
يمكن
مليون

View File

@ -0,0 +1,260 @@
# Stop Words List from http://members.unine.ch/jacques.savoy/clef/index.html
а
автентичен
аз
ако
ала
бе
без
беше
би
бивш
бивша
бившо
бил
била
били
било
благодаря
близо
бъдат
бъде
бяха
в
вас
ваш
ваша
вероятно
вече
взема
ви
вие
винаги
внимава
време
все
всеки
всички
всичко
всяка
във
въпреки
върху
г
ги
главен
главна
главно
глас
го
година
години
годишен
д
да
дали
два
двама
двамата
две
двете
ден
днес
дни
до
добра
добре
добро
добър
докато
докога
дори
досега
доста
друг
друга
други
е
евтин
едва
един
една
еднаква
еднакви
еднакъв
едно
екип
ето
живот
за
забавям
зад
заедно
заради
засега
заспал
затова
защо
защото
и
из
или
им
има
имат
иска
й
каза
как
каква
какво
както
какъв
като
кога
когато
което
които
кой
който
колко
която
къде
където
към
лесен
лесно
ли
лош
м
май
малко
ме
между
мек
мен
месец
ми
много
мнозина
мога
могат
може
мокър
моля
момента
му
н
на
над
назад
най
направи
напред
например
нас
не
него
нещо
нея
ни
ние
никой
нито
нищо
но
нов
нова
нови
новина
някои
някой
няколко
няма
обаче
около
освен
особено
от
отгоре
отново
още
пак
по
повече
повечето
под
поне
поради
после
почти
прави
пред
преди
през
при
пък
първата
първи
първо
пъти
равен
равна
с
са
сам
само
се
сега
си
син
скоро
след
следващ
сме
смях
според
сред
срещу
сте
съм
със
също
т
тази
така
такива
такъв
там
твой
те
тези
ти
т.н.
то
това
тогава
този
той
толкова
точно
три
трябва
тук
тъй
тя
тях
у
утре
харесва
хиляди
ч
часа
че
често
чрез
ще
щом
юмрук
я
як

Some files were not shown because too many files have changed in this diff Show More