I have some code that stores, in redis, a flag of whether a user is active, under a unique key per user.
class RedisProfileActiveRepo implements ProfileActiveRepo
{
/** #var Redis */
private $redis;
public function __construct(Redis $redis)
{
$this->redis = $redis;
}
public function markProfileIsActive(int $profile_id)
{
$keyname = ProfileIsActiveKey::getAbsoluteKeyName($profile_id);
// Set the user specific key for 10 minutes
$result = $this->redis->setex($keyname, 10 * 60, 'foobar');
}
public function getNumberOfActiveProfiles()
{
$count = 0;
$pattern = ProfileIsActiveKey::getWildcardKeyName();
$iterator = null;
while (($keys = $this->redis->scan($iterator, $pattern)) !== false) {
$count += count($keys);
}
return $count;
}
}
When I generate the keys from this code:
namespace ProjectName;
class ProfileIsActive
{
public static function getAbsoluteKeyName(int $profile_id) : string
{
return __CLASS__ . '_' . $profile_id;
}
public static function getWildcardKeyName() : string
{
return __CLASS__ . '_*';
}
}
Which results in the keys looking like ProjectName\ProfileIsActive_1234 the scan command in Redis fails to match any keys.
When I replace the slashes with underscores:
class ProfileIsActive
{
public static function getAbsoluteKeyName(int $profile_id) : string
{
return str_replace('\\', '', __CLASS__) . '_' . $profile_id;
}
public static function getWildcardKeyName() : string
{
return str_replace('\\', '', __CLASS__) . '_*';
}
}
The code works as expected.
My question is - why is doing a scan with a slash in the keyname failing to behave as expected, and are there any other characters that should be avoided in keynames to avoid similar problems?
Theoretically latest Redis autoescapes backslashes when you set keys at redis-cli:
127.0.0.1:6379> set this\test 1
OK
127.0.0.1:6379> keys this*
1) "this\\test"
Issue a MONITOR command in redis-cli before you run your php client code, and watch for SCAN commands. If your collection is big enough and your count parameter is absent or low enough you might not get the record:
127.0.0.1:6379> scan 0 match this*
1) "73728"
2) (empty list or set)
127.0.0.1:6379> scan 0 match this* count 10000
1) "87704"
2) 1) "this\\test"
Related
I am using the highlighting feature of Lucene to isolate matching terms for my query, but some of the matched terms are excessive.
I have some simple test cases which are delivered in an Ant project (download details below).
Materials
You can download the test case here: mydemo_with_libs.zip
That archive includes the Lucene 8.6.3 libraries which my test uses; if you prefer a copy without the JAR files you can download that from here: mydemo_without_libs.zip
The necessary libraries are: core, analyzers, queries, queryparser, highlighter, and memory.
You can run the test case by unzipping the archive into an empty directory and running the Ant command ant synsearch
Input
I have provided a short synonym list which is used for indexing and analysing in the highlighting methods:
cope,manage
jobs,tasks
simultaneously,at once
and there is one document being indexed:
Queues are a useful way of grouping jobs together in order to manage a number of them at once. You can:
hold or release multiple jobs at the same time;
group multiple tasks (for the same event);
control the priority of jobs in the queue;
Eventually log all events that take place in a queue.
Use either job.queue or task.queue in specifications.
Process
When building the index I am storing the text field, and using a custom analyzer. This is because (in the real world) the content I am indexing is technical documentation, so stripping out punctuation is inappropriate because so much of it may be significant in technical expressions. My analyzer uses a TechTokenFilter which breaks the stream up into tokens consisting of strings of words or digits, or individual characters which don't match the previous pattern.
Here's the relevant code for the analyzer:
public class MyAnalyzer extends Analyzer {
public MyAnalyzer(String synlist) {
if (synlist != "") {
this.synlist = synlist;
this.useSynonyms = true;
}
}
public MyAnalyzer() {
this.useSynonyms = false;
}
#Override
protected TokenStreamComponents createComponents(String fieldName) {
WhitespaceTokenizer src = new WhitespaceTokenizer();
TokenStream result = new TechTokenFilter(new LowerCaseFilter(src));
if (useSynonyms) {
result = new SynonymGraphFilter(result, getSynonyms(synlist), Boolean.TRUE);
result = new FlattenGraphFilter(result);
}
return new TokenStreamComponents(src, result);
}
and here's my filter:
public class TechTokenFilter extends TokenFilter {
private final CharTermAttribute termAttr;
private final PositionIncrementAttribute posIncAttr;
private final ArrayList<String> termStack;
private AttributeSource.State current;
private final TypeAttribute typeAttr;
public TechTokenFilter(TokenStream tokenStream) {
super(tokenStream);
termStack = new ArrayList<>();
termAttr = addAttribute(CharTermAttribute.class);
posIncAttr = addAttribute(PositionIncrementAttribute.class);
typeAttr = addAttribute(TypeAttribute.class);
}
#Override
public boolean incrementToken() throws IOException {
if (this.termStack.isEmpty() && input.incrementToken()) {
final String currentTerm = termAttr.toString();
final int bufferLen = termAttr.length();
if (bufferLen > 0) {
if (termStack.isEmpty()) {
termStack.addAll(Arrays.asList(techTokens(currentTerm)));
current = captureState();
}
}
}
if (!this.termStack.isEmpty()) {
String part = termStack.remove(0);
restoreState(current);
termAttr.setEmpty().append(part);
posIncAttr.setPositionIncrement(1);
return true;
} else {
return false;
}
}
public static String[] techTokens(String t) {
List<String> tokenlist = new ArrayList<String>();
String[] tokens;
StringBuilder next = new StringBuilder();
String token;
char minus = '-';
char underscore = '_';
char c, prec, subc;
// Boolean inWord = false;
for (int i = 0; i < t.length(); i++) {
prec = i > 0 ? t.charAt(i - 1) : 0;
c = t.charAt(i);
subc = i < (t.length() - 1) ? t.charAt(i + 1) : 0;
if (Character.isLetterOrDigit(c) || c == underscore) {
next.append(c);
// inWord = true;
}
else if (c == minus && Character.isLetterOrDigit(prec) && Character.isLetterOrDigit(subc)) {
next.append(c);
} else {
if (next.length() > 0) {
token = next.toString();
tokenlist.add(token);
next.setLength(0);
}
if (Character.isWhitespace(c)) {
// shouldn't be possible because the input stream has been tokenized on
// whitespace
} else {
tokenlist.add(String.valueOf(c));
}
// inWord = false;
}
}
if (next.length() > 0) {
token = next.toString();
tokenlist.add(token);
// next.setLength(0);
}
tokens = tokenlist.toArray(new String[0]);
return tokens;
}
}
Examining the index I can see that the index contains the separate terms I expect, including the synonym values. For example the text at the end of the first line has produced the terms
of
them
at , simultaneously
once
.
You
can
:
and the text at the end of the third line has produced the terms
same
event
)
;
When the application performs a search it analyzes the query without using the synonym list (because the synonyms are already in the index), but I have discovered that I need to include the synonym list when analyzing the stored text to identify the matching fragments.
Searches match the correct documents, but the code I have added to identify the matching terms over-performs. I won't show all the search method here, but will focus on the code which lists matched terms:
public static void doSearch(IndexReader reader, IndexSearcher searcher,
Query query, int max, String synList) throws IOException {
SimpleHTMLFormatter htmlFormatter = new SimpleHTMLFormatter("\001", "\002");
Highlighter highlighter = new Highlighter(htmlFormatter, new QueryScorer(query));
Analyzer analyzer;
if (synList != null) {
analyzer = new MyAnalyzer(synList);
} else {
analyzer = new MyAnalyzer();
}
// Collect all the docs
TopDocs results = searcher.search(query, max);
ScoreDoc[] hits = results.scoreDocs;
int numTotalHits = Math.toIntExact(results.totalHits.value);
System.out.println("\nQuery: " + query.toString());
System.out.println("Matches: " + numTotalHits);
// Collect matching terms
HashSet<String> matchedWords = new HashSet<String>();
int start = 0;
int end = Math.min(numTotalHits, max);
for (int i = start; i < end; i++) {
int id = hits[i].doc;
float score = hits[i].score;
Document doc = searcher.doc(id);
String docpath = doc.get("path");
String doctext = doc.get("text");
try {
TokenStream tokens = TokenSources.getTokenStream("text", null, doctext, analyzer, -1);
TextFragment[] frag = highlighter.getBestTextFragments(tokens, doctext, false, 100);
for (int j = 0; j < frag.length; j++) {
if ((frag[j] != null) && (frag[j].getScore() > 0)) {
String match = frag[j].toString();
addMatchedWord(matchedWords, match);
}
}
} catch (InvalidTokenOffsetsException e) {
System.err.println(e.getMessage());
}
System.out.println("matched file: " + docpath);
}
if (matchedWords.size() > 0) {
System.out.println("matched terms:");
for (String word : matchedWords) {
System.out.println(word);
}
}
}
Problem
While the correct documents are selected by these queries, and the fragments chosen for highlighting do contain the query terms, the highlighted pieces in some of the selected fragments extend over too much of the input.
For example, if the query is
+text:event +text:manage
(the first example in the test case) then I would expect to see 'event' and 'manage' in the highlighted list. But what I actually see is
event);
manage
Despite the highlighting process using an analyzer which breaks terms apart and treats punctuation characters as single terms, the highlight code is "hungry" and breaks on whitespace alone.
Similarly if the query is
+text:queeu~1
(my final test case) I would expect to only see 'queue' in the list. But I get
queue.
job.queue
task.queue
queue;
It is so nearly there... but I don't understand why the highlighted pieces are inconsistent with the index, and I don't think I should have to parse the list of matches through yet another filter to produce the correct list of matches.
I would really appreciate any pointers to what I am doing wrong or how I could improve my code to deliver exactly what I need.
Thanks for reading this far!
I managed to get this working by replacing the WhitespaceTokenizer and TechTokenFilter in my analyzer with a PatternTokenizer; the regular expression took a bit of work but once I had it all the matching terms were extracted with pinpoint accuracy.
The replacement analyzer:
public class MyAnalyzer extends Analyzer {
public MyAnalyzer(String synlist) {
if (synlist != "") {
this.synlist = synlist;
this.useSynonyms = true;
}
}
public MyAnalyzer() {
this.useSynonyms = false;
}
private static final String tokenRegex = "(([\\w]+-)*[\\w]+)|[^\\w\\s]";
#Override
protected TokenStreamComponents createComponents(String fieldName) {
PatternTokenizer src = new PatternTokenizer(Pattern.compile(tokenRegex), 0);
TokenStream result = new LowerCaseFilter(src);
if (useSynonyms) {
result = new SynonymGraphFilter(result, getSynonyms(synlist), Boolean.TRUE);
result = new FlattenGraphFilter(result);
}
return new TokenStreamComponents(src, result);
}
got crash when the ids is > 999
android.database.sqlite.SQLiteException: too many SQL variables (code 1): ,
while compiling: delete from data where ids in (?,?,...)
saw this seems there is max limit to 999.
how to delete more than 1000 with Room?
Probably you have a list of ids to delete. Open a transaction, split the list in sublist and execute the SQL delete operation once for sublist.
For more information about Room the official documentation about Transactions with Room.
I didn't test the following code, but I think that it accomplishes your need.
#Dao
public interface DataDao {
#Delete("delete from data where ids in :filterValues")
long delete(List<String> filterValues)
#Transaction
public void deleteData(List<Data> dataToDelete) {
// split the array in list of 100 elements (change as you prefer but < 999)
List<List<Data>> subLists=DeleteHelper.chopped(dataToDelete, 100);
List<String> ids=new ArrayList<>();
for (List<Data> list: subList) {
list.clear();
for (Data item: list) {
ids.add(item.getId());
}
delete(ids);
}
}
}
public abstract class DeleteHelper {
// chops a list into non-view sublists of length L
public static <T> List<List<T>> chopped(List<T> list, final int L) {
List<List<T>> parts = new ArrayList<List<T>>();
final int N = list.size();
for (int i = 0; i < N; i += L) {
parts.add(new ArrayList<T>(
list.subList(i, Math.min(N, i + L)))
);
}
return parts;
}
}
I hope this help.
I think there are two ways to solve it.
First, chop chop your list and runs multiple times with delete method. (just like #xcesco answered)
Second, you can write very long query and run it with #RawQuery.
#RawQuery
abstract int simpleRawQuery(SupportSQLiteQuery sqliteQuery)
#Transaction
public int deleteData(List<Long> pkList) {
SimpleSQLiteQuery query = new SimpleSQLiteQuery("DELETE FROM tb WHERE _id IN (" + StringUtils.join(pkList,",") + ")";
return simpleRawQuery(query)
}
// #param physicalAddress - the actual address of the home a host wants to list (not the ethereum address)
// #return _id - list of ids for homes
function listHomesByAddress(string _physicalAddress) public returns(uint [] _id ) {
uint [] results;
for(uint i = 0 ; i<homes.length; i++) {
if(keccak256(homes[i].physicalAddress) == keccak256(_physicalAddress) && homes[i].available == true) {
results.push(homes[i].id);
}
}
return results;
}
The result is supposed to be a list of ids which match the physical address entered however it does not filter through but returns all the available homes.
When I change to using String utils nothing changes.
Here is the whole code:
pragma solidity ^0.4.0;
import "browser/StringUtils.sol";
// #title HomeListing
contract HomeListing {
struct Home {
uint id;
string physicalAddress;
bool available;
}
Home[] public homes;
mapping (address => Home) hostToHome;
event HomeEvent(uint _id);
event Test(uint length);
constructor() {
}
// #param physicalAddress - the actual address of the home a host wants to list (not the ethereum address)
function addHome(string _physicalAddress) public {
uint _id = uint(keccak256(_physicalAddress, msg.sender));
homes.push(Home(_id, _physicalAddress, true));
}
// #param physicalAddress - the actual address of the home a host wants to list (not the ethereum address)
// #return _id - list of ids for homes
function listHomesByAddress(string _physicalAddress) public returns(uint [] _id ) {
uint [] results;
for(uint i = 0 ; i<homes.length; i++) {
string location = homes[i].physicalAddress;
if(StringUtils.equal(location,_physicalAddress )) {
results.push(homes[i].id);
}
}
return results;
}
}
The part giving you trouble is the line uint[] results;. Arrays declared as local variables reference storage memory by default. From the "What is the memory keyword" section of the Solidity docs:
There are defaults for the storage location depending on which type of variable it concerns:
state variables are always in storage
function arguments are in memory by default
local variables of struct, array or mapping type reference storage by default
local variables of value type (i.e. neither array, nor struct nor mapping) are stored in the stack
The result is you're referencing the first storage slot of your contract, which happens to be Home[] public homes. That's why you're getting the entire array back.
To fix the problem, you need to use a memory array. However, you have an additional problem in that you can't use dynamic memory arrays in Solidity. A workaround is to decide on a result size limit and declare your array statically.
Example (limited to 10 results):
function listHomesByAddress(string _physicalAddress) public view returns(uint[10]) {
uint [10] memory results;
uint j = 0;
for(uint i = 0 ; i<homes.length && j < 10; i++) {
if(keccak256(homes[i].physicalAddress) == keccak256(_physicalAddress) && homes[i].available == true) {
results[j++] = homes[i].id;
}
}
return results;
}
I'm would like to check that all my keys in redis are correct.
I'm storing the keys in groups like so:
userid:fname
userid:lname
userid:age
...
I would like to iterate over the them by grouping them by userid and then check each group from fname, lname and age.
How can I do this?
ScanParams params = new ScanParams();
params.match("userid:fname*");
// Use "0" to do a full iteration of the collection.
ScanResult<String> scanResult = jedis.scan("0", params);
List<String> keys = scanResult.getResult();
Repeat above code for lname and age. Or, match user:id and then filter the groups using a regex and iterating through keys.
EDIT: For large collections (millions of keys), a scan result will return a few tens of elements. Adjust your code accordingly to continue scanning until the whole collection of keys has been scanned:
ScanParams params = new ScanParams();
params.match("userid:fname*");
// An iteration starts at "0": http://redis.io/commands/scan
ScanResult<String> scanResult = jedis.scan("0", params);
List<String> keys = scanResult.getResult();
String nextCursor = scanResult.getStringCursor();
int counter = 0;
while (true) {
for (String key : keys) {
addKeyToProperGroup(key);
}
// An iteration also ends at "0"
if (nextCursor.equals("0")) {
break;
}
scanResult = jedis.scan(nextCursor, params);
nextCursor = scanResult.getStringCursor();
keys = scanResult.getResult();
}
You can use Redis based java.util.Iterator and java.lang.Iterable interfaces offered by Redisson Redis client.
Here is an example:
RedissonClient redissonClient = RedissonClient.create(config);
RKeys keys = redissonClient.getKeys();
// default batch size on each SCAN invocation is 10
for (String key: keys.getKeys()) {
...
}
// default batch size on each SCAN invocation is 250
for (String key: keys.getKeys(250)) {
...
}
I am struggeling on how to use indexes in cypher.
After creating and indexing nodes in java
I am fine with executing cypher queries on those nodes.
I am fine as well with querying those nodes using the created index in java.
However, when I call the index in the cypher statement I get an MissingIndexException.
So, why can't cypher find the index? Do I have to create a separate cypher index? (I have not found anything about that)
I am using version 1.8.2
Here's what I did:
public class IndexTester {
String DB_PATH = "target/java-query-db";
String resultString ="";
GraphDatabaseService db = new GraphDatabaseFactory().newEmbeddedDatabase( DB_PATH );
ExecutionEngine engine = new ExecutionEngine( db );
IndexManager index = db.index();
Index<Node> personIndex;
Node n;
Node n1;
public static void main( String[] args )
{
IndexTester indexTester = new IndexTester();
indexTester.runIndex();
}
public void runIndex(){
Transaction tx = db.beginTx();
try
{
personIndex = index.forNodes( "person" );
n = createAndIndexNode("type", "adult", personIndex, db);
addPropertyAndIndexNode("name", "John", personIndex, n);
addPropertyAndIndexNode("id", "1", personIndex, n);
n1 = createAndIndexNode("type", "adult", personIndex, db);
addPropertyAndIndexNode("name", "Jane", personIndex, n1);
addPropertyAndIndexNode("id", "2", personIndex, n1);
//This works fine!!
Node foundNode = personIndex.get("name", "John").getSingle();
System.out.println("Found Node: " + foundNode.getProperty("name"));
//This throws a MissingIndexException
resultString = engine.execute( "start m=node:personIndex(name= 'John') return m" ).toString();
System.out.println(resultString);
n.delete();
n1.delete();
tx.success();
}
finally
{
tx.finish();
}
}
private Node createAndIndexNode(final String property, final String name, Index<Node> nodeIndex, GraphDatabaseService db ) {
Node node = db.createNode();
node.setProperty(property , name);
nodeIndex.add(node, property, name);
return node;
}
public Node addPropertyAndIndexNode(String property, String name, Index<Node> nodeIndex, Node node)
{
node.setProperty( property, name );
nodeIndex.add( node, property, node.getProperty( property ) );
return node;
}
}
Any ideas / suggestions how to solve this?
Thank you!!
I think the actual name of your index is just person (as specified here: index.forNodes( "person" );), rather than personIndex.
Try:
start m=node:person(name= 'John') return m