You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by xe...@apache.org on 2016/02/11 20:24:57 UTC

cassandra git commit: fix EQ semantics of analyzed SASI indexes

Repository: cassandra
Updated Branches:
  refs/heads/trunk 6dc1004ce -> 479e8aff1


fix EQ semantics of analyzed SASI indexes

patch by xedin; reviewed by beobal for CASSANDRA-11130


Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo
Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/479e8aff
Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/479e8aff
Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/479e8aff

Branch: refs/heads/trunk
Commit: 479e8aff1346d41ebc05e1a113996a803228284d
Parents: 6dc1004
Author: Pavel Yaskevich <xe...@apache.org>
Authored: Sun Feb 7 17:15:21 2016 -0800
Committer: Pavel Yaskevich <xe...@apache.org>
Committed: Thu Feb 11 11:24:40 2016 -0800

----------------------------------------------------------------------
 CHANGES.txt                                     |   1 +
 src/java/org/apache/cassandra/cql3/Cql.g        |   2 +-
 .../org/apache/cassandra/cql3/Operator.java     |   9 +
 .../org/apache/cassandra/cql3/Relation.java     |   4 +-
 .../apache/cassandra/db/filter/RowFilter.java   |   1 +
 .../index/sasi/analyzer/AbstractAnalyzer.java   |   8 +
 .../index/sasi/analyzer/StandardAnalyzer.java   |   5 +
 .../cassandra/index/sasi/conf/ColumnIndex.java  |  12 +-
 .../cassandra/index/sasi/conf/IndexMode.java    |   2 +-
 .../index/sasi/disk/OnDiskIndexBuilder.java     |   4 +-
 .../index/sasi/memory/TrieMemIndex.java         |   2 +
 .../cassandra/index/sasi/plan/Expression.java   |   7 +-
 .../cassandra/index/sasi/plan/Operation.java    |   5 +
 .../cassandra/index/sasi/SASIIndexTest.java     | 219 +++++++++++++++++--
 .../index/sasi/plan/OperationTest.java          |   6 +-
 15 files changed, 263 insertions(+), 24 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/CHANGES.txt
----------------------------------------------------------------------
diff --git a/CHANGES.txt b/CHANGES.txt
index c09a453..28651e2 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 3.4
+ * fix EQ semantics of analyzed SASI indexes (CASSANDRA-11130)
  * Support long name output for nodetool commands (CASSANDRA-7950)
  * Encrypted hints (CASSANDRA-11040)
  * SASI index options validation (CASSANDRA-11136)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/cql3/Cql.g
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/Cql.g b/src/java/org/apache/cassandra/cql3/Cql.g
index d560119..5cb479c 100644
--- a/src/java/org/apache/cassandra/cql3/Cql.g
+++ b/src/java/org/apache/cassandra/cql3/Cql.g
@@ -209,7 +209,7 @@ options {
         }
         else
         {
-            operator = Operator.EQ;
+            operator = Operator.LIKE_MATCHES;
             endIndex += 1;
         }
 

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/cql3/Operator.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/Operator.java b/src/java/org/apache/cassandra/cql3/Operator.java
index d518961..accb786 100644
--- a/src/java/org/apache/cassandra/cql3/Operator.java
+++ b/src/java/org/apache/cassandra/cql3/Operator.java
@@ -126,6 +126,14 @@ public enum Operator
         {
             return "LIKE '%<term>%'";
         }
+    },
+    LIKE_MATCHES(13)
+    {
+        @Override
+        public String toString()
+        {
+            return "LIKE '<term>'";
+        }
     };
 
     /**
@@ -222,6 +230,7 @@ public enum Operator
                 return ByteBufferUtil.startsWith(leftOperand, rightOperand);
             case LIKE_SUFFIX:
                 return ByteBufferUtil.endsWith(leftOperand, rightOperand);
+            case LIKE_MATCHES:
             case LIKE_CONTAINS:
                 return ByteBufferUtil.contains(leftOperand, rightOperand);
             default:

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/cql3/Relation.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/cql3/Relation.java b/src/java/org/apache/cassandra/cql3/Relation.java
index 81f46a6..616fd30 100644
--- a/src/java/org/apache/cassandra/cql3/Relation.java
+++ b/src/java/org/apache/cassandra/cql3/Relation.java
@@ -112,7 +112,8 @@ public abstract class Relation {
     {
         return relationType == Operator.LIKE_PREFIX
                 || relationType == Operator.LIKE_SUFFIX
-                || relationType == Operator.LIKE_CONTAINS;
+                || relationType == Operator.LIKE_CONTAINS
+                || relationType == Operator.LIKE_MATCHES;
     }
 
     /**
@@ -153,6 +154,7 @@ public abstract class Relation {
             case LIKE_PREFIX:
             case LIKE_SUFFIX:
             case LIKE_CONTAINS:
+            case LIKE_MATCHES:
                 return newLikeRestriction(cfm, boundNames, relationType);
             default: throw invalidRequest("Unsupported \"!=\" relation: %s", this);
         }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/db/filter/RowFilter.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/db/filter/RowFilter.java b/src/java/org/apache/cassandra/db/filter/RowFilter.java
index 1141fd9..fcc3cd5 100644
--- a/src/java/org/apache/cassandra/db/filter/RowFilter.java
+++ b/src/java/org/apache/cassandra/db/filter/RowFilter.java
@@ -604,6 +604,7 @@ public abstract class RowFilter implements Iterable<RowFilter.Expression>
                 case LIKE_PREFIX:
                 case LIKE_SUFFIX:
                 case LIKE_CONTAINS:
+                case LIKE_MATCHES:
                     {
                         assert !column.isComplex() : "Only CONTAINS and CONTAINS_KEY are supported for 'complex' types";
                         ByteBuffer foundValue = getValue(metadata, partitionKey, row);

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java
index b3fdd8c..31c66cc 100644
--- a/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/AbstractAnalyzer.java
@@ -42,6 +42,14 @@ public abstract class AbstractAnalyzer implements Iterator<ByteBuffer>
 
     public abstract void reset(ByteBuffer input);
 
+    /**
+     * @return true if current analyzer provides text tokenization, false otherwise.
+     */
+    public boolean isTokenizing()
+    {
+        return false;
+    }
+
     public static String normalize(String original)
     {
         return Normalizer.isNormalized(original, Normalizer.Form.NFC)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java
index bcc63df..5e09b9f 100644
--- a/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java
+++ b/src/java/org/apache/cassandra/index/sasi/analyzer/StandardAnalyzer.java
@@ -191,4 +191,9 @@ public class StandardAnalyzer extends AbstractAnalyzer
         scanner.yyreset(reader);
         this.inputReader = reader;
     }
+
+    public boolean isTokenizing()
+    {
+        return true;
+    }
 }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java b/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java
index 29e7c28..1703bd4 100644
--- a/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java
+++ b/src/java/org/apache/cassandra/index/sasi/conf/ColumnIndex.java
@@ -37,6 +37,7 @@ 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.plan.Expression.Op;
 import org.apache.cassandra.index.sasi.utils.RangeIterator;
 import org.apache.cassandra.io.sstable.Component;
 import org.apache.cassandra.io.sstable.format.SSTableReader;
@@ -58,6 +59,8 @@ public class ColumnIndex
     private final Component component;
     private final DataTracker tracker;
 
+    private final boolean isTokenized;
+
     public ColumnIndex(AbstractType<?> keyValidator, ColumnDefinition column, IndexMetadata metadata)
     {
         this.keyValidator = keyValidator;
@@ -67,6 +70,7 @@ public class ColumnIndex
         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()));
+        this.isTokenized = getAnalyzer().isTokenizing();
     }
 
     /**
@@ -170,9 +174,13 @@ public class ColumnIndex
         return isIndexed() ? mode.isLiteral : (validator instanceof UTF8Type || validator instanceof AsciiType);
     }
 
-    public boolean supports(Operator operator)
+    public boolean supports(Operator op)
     {
-        return mode.supports(Expression.Op.valueOf(operator));
+        Op operator = Op.valueOf(op);
+        return !(isTokenized && operator == Op.EQ) // EQ is only applicable to non-tokenized indexes
+            && !(isLiteral() && operator == Op.RANGE) // RANGE only applicable to indexes non-literal indexes
+            && mode.supports(operator); // for all other cases let's refer to index itself
+
     }
 
     public static ByteBuffer getValueOf(ColumnDefinition column, Row row, int nowInSecs)

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java b/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java
index b9c5653..1c85ed5 100644
--- a/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java
+++ b/src/java/org/apache/cassandra/index/sasi/conf/IndexMode.java
@@ -179,6 +179,6 @@ public class IndexMode
 
     public boolean supports(Op operator)
     {
-        return !(isLiteral && operator == Op.RANGE) && mode.supports(operator);
+        return mode.supports(operator);
     }
 }

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java
index 20a8739..04b7b1c 100644
--- a/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java
+++ b/src/java/org/apache/cassandra/index/sasi/disk/OnDiskIndexBuilder.java
@@ -50,8 +50,8 @@ public class OnDiskIndexBuilder
 
     public enum Mode
     {
-        PREFIX(EnumSet.of(Op.EQ, Op.PREFIX, Op.NOT_EQ, Op.RANGE)),
-        CONTAINS(EnumSet.of(Op.EQ, Op.CONTAINS, Op.SUFFIX, Op.NOT_EQ)),
+        PREFIX(EnumSet.of(Op.EQ, Op.MATCH, Op.PREFIX, Op.NOT_EQ, Op.RANGE)),
+        CONTAINS(EnumSet.of(Op.MATCH, Op.CONTAINS, Op.SUFFIX, Op.NOT_EQ)),
         SPARSE(EnumSet.of(Op.EQ, Op.NOT_EQ, Op.RANGE));
 
         Set<Op> supportedOps;

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java b/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java
index 99a417a..0da65c7 100644
--- a/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java
+++ b/src/java/org/apache/cassandra/index/sasi/memory/TrieMemIndex.java
@@ -182,6 +182,7 @@ public class TrieMemIndex extends MemIndex
             switch (operator)
             {
                 case EQ:
+                case MATCH:
                     ConcurrentSkipListSet<DecoratedKey> keys = trie.getValueForExactKey(value);
                     return keys == null ? Collections.emptyList() : Collections.singletonList(keys);
 
@@ -219,6 +220,7 @@ public class TrieMemIndex extends MemIndex
             switch (operator)
             {
                 case EQ:
+                case MATCH:
                     ConcurrentSkipListSet<DecoratedKey> keys = trie.getValueForExactKey(value);
                     return keys == null ? Collections.emptyList() : Collections.singletonList(keys);
 

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/plan/Expression.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/plan/Expression.java b/src/java/org/apache/cassandra/index/sasi/plan/Expression.java
index 43f8251..679d866 100644
--- a/src/java/org/apache/cassandra/index/sasi/plan/Expression.java
+++ b/src/java/org/apache/cassandra/index/sasi/plan/Expression.java
@@ -46,7 +46,7 @@ public class Expression
 
     public enum Op
     {
-        EQ, PREFIX, SUFFIX, CONTAINS, NOT_EQ, RANGE;
+        EQ, MATCH, PREFIX, SUFFIX, CONTAINS, NOT_EQ, RANGE;
 
         public static Op valueOf(Operator operator)
         {
@@ -73,6 +73,9 @@ public class Expression
                 case LIKE_CONTAINS:
                     return CONTAINS;
 
+                case LIKE_MATCHES:
+                    return MATCH;
+
                 default:
                     throw new IllegalArgumentException("unknown operator: " + operator);
             }
@@ -140,6 +143,7 @@ public class Expression
             case LIKE_PREFIX:
             case LIKE_SUFFIX:
             case LIKE_CONTAINS:
+            case LIKE_MATCHES:
             case EQ:
                 lower = new Bound(value, true);
                 upper = lower;
@@ -262,6 +266,7 @@ public class Expression
             switch (operation)
             {
                 case EQ:
+                case MATCH:
                 // Operation.isSatisfiedBy handles conclusion on !=,
                 // here we just need to make sure that term matched it
                 case NOT_EQ:

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/src/java/org/apache/cassandra/index/sasi/plan/Operation.java
----------------------------------------------------------------------
diff --git a/src/java/org/apache/cassandra/index/sasi/plan/Operation.java b/src/java/org/apache/cassandra/index/sasi/plan/Operation.java
index 28bcc51..f8b02a3 100644
--- a/src/java/org/apache/cassandra/index/sasi/plan/Operation.java
+++ b/src/java/org/apache/cassandra/index/sasi/plan/Operation.java
@@ -292,9 +292,13 @@ public class Operation extends RangeIterator<Long, Token>
             switch (e.operator())
             {
                 case EQ:
+                    isMultiExpression = false;
+                    break;
+
                 case LIKE_PREFIX:
                 case LIKE_SUFFIX:
                 case LIKE_CONTAINS:
+                case LIKE_MATCHES:
                     isMultiExpression = true;
                     break;
 
@@ -341,6 +345,7 @@ public class Operation extends RangeIterator<Long, Token>
             case LIKE_PREFIX:
             case LIKE_SUFFIX:
             case LIKE_CONTAINS:
+            case LIKE_MATCHES:
                 return 4;
 
             case GTE:

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java b/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
index 2ae1e70..a88e594 100644
--- a/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
+++ b/test/unit/org/apache/cassandra/index/sasi/SASIIndexTest.java
@@ -167,7 +167,7 @@ public class SASIIndexTest
 
         ColumnFamilyStore store = loadData(data, forceFlush);
 
-        Set<String> rows= getIndexed(store, 10, buildExpression(UTF8Type.instance.decompose("first_name"), Operator.EQ, UTF8Type.instance.decompose("doesntmatter")));
+        Set<String> rows= getIndexed(store, 10, buildExpression(UTF8Type.instance.decompose("first_name"), Operator.LIKE_MATCHES, UTF8Type.instance.decompose("doesntmatter")));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[]{}, rows.toArray(new String[rows.size()])));
     }
 
@@ -502,18 +502,18 @@ public class SASIIndexTest
         store = loadData(part4, forceFlush);
 
         rows = getIndexed(store, 10,
-                          buildExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("Susana")),
+                          buildExpression(firstName, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("Susana")),
                           buildExpression(age, Operator.LTE, Int32Type.instance.decompose(13)),
                           buildExpression(age, Operator.GT, Int32Type.instance.decompose(10)));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key12" }, rows.toArray(new String[rows.size()])));
 
         rows = getIndexed(store, 10,
-                          buildExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("Demario")),
+                          buildExpression(firstName, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("Demario")),
                           buildExpression(age, Operator.LTE, Int32Type.instance.decompose(30)));
         Assert.assertTrue(rows.toString(), rows.size() == 0);
 
         rows = getIndexed(store, 10,
-                          buildExpression(firstName, Operator.EQ, UTF8Type.instance.decompose("Josephine")));
+                          buildExpression(firstName, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("Josephine")));
         Assert.assertTrue(rows.toString(), rows.size() == 0);
 
         rows = getIndexed(store, 10,
@@ -1142,7 +1142,7 @@ public class SASIIndexTest
         rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("ン")));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key4", "key5" }, rows.toArray(new String[rows.size()])));
 
-        rows = getIndexed(store, 10, buildExpression(comment, Operator.EQ, UTF8Type.instance.decompose("レストラン")));
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("レストラン")));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key4" }, rows.toArray(new String[rows.size()])));
     }
 
@@ -1211,7 +1211,7 @@ public class SASIIndexTest
         rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_SUFFIX, UTF8Type.instance.decompose("ン")));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key3" }, rows.toArray(new String[rows.size()])));
 
-        rows = getIndexed(store, 10, buildExpression(comment, Operator.EQ, UTF8Type.instance.decompose("ベンジャミン ウエスト")));
+        rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("ベンジャミン ウエスト")));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key4" }, rows.toArray(new String[rows.size()])));
     }
 
@@ -1235,12 +1235,12 @@ public class SASIIndexTest
 
             Set<String> rows;
 
-            rows = getIndexed(store, 10, buildExpression(comment, Operator.EQ, bigValue.duplicate()));
+            rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, bigValue.duplicate()));
             Assert.assertEquals(0, rows.size());
 
             store.forceBlockingFlush();
 
-            rows = getIndexed(store, 10, buildExpression(comment, Operator.EQ, bigValue.duplicate()));
+            rows = getIndexed(store, 10, buildExpression(comment, Operator.LIKE_MATCHES, bigValue.duplicate()));
             Assert.assertEquals(0, rows.size());
         }
     }
@@ -1471,6 +1471,10 @@ public class SASIIndexTest
         update(rm, name, UTF8Type.instance.decompose("Vijay"), System.currentTimeMillis());
         rm.apply();
 
+        rm = new Mutation(KS_NAME, decoratedKey("key8")); // this name is going to be tokenized
+        update(rm, name, UTF8Type.instance.decompose("Jean-Claude"), System.currentTimeMillis());
+        rm.apply();
+
         // this flush is going to produce range - 'jason' -> 'vijay'
         store.forceBlockingFlush();
 
@@ -1478,11 +1482,12 @@ public class SASIIndexTest
         // since simple interval tree lookup is not going to cover it, prefix lookup actually required.
 
         Set<String> rows;
+
         rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("J")));
-        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key2", "key5", "key6" }, rows.toArray(new String[rows.size()])));
+        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key2", "key5", "key6", "key8"}, rows.toArray(new String[rows.size()])));
 
         rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("j")));
-        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key2", "key5", "key6" }, rows.toArray(new String[rows.size()])));
+        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key2", "key5", "key6", "key8" }, rows.toArray(new String[rows.size()])));
 
         rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("m")));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key3", "key4" }, rows.toArray(new String[rows.size()])));
@@ -1495,13 +1500,28 @@ public class SASIIndexTest
 
         rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_PREFIX, UTF8Type.instance.decompose("j")),
                                      buildExpression(name, Operator.NEQ, UTF8Type.instance.decompose("joh")));
-        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key2", "key6" }, rows.toArray(new String[rows.size()])));
+        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key2", "key6", "key8" }, rows.toArray(new String[rows.size()])));
 
-        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("pavel")));
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("pavel")));
         Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key1" }, rows.toArray(new String[rows.size()])));
 
         rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Pave")));
         Assert.assertTrue(rows.isEmpty());
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Pavel")));
+        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key1" }, rows.toArray(new String[rows.size()])));
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("JeAn")));
+        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key8" }, rows.toArray(new String[rows.size()])));
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("claUde")));
+        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key8" }, rows.toArray(new String[rows.size()])));
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Jean")));
+        Assert.assertTrue(rows.isEmpty());
+
+        rows = getIndexed(store, 10, buildExpression(name, Operator.EQ, UTF8Type.instance.decompose("Jean-Claude")));
+        Assert.assertTrue(rows.toString(), Arrays.equals(new String[] { "key8" }, rows.toArray(new String[rows.size()])));
     }
 
     @Test
@@ -1746,6 +1766,178 @@ public class SASIIndexTest
         }
     }
 
+    @Test
+    public void testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes()
+    {
+        String containsTable = "sasi_like_contains_test";
+        String prefixTable = "sasi_like_prefix_test";
+        String analyzedPrefixTable = "sasi_like_analyzed_prefix_test";
+
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, containsTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, prefixTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE TABLE IF NOT EXISTS %s.%s (k int primary key, v text);", KS_NAME, analyzedPrefixTable));
+
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS ON %s.%s(v) " +
+                "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = { 'mode' : 'CONTAINS' };", KS_NAME, containsTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS ON %s.%s(v) " +
+                "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = { 'mode' : 'PREFIX' };", KS_NAME, prefixTable));
+        QueryProcessor.executeOnceInternal(String.format("CREATE CUSTOM INDEX IF NOT EXISTS ON %s.%s(v) " +
+                "USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = { 'mode' : 'PREFIX', 'analyzed': 'true' };", KS_NAME, analyzedPrefixTable));
+
+        testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes(containsTable, prefixTable, analyzedPrefixTable, false);
+        testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes(containsTable, prefixTable, analyzedPrefixTable, true);
+    }
+
+    private void testLIKEAndEQSemanticsWithDifferenceKindsOfIndexes(String containsTable,
+                                                                    String prefixTable,
+                                                                    String analyzedPrefixTable,
+                                                                    boolean forceFlush)
+    {
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (?, ?);", KS_NAME, containsTable), 0, "Pavel");
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (?, ?);", KS_NAME, prefixTable), 0, "Jean-Claude");
+        QueryProcessor.executeOnceInternal(String.format("INSERT INTO %s.%s (k, v) VALUES (?, ?);", KS_NAME, analyzedPrefixTable), 0, "Jean-Claude");
+
+        if (forceFlush)
+        {
+            Keyspace keyspace = Keyspace.open(KS_NAME);
+            for (String table : Arrays.asList(containsTable, prefixTable, analyzedPrefixTable))
+                keyspace.getColumnFamilyStore(table).forceBlockingFlush();
+        }
+
+        UntypedResultSet results;
+
+        // CONTAINS
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pav';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pavel';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Pav';", KS_NAME, containsTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since CONTAINS indexes only support LIKE
+        }
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Pav%%';", KS_NAME, containsTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since CONTAINS indexes only support LIKE '%<term>' and LIKE '%<term>%'
+        }
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Pav';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Pav%%';", KS_NAME, containsTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        // PREFIX
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Jean';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Jean-Claude';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jea';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(0, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jea%%';", KS_NAME, prefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Jea';", KS_NAME, prefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%'
+        }
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Jea%%';", KS_NAME, prefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%'
+        }
+
+        // PREFIX + analyzer
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v = 'Jean';", KS_NAME, analyzedPrefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support EQ without tokenization
+        }
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jean';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Claude';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jean-Claude';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Jean%%';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        results = QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE 'Claude%%';", KS_NAME, analyzedPrefixTable));
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Jean';", KS_NAME, analyzedPrefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%' and LIKE '<term>'
+        }
+
+        try
+        {
+            QueryProcessor.executeOnceInternal(String.format("SELECT * FROM %s.%s WHERE v LIKE '%%Claude%%';", KS_NAME, analyzedPrefixTable));
+            Assert.fail();
+        }
+        catch (InvalidRequestException e)
+        {
+            // expected since PREFIX indexes only support LIKE '<term>%' and LIKE '<term>'
+        }
+
+        for (String table : Arrays.asList(containsTable, prefixTable, analyzedPrefixTable))
+            QueryProcessor.executeOnceInternal(String.format("TRUNCATE TABLE %s.%s", KS_NAME, table));
+    }
+
     private static ColumnFamilyStore loadData(Map<String, Pair<String, Integer>> data, boolean forceFlush)
     {
         return loadData(data, System.currentTimeMillis(), forceFlush);
@@ -1860,7 +2052,8 @@ public class SASIIndexTest
                 {
                     try (UnfilteredRowIterator row = rows.next())
                     {
-                        add(AsciiType.instance.compose(row.partitionKey().getKey()));
+                        if (!row.isEmpty())
+                            add(AsciiType.instance.compose(row.partitionKey().getKey()));
                     }
                 }
             }};

http://git-wip-us.apache.org/repos/asf/cassandra/blob/479e8aff/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
----------------------------------------------------------------------
diff --git a/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java b/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
index 4f38b92..cf2b8c0 100644
--- a/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
+++ b/test/unit/org/apache/cassandra/index/sasi/plan/OperationTest.java
@@ -181,21 +181,21 @@ public class OperationTest extends SchemaLoader
 
         // comment = 'soft eng' and comment != 'likes do'
         ListMultimap<ColumnDefinition, Expression> e = Operation.analyzeGroup(controller, OperationType.OR,
-                                                    Arrays.asList(new SimpleExpression(comment, Operator.EQ, UTF8Type.instance.decompose("soft eng")),
+                                                    Arrays.asList(new SimpleExpression(comment, Operator.LIKE_MATCHES, UTF8Type.instance.decompose("soft eng")),
                                                                   new SimpleExpression(comment, Operator.NEQ, UTF8Type.instance.decompose("likes do"))));
 
         List<Expression> expectedExpressions = new ArrayList<Expression>(2)
         {{
                 add(new Expression("comment", UTF8Type.instance)
                 {{
-                        operation = Op.EQ;
+                        operation = Op.MATCH;
                         lower = new Bound(UTF8Type.instance.decompose("soft"), true);
                         upper = lower;
                 }});
 
                 add(new Expression("comment", UTF8Type.instance)
                 {{
-                        operation = Op.EQ;
+                        operation = Op.MATCH;
                         lower = new Bound(UTF8Type.instance.decompose("eng"), true);
                         upper = lower;
                 }});