Skip to content

Commit

Permalink
implement FullTextIndex and FullTextExpression (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
bangfalse authored Oct 8, 2020
1 parent 6a3a8be commit c9102f7
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.couchbase.lite.DocumentReplication;
import com.couchbase.lite.DocumentReplicationListener;
import com.couchbase.lite.Expression;
import com.couchbase.lite.FullTextIndex;
import com.couchbase.lite.FullTextIndexItem;
import com.couchbase.lite.IndexBuilder;
import com.couchbase.lite.ListenerToken;
import com.couchbase.lite.LogLevel;
Expand Down Expand Up @@ -141,6 +143,33 @@ private ValueIndex inflateValueIndex(List<Map<String, Object>> items) {
return IndexBuilder.valueIndex(array);
}

private FullTextIndex inflateFullTextIndex(List<Map<String, Object>> items) {
List<FullTextIndexItem> indices = new ArrayList<>();
Boolean ignoreAccents = null;
String language = null;
for (int i=0; i < items.size(); ++i) {
Map<String, Object> item = items.get(i);
if (item.containsKey("property")) {
String property = (String) item.get("property");
assert property != null;
indices.add(FullTextIndexItem.property(property));
} else if (item.containsKey("ignoreAccents")) {
ignoreAccents = (Boolean) item.get("ignoreAccents");
} else if (item.containsKey("language")) {
language = (String) item.get("language");
}
}

FullTextIndex index = IndexBuilder.fullTextIndex(indices.toArray(new FullTextIndexItem[0]));
if (ignoreAccents != null) {
index = index.ignoreAccents(ignoreAccents);
}
if (language != null) {
index = index.setLanguage(language);
}
return index;
}

private class DatabaseCallHander implements MethodCallHandler {
@Override
public void onMethodCall(MethodCall call, @NonNull final Result result) {
Expand Down Expand Up @@ -264,6 +293,49 @@ public void run() {
});


} else {
result.error("errArg", "invalid arguments", null);
}

break;
case ("createFullTextIndex"):
if (database == null) {
result.error("errDatabase", "Database with name " + dbname + "not found", null);
return;
}

if (call.hasArgument("index") && call.hasArgument("withName")) {
final List<Map<String, Object>> items = call.argument("index");
final String indexName = call.argument("withName");

final Database db = database;
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
assert items != null;
FullTextIndex fullTextIndex = inflateFullTextIndex(items);
assert indexName != null;
assert fullTextIndex != null;
db.createIndex(indexName, fullTextIndex);
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
result.success(true);
}
});
} catch (final CouchbaseLiteException e) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
result.error("errIndex", "Error creating index", e.toString());
}
});
}
}
});


} else {
result.error("errArg", "invalid arguments", null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.couchbase.lite.DataSource;
import com.couchbase.lite.Expression;
import com.couchbase.lite.From;
import com.couchbase.lite.FullTextExpression;
import com.couchbase.lite.FullTextFunction;
import com.couchbase.lite.Function;
import com.couchbase.lite.GroupBy;
import com.couchbase.lite.Join;
Expand Down Expand Up @@ -344,6 +346,9 @@ static Expression inflateExpressionFromArray(List<Map<String, Object>> expressio
case ("value"):
returnExpression = Expression.value(currentExpression.get("value"));
break;
case ("rank"):
returnExpression = FullTextFunction.rank((String)currentExpression.get("rank"));
break;
case ("abs"):
returnExpression = Function.abs(inflateExpressionFromArray(QueryMap.getListOfMapFromGenericList(currentExpression.get("abs"))));
break;
Expand Down Expand Up @@ -454,6 +459,10 @@ static Expression inflateExpressionFromArray(List<Map<String, Object>> expressio
case ("upper"):
returnExpression = Function.upper(inflateExpressionFromArray(QueryMap.getListOfMapFromGenericList(currentExpression.get("upper"))));
break;
case ("fullTextMatch"):
List<String> values = getStringList(currentExpression.get("fullTextMatch"));
returnExpression = FullTextExpression.index(values.get(0)).match(values.get(1));
break;

}
} else {
Expand Down Expand Up @@ -548,6 +557,18 @@ static Expression inflateExpressionFromArray(List<Map<String, Object>> expressio
}
return returnExpression;
}

private static List<String> getStringList(Object valueList) {
List<String> result = new ArrayList<>();
if (valueList instanceof List<?>) {
for (Object value : (List<?>)valueList) {
if (value instanceof String) {
result.add((String)value);
}
}
}
return result;
}
}

class QueryMap {
Expand Down
8 changes: 7 additions & 1 deletion ios/Classes/QueryJson.swift
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,8 @@ public class QueryJson {
returnExpression = Expression.string(value)
case ("value", let value, _):
returnExpression = Expression.value(value)
case ("rank", let value as String, _):
returnExpression = FullTextFunction.rank(value)
case ("abs", let value, _):
returnExpression = Function.abs(inflateExpressionFromArray(expressionParametersArray:
QueryMap.getListOfMapFromGenericList(objectList: value)))
Expand Down Expand Up @@ -561,7 +563,11 @@ public class QueryJson {
case ("upper", let value, _):
returnExpression = Function.upper(inflateExpressionFromArray(expressionParametersArray:
QueryMap.getListOfMapFromGenericList(objectList: value)))

case ("fullTextMatch", let value as [String], _):
returnExpression =
FullTextExpression
.index(value[0])
.match(value[1])
default:
break
}
Expand Down
59 changes: 59 additions & 0 deletions ios/Classes/SwiftCouchbaseLitePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,36 @@ public class SwiftCouchbaseLitePlugin: NSObject, FlutterPlugin, CBManagerDelegat

}

private func inflateFullTextIndex(items: [Dictionary<String, Any>] ) -> FullTextIndex? {

var indices: Array<FullTextIndexItem> = [];
var ignoreAccents: Bool?
var language: String?
for item in items {
if let value = item["property"], let name = value as? String {
indices.append(FullTextIndexItem.property(name))
} else if let value = item["ignoreAccents"], let boolValue = value as? Bool {
ignoreAccents = boolValue
} else if let value = item["language"], let languageCode = value as? String {
language = languageCode
} else {
return nil //Unsupported full-text index item
}
}

var index = IndexBuilder.fullTextIndex(items: indices)
if let ignoreAccents = ignoreAccents{
index = index.ignoreAccents(ignoreAccents)
}
if let language = language {
index = index.language(language)
}


return index

}

public func handleDatabase(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch (call.method) {
case "clearBlobCache":
Expand Down Expand Up @@ -148,6 +178,35 @@ public class SwiftCouchbaseLitePlugin: NSObject, FlutterPlugin, CBManagerDelegat
}
}
}

case "createFullTextIndex":
guard let database = mCBManager.getDatabase(name: dbname) else {
result(FlutterError.init(code: "errDatabase", message: "Database with name \(dbname) not found", details: nil))
return
}
guard let indexName = arguments["withName"] as? String, let index = arguments["index"] as? [Dictionary<String, Any>] else {
result(FlutterError.init(code: "errArgs", message: "Error: Invalid Arguments", details: call.arguments.debugDescription))
return
}

guard let fullTextIndex = inflateFullTextIndex(items: index) else {
result(FlutterError.init(code: "errIndex", message: "Error creating index \(indexName)", details: "Failed to inflate fullTextIndex"))
return
}

databaseDispatchQueue.async {
do {
try database.createIndex(fullTextIndex, withName: indexName);
DispatchQueue.main.async {
result(true)
}
} catch {
DispatchQueue.main.async {
result(FlutterError.init(code: "errIndex", message: "Error creating index \(indexName)", details: error.localizedDescription))
}
}
}


case "deleteIndex":
guard let database = mCBManager.getDatabase(name: dbname) else {
Expand Down
1 change: 1 addition & 0 deletions lib/couchbase_lite.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ part 'src/index.dart';
part 'src/listener_token.dart';
part 'src/mutable_document.dart';
part 'src/query/expression/expression.dart';
part 'src/query/expression/full_text_expression.dart';
part 'src/query/expression/meta.dart';
part 'src/query/expression/meta_expression.dart';
part 'src/query/expression/property_expression.dart';
Expand Down
16 changes: 14 additions & 2 deletions lib/src/database.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,20 @@ class Database {
/// Creates an index [withName] which could be a value index or a full-text search index.
/// The name can be used for deleting the index. Creating a new different index with an existing index
/// name will replace the old index; creating the same index with the same name will be no-ops.
Future<bool> createIndex(ValueIndex index, {@required String withName}) {
return _methodChannel.invokeMethod('createIndex', <String, dynamic>{
Future<bool> createIndex(Index index, {@required String withName}) {
var methodName;
if (index is ValueIndex) {
methodName = 'createIndex';
} else if (index is FullTextIndex) {
methodName = 'createFullTextIndex';
} else {
throw ArgumentError.value(
index.runtimeType,
'index',
'unknown index type',
);
}
return _methodChannel.invokeMethod(methodName, <String, dynamic>{
'database': name,
'index': index.toJson(),
'withName': withName
Expand Down
59 changes: 58 additions & 1 deletion lib/src/index.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
part of couchbase_lite;

abstract class Index {
List<Map<String, dynamic>> toJson();
}

class ValueIndexItem {
ValueIndexItem(this._map);

Expand All @@ -19,11 +23,12 @@ class ValueIndexItem {
Map<String, dynamic> toJson() => _map;
}

class ValueIndex {
class ValueIndex extends Index {
ValueIndex(this._valueIndexItems);

final List<ValueIndexItem> _valueIndexItems;

@override
List<Map<String, dynamic>> toJson() {
var map = <Map<String, dynamic>>[];
for (var item in _valueIndexItems) {
Expand All @@ -33,9 +38,61 @@ class ValueIndex {
}
}

class FullTextIndexItem {
FullTextIndexItem(this._map);

/// Creates a FullTextIndexItem for the given [property]
factory FullTextIndexItem.property(String property) {
return FullTextIndexItem({'property': property});
}

Map<String, dynamic> _map;

/// Returns the json representation of this object
Map<String, dynamic> toJson() => _map;
}

class FullTextIndex extends Index {
FullTextIndex(this._fullTextIndexItems);

final List<FullTextIndexItem> _fullTextIndexItems;
bool _ignoreAccents;
String _language;

FullTextIndex ignoreAccents(bool ignoreAccents) {
_ignoreAccents = ignoreAccents;
return this;
}

FullTextIndex language(String language) {
_language = language;
return this;
}

@override
List<Map<String, dynamic>> toJson() {
var map = <Map<String, dynamic>>[];
for (var item in _fullTextIndexItems) {
map.add(item.toJson());
}
if (_ignoreAccents != null) {
map.add({'ignoreAccents': _ignoreAccents});
}
if (_language != null) {
map.add({'language': _language});
}
return map;
}
}

class IndexBuilder {
/// Creates a value index with the given index items. The index items are a list of the properties or expressions to be indexed.
static ValueIndex valueIndex({@required List<ValueIndexItem> items}) {
return ValueIndex(items);
}

/// Creates a full-text index with the given index items. The index items are a list of the properties to be indexed.
static FullTextIndex fullTextIndex({@required List<FullTextIndexItem> items}) {
return FullTextIndex(items);
}
}
39 changes: 39 additions & 0 deletions lib/src/query/expression/full_text_expression.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
part of couchbase_lite;

class FullTextExpressionIndex {
final String _name;
FullTextExpressionIndex._(this._name);

FullTextExpression match(String query) {
return FullTextExpression._match(_name, query);
}
}

class FullTextExpression extends Object with Expression {
static FullTextExpressionIndex index(String name) {
return FullTextExpressionIndex._(name);
}

factory FullTextExpression.rank(String indexName) {
final expression = FullTextExpression._();
expression._internalExpressionStack.add({'rank': indexName});
return expression;
}

FullTextExpression._();

FullTextExpression._match(String indexName, String query) {
_internalExpressionStack.add({
'fullTextMatch': [indexName, query],
});
}

FullTextExpression._clone(FullTextExpression expression) {
_internalExpressionStack.addAll(expression.internalExpressionStack);
}

@override
FullTextExpression _clone() {
return FullTextExpression._clone(this);
}
}

2 comments on commit c9102f7

@richard457
Copy link

@richard457 richard457 commented on c9102f7 Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was no example of how to add this Index to the database I guess but not sure it is like this @bangfalse bangfalse @bawelter @tediyuwono @SaltechDevelopers @lchristille

const String FullTextIndex = 'FullTextIndex';
if(!indices.contains(FullTextIndex)){
         final ValueIndex index = IndexBuilder.valueIndex(items: [
          ValueIndexItem.property('type'),
          ValueIndexItem.expression(Expression.property('name'))
        ]);
        await database.createIndex(index, withName: FullTextIndex);
      }

Correct me if I am wrong

@bangfalse
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to use FullTextIndexItem and IndexBuilder.fullTextIndex to build an FTS index. An example usage looks like this:

await _database.createIndex(
  IndexBuilder.fullTextIndex(items: [
    FullTextIndexItem.property('text'),
  ]),
  withName: 'IX_FT_EntryText',
);

This follows the project's stated goal:

to align this library with the Swift SDK API for Couchbase Lite

Note that only properties can currently be indexed by Couchbase Lite FTS, not expressions.

Please sign in to comment.