Using Redis to store results from a query from neo4j - node.js

I have a query in neo4j with some aggregation functions, which takes around 10 seconds to retrieve the information. What I would like to do is store the query results into redis and from time to time the redis database would update the results from neo4j.
One record will be like:
{ entry: "123", model: "abc", reactants: [{specie: "abc#12", color: "black"}], .... }
I'm using node.js with express, thank you in advance for your attention
UPDATE: my query is quite extensive, I had to do that 'UNWIND' part to be able to search by the reactants (I wanted the products too but I didn't know how to do it). I don't know if it's possible to be optimized to at least 2 seconds but here it goes:
MATCH (rx:ModelReaction),
(rx)-[l:left_component]->(lc:MetaboliteSpecie),
(rx)-[r:right_component]->(rc:MetaboliteSpecie)
OPTIONAL MATCH (rx)-[:has_gpr]-(gpr:Root)
OPTIONAL MATCH (rx)-[:has_crossreference_to]-(cr)-[:has_ec_number]-(ec)
WITH rx,r,cr,ec,gpr,
COLLECT(DISTINCT {specie: l.cpdEntry, stoichiometry: l.stoichiometry}) as reacts
UNWIND reacts as rcts
WITH rx,r,cr,ec,gpr, rcts, reacts
WHERE rcts.specie =~ {searchText} OR rx.entry =~ {searchText} OR
rx.name =~ {searchText} OR (ec.entry IS NOT NULL AND
ec.entry =~ {searchText}) OR rx.geneRule =~ {searchText}
RETURN {entry: rx.entry,
reactants: reacts,
products:COLLECT(DISTINCT {specie: r.cpdEntry,
stoichiometry: r.stoichiometry}),
orientation: rx.orientation, name: rx.name, ecnumber: ec.entry,
gpr_rule: rx.geneRule, gpr_normalized: gpr.normalized_rule}
ORDER BY ' + reactionsTableMap[sortCol] + ' ' + order + ' SKIP {offset} LIMIT {number}'

The easiest is to store the result from Neo4j as a JSON string in redis, and set an expiry time on that key. Now when you need to retrieve the data, you check if the key is there, then redis serves as a cache, and if the key is not there, you ask Neo4j, store the result in redis and returns that to your Node.js program.
Pseudo code since I don't know Node.js specifics regarding Neo4J and Redis:
var result = redis.get("Record:123")
if (result == null) {
result = neo4j.query("...");
redis.setex("Record:123", toJson(result), 10); // set with expiry time
}
return result;
Redis will handle the expiry so you don't have to.
If you want to store them all, you can store them in a LIST or a ZSET (sorted set by record Id for example) and just call redis LRANGE/ZRANGE to retrieve a part of that list/set
Example with list:
var exist = redis.exist("Records"); // check if something stored in redis
if (!exist) {
var queryResult = neo4j.query("...); // get a list of results from neo4j
queryResult.foreach(result => redis.lpush("Records", toJson(result))); // add the results in the redis list
}
return redis.lrange("Records", 0, 50); // get the 50 first items
Now just iterate on that using the two parameters of redis.lrange by getting the ten items then the next ten.
You can also call redis EXPIRE to set an expiry time on the redis list.

Related

Cosmos DB .NET SDK V3 Query With Paging example needed

I'm struggling to find a code example from MS for the v3 SDK for queries with paging, they provide examples for V2 but that SDK is a completely different code base using the "CreateDocumentQuery" method.
I've tried searching through GitHub here: https://github.com/Azure/azure-cosmos-dotnet-v3/blob/master/Microsoft.Azure.Cosmos.Samples/Usage/Queries/Program.cs
I believe I'm looking for a method example using continuation tokens, with the assumption that if I cache the previously used continuation tokens in my web app then I can page backwards as well as forwards?
I'm also not quite understanding MS explanation in that MaxItemCount doesn't actually mean it will only try to return X items, but simply limits the No. of items in each search across each partition, confused!
Can anyone point me to the right place for a code example please? I also tried searching through https://learn.microsoft.com/en-us/azure/cosmos-db/sql-query-pagination but appears to lead us to the older SDK (V2 I believe)
UPDATE (following comments from Gaurav below)
public async Task<(List<T>, string)> QueryWithPagingAsync(string query, int pageSize, string continuationToken)
{
try
{
Container container = GetContainer();
List<T> entities = new(); // Create a local list of type <T> objects.
QueryDefinition queryDefinition = new QueryDefinition(query);
using FeedIterator<T> resultSetIterator = container.GetItemQueryIterator<T>(
query, // SQL Query passed to this method.
continuationToken, // Value is always null for the first run.
requestOptions: new QueryRequestOptions()
{
// Optional if we already know the partition key value.
// Not relevant here becuase we're passing <T> which could
// be any model class passed to the generic method.
//PartitionKey = new PartitionKey("MyParitionKeyValue"),
// This does not actually limit how many documents are returned if
// what we're querying resides across multiple partitions.
// If we set the value to 1, then control the number of times
// the loop below performs the ReadNextAsync, then we can control
// the number of items we return from this method. I'm not sure
// whether this is best way to go, it seems we'd be calling
// the API X no. times by the number of items to return?
MaxItemCount = 1
});
// Set var i to zero, we'll use this to control the number of iterations in
// the loop, then once i is equal to the pageSize then we exit the loop.
// This allows us to limit the number of documents to return (hope this is the best way to do it)
var i = 0;
while (resultSetIterator.HasMoreResults & i < pageSize)
{
FeedResponse<T> response = await resultSetIterator.ReadNextAsync();
entities.AddRange(response);
continuationToken = response.ContinuationToken;
i++; // Add 1 to var i in each iteration.
}
return (entities, continuationToken);
}
catch (CosmosException ex)
{
//Log.Error($"Entities was not retrieved successfully - error details: {ex.Message}");
if (ex.StatusCode == HttpStatusCode.NotFound)
{
return (null, null);
}
else { throw; }
}
}
The above method is my latest attempt, and whilst I'm able to use and return continuation tokens, the next challenge is how to control the number of items returned from Cosmos. In my environment, you may notice the above method is used in a repo with where we're passing in model classes from different calling methods, therefore hard coding the partition key is not practical and I'm struggling with configuring the number of items returned. The above method is in fact controlling the number of items I am returning to the calling method further up the chain, but I'm worried that my methodology is resulting in multiple calls to Cosmos i.e. if I set the page size to 1000 items, am I making an HTTP call to Cosmos 1000 times?
I was looking at a thread here https://stackoverflow.com/questions/54140814/maxitemcount-feed-options-property-in-cosmos-db-doesnt-work but not sure the answer in that thread is a solution, and given I'm using the V3 SDK, there does not seem to be the "PageSize" parameter available to use in the request options.
However I also found an official Cosmos code sample here: https://github.com/Azure/azure-cosmos-dotnet-v3/blob/master/Microsoft.Azure.Cosmos.Samples/Usage/Queries/Program.cs#L154-L186 (see example method "QueryItemsInPartitionAsStreams" line 171) and it looks like they have used a similar pattern i.e. setting the MaxItemCount variable to 1 and then controlling the no. of items returned in the loop before exiting. I guess I'd just like to understand better what, if any impact this might have on the RUs and API calls to Cosmos?
Please try the following code. It fetches all documents from a container with a maximum of 100 documents in a single request.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
namespace CosmosDbSQLAPISamples
{
class Program
{
private static string connectionString =
"AccountEndpoint=https://account-name.documents.azure.com:443/;AccountKey=account-key==;";
private static string databaseName = "database-name";
private static string containerName = "container-name";
static async Task Main(string[] args)
{
CosmosClient client = new CosmosClient(connectionString);
Container container = client.GetContainer(databaseName, containerName);
string query = "Select * From Root r";
string continuationToken = null;
int pageSize = 100;
do
{
var (entities, item2) = await GetDataPage(container, query, continuationToken, pageSize);
continuationToken = item2;
Console.WriteLine($"Total entities fetched: {entities.Count}; More entities available: {!string.IsNullOrWhiteSpace(continuationToken)}");
} while (continuationToken != null);
}
private static async Task<(List<dynamic>, string)> GetDataPage(Container container, string query, string continuationToken, int pageSize)
{
List<dynamic> entities = new(); // Create a local list of type <T> objects.
QueryDefinition queryDefinition = new QueryDefinition(query);
QueryRequestOptions requestOptions = new QueryRequestOptions()
{
MaxItemCount = pageSize
};
FeedIterator<dynamic> resultSetIterator = container.GetItemQueryIterator<dynamic>(query, continuationToken, requestOptions);
FeedResponse<dynamic> response = await resultSetIterator.ReadNextAsync();
entities.AddRange(response);
continuationToken = response.ContinuationToken;
return (entities, continuationToken);
}
}
}
UPDATE
I think I understand your concerns now. Essentially there are two things you would need to consider:
MaxItemCount - This is the maximum number of documents that will be returned by Cosmos DB in a single request. Please note that you can get anywhere from 0 to the value specified for this parameter. For example, if you specify 100 as MaxItemCount you can get anywhere from 0 to 100 documents in a single request.
FeedIterator - It keeps track of continuation token internally. Based on the response received, it sets HasMoreResults to true or false if a continuation token is found. Default value for HasMoreResults is true.
Now coming to your code, when you do something like:
while (resultSetIterator.HasMoreResults)
{
//some code here...
}
Because FeedIterator keeps track of the continuation token, this loop will return all the documents that match the query. If you notice, in my code I am not using this logic. I simply send the request once and then return the result.
I think setting MaxItemCount to 1 is a bad idea. If you want to fetch say 100 then you're making a minimum of 100 requests to your Cosmos DB account. If you have a hard need to get exactly 100 (or any fixed number) documents from your API, you can implement your own pagination logic. For example, please see code below. It fetches a total of 1000 documents with a maximum of 100 documents in a single request.
static async Task Main(string[] args)
{
CosmosClient client = new CosmosClient(connectionString);
Container container = client.GetContainer(databaseName, containerName);
string query = "Select * From Root r";
string continuationToken = null;
int pageSize = 100;
int maxDocumentsToFetch = 1000;
List<dynamic> documents = new List<dynamic>();
do
{
var numberOfDocumentsToFetch = Math.Min(pageSize, maxDocumentsToFetch);
var (entities, item2) = await GetDataPage(container, query, continuationToken, numberOfDocumentsToFetch);
continuationToken = item2;
Console.WriteLine($"Total entities fetched: {entities.Count}; More entities available: {!string.IsNullOrWhiteSpace(continuationToken)}");
maxDocumentsToFetch -= entities.Count;
documents.AddRange(entities);
} while (maxDocumentsToFetch > 0 && continuationToken != null);
}
The solution:
Summary:
From the concerns raised in my question and taking note from Gaurav Mantri's comments, if we are fetching the items from Cosmos in a loop then the MaxItemCount does not actually limit the total number of results returned but simply limits the number of results per request. If we continue to fetch more items in the loop then we end up with more results returned than what the user may want to retrieve.
In my case, the reason for paging is to present the items back to the web App using a razor list view, but we want to be able to set the maximum number of results returned per page.
The solution below is based on capturing information on the count of items in each iteration of the loop, therefore if we check the Count of the items returned on each iteration of the loop and if we have achieved less than or equal to the MaxItemCount value then we break from the loop with our set maximum number of items and the continuationToken that we can use on the next method run.
I have tested the method with continuation tokens and am able to affectively page backwards and forwards, but the key difference from the code example in my original question is that we're only calling Cosmos DB once to get the desired number of results back, as opposed to limiting the request to one item per run and having to run multiple requests.
public async Task<(List<T>, string)> QueryWithPagingAsync(string query, int pageSize, string continuationToken)
{
string unescapedContinuationToken = null;
if (!String.IsNullOrEmpty(continuationToken)) // Check if null before unescaping.
{
unescapedContinuationToken = Regex.Unescape(continuationToken); // Needed in my case...
}
try
{
Container container = GetContainer();
List<T> entities = new(); // Create a local list of type <T> objects.
QueryDefinition queryDefinition = new(query); // Create the query definition.
using FeedIterator<T> resultSetIterator = container.GetItemQueryIterator<T>(
query, // SQL Query passed to this method.
unescapedContinuationToken, // Value is always null for the first run.
requestOptions: new QueryRequestOptions()
{
// MaxItemCount does not actually limit how many documents are returned
// from Cosmos, if what we're querying resides across multiple partitions.
// However this parameter will control the max number of items
// returned on 'each request' to Cosmos.
// In the loop below, we check the Count of the items returned
// on each iteration of the loop and if we have achieved less than or
// equal to the MaxItemCount value then we break from the loop with
// our set maximum number of items and the continuationToken
// that we can use on the next method run.
// 'pageSize' is the max no. items we want to return for each page in our list view.
MaxItemCount = pageSize,
});
while (resultSetIterator.HasMoreResults)
{
FeedResponse<T> response = await resultSetIterator.ReadNextAsync();
entities.AddRange(response);
continuationToken = response.ContinuationToken;
// After the first iteration, we get the count of items returned.
// Now we'll either return the exact number of items that was set
// by the MaxItemCount, OR we may find there were less results than
// the MaxItemCount, but either way after the first run, we should
// have the number of items returned that we want, or at least
// the maximum number of items we want to return, so we break from the loop.
if (response.Count <= pageSize) { break; }
}
return (entities, continuationToken);
}
catch (CosmosException ex)
{
//Log.Error($"Entities was not retrieved successfully - error details: {ex.Message}");
if (ex.StatusCode == HttpStatusCode.NotFound)
{
return (null, null);
}
else { throw; }
}
}
In Code:
var sqlQueryText = $"SELECT * FROM c WHERE OFFSET {offset} LIMIT {limit}";
but this is more expensive (more RU/s) then using continuationToken.
When using Offset/Limit continuationToken will be used in background by Azure Cosmos SDK to get all the results.

Firestore security rules with set {merge}

In my Firestore data I have the following
ItemID
amountOfItemsToPurchase: 1
itemsLeft: 3
If a user wants to update the items to purchase for the same ItemID or create the document if the ItemID is not present, I use set(, {merge:true}, however in terms of the Firestore Security rules things get complicated.
I've written the following test:
const initialUserDoc = adminFirestore.collection("Users").doc(VALID_USER_ID).collection("Cart").doc(documentID);
await initialUserDoc.set({
"amountOfItemsToPurchase": 1,
"itemsLeft": 3
});
// Get the user's node and grab the example user
const userTestRef = db.collection("Users").doc(VALID_USER_ID).collection("Cart").doc(documentID);
await firebase.assertFails(userTestRef.set({
"amountOfItemsToPurchase": firebase.firestore.FieldValue.increment(1000),
}, {merge: true}));
This test results in the following: Error: Expected request to fail, but it succeeded
What I want for every scenario (update or create), to avoid that the amountOfItemsToPurchase exceeds the itemsLeft, for that I use the following:
request.resource.data.amountOfItemsToPurchase <= request.resource.data.itemsLeft => This will be in the allow create portion.
This rises the following:
Does the allow create or the allow update** will be called? and also why is not taking into account the itemsLeft variable
From your description and the comments, your security rule looks OK.
Let me write it for futher discussion:
match /items/{id} {
allow create, update:
if request.resource.data.amountOfItemsToPurchase < request.resource.data.itemsLeft;
}
First gotcha: use request.resource to be able to read the value of the field after the update.
Second gotcha: when using set, the FieldValue.increment sentinel does not increment but sets the value. You should use the update function to actually increment the value.
Third gotcha: FieldValue.increment with set function and {merge: true} does increment the value!
So in the end the rule works fine as is, I can confirm it works for me.

Data tracking in DocumentDB

I was trying to keep the history of data (at least one step back) of DocumentDB.
For example, if I have a property called Name in document with value "Pieter". Now I am changing that to "Sam", I have to maintain the history , it was "Pieter" previously.
As of now I am thinking of a pre-trigger. Any other solutions ?
Cosmos DB (formerly DocumentDB) now offers change tracking via Change Feed. With Change Feed, you can listen for changes on a particular collection, ordered by modification within a partition.
Change feed is accessible via:
Azure Functions
DocumentDB (SQL) SDK
Change Feed Processor Library
For example, here's a snippet from the Change Feed documentation, on reading from the Change Feed, for a given partition (full code example in the doc here):
IDocumentQuery<Document> query = client.CreateDocumentChangeFeedQuery(
collectionUri,
new ChangeFeedOptions
{
PartitionKeyRangeId = pkRange.Id,
StartFromBeginning = true,
RequestContinuation = continuation,
MaxItemCount = -1,
// Set reading time: only show change feed results modified since StartTime
StartTime = DateTime.Now - TimeSpan.FromSeconds(30)
});
while (query.HasMoreResults)
{
FeedResponse<dynamic> readChangesResponse = query.ExecuteNextAsync<dynamic>().Result;
foreach (dynamic changedDocument in readChangesResponse)
{
Console.WriteLine("document: {0}", changedDocument);
}
checkpoints[pkRange.Id] = readChangesResponse.ResponseContinuation;
}
If you're trying to make an audit log I'd suggest looking into Event Sourcing.Building your domain from events ensures a correct log. See https://msdn.microsoft.com/en-us/library/dn589792.aspx and http://www.martinfowler.com/eaaDev/EventSourcing.html

MongoDB Database Semaphores and Node.js Process.NextTick()

This may be a vary bad idea, or a possible solution that we have to a database concurrency problem.
We have a method that is called to do an update of a mongo record. We are seeing some concurrency problems - process A reads the record, process B reads the record, process A makes mods and saves the record, process makes B mods and saves the record. Because B reads after A, before A writes, it doesn't know about the changes A made, and we lose the data from A.
I'm wondering if we could not use a database semaphore, basically a field on the collection, that is a boolean. If we read the record at the start of the method, and the field is true, it's being edited. At that point, re-call the method using process.nexttick(), with the same data. Otherwise, set the semaphore, and carry on.
There would still be a bit of time between the read and the save, but it should be/could be faster than what we are doing now.
Be something like this. Any thoughts, anyone done anything like this? Will it even work?
function remove_source(service_id,session, next)
{
var User = Mongoose.model("User");
/* get the user, based on the session user id */
User.findById(session.me,function(err,user_info)
{
if (user_info.semaphore === true)
{
process.nextTick(remove_source(service_id,session,next));
}
else
{
user_info.semaphore = true;
user_info.save(function(err,user_new)
{
if (err) next(err,user_new);
else continue_on(null,user_new);
});
}
function continue_on(user_new)
{
etc.......
}
Edit: New Code:
The function now looks as follows. I'm doing individual updates to the arrays. This of course means that I now have the possibility, if the transaction fails between the first and second transactions, of having data out of sync. I'm thinking that I could simply resave the user object that I retrieved on entry into the function, overwriting my changes. I don't know if Mongoose/Mongo will not do the save if I have not changed that object, will have to try and see. Any more thoughts?
var User = Mongoose.model("User");
/* get the user, based on the session user id */
User.findById(session.me,function(err,user_info)
{
if (err)
{
next(err,user_info,null);
return;
}
if (!user_info || user_info.length === 0)
{
next(_e("ACCOUNT_NOT_FOUND"),"user_id: " + session.me);
return;
}
var source_service_info = _.where(user_info.credentials, {"source_service_id": service_id});
var source_service = source_service_info.source_service;
User.findByIdAndUpdate(session.me,{$pull: {"credentials": {"source_service_id": service_id}}},{},function(err,user_credential_removed)
{
if (err)
{
next(err,user_info,null);
return;
}
User.findByIdAndUpdate(session.me,{$pull: {"criteria": {"source_service": source_service}}},{},function(err,user_criteria_removed)
{
if (err)
{
next(err,user_info,null);
return;
}
else
{
next(null,user_criteria_removed);
}
});
});
});
};
The problem with your approach is that it just shortens the time during which the data could be read by a second process, it doesn't eliminate the problem.
The solution to this would be to set your semaphore in the same action as the read. I haven't used Mongoose, but in MongoDB you can use findAndModify to only return a User record if the semaphore is false, and if it is false, in one atomic operation, set the semaphore to true.
If you don't want to use findAndModify, you could first do an update that sets the semaphore true (or to some specific ID value so you know that it is YOUR semaphore) only if the semaphore is not set. Then, if that process succeeds, you could do the find (perhaps passing your semaphore ID as a criterion in the find). However, findAndModify, if it is available in Mongoose, would do that in one step.
A variation of that is described here: http://docs.mongodb.org/manual/tutorial/isolate-sequence-of-operations/ where you do a form of optimistic locking that checks that the old values are unchanged before changing them to the new values.
There is a variation on this that uses a separate table to simulate a two-phase commit: http://docs.mongodb.org/manual/tutorial/perform-two-phase-commits/
Edited: Upon interchange below, this seems to be a schema and updating issue. Question may become something like: I have some entries in an array, and the ordinal index to those entries relates to some other arrays as well. How do I perform deletes without having mismatches?
Three off the top possibilities occur, depending on frequency in the real world vs QA test scenarios.
Consider adding a deleted flag but keeping the records in the same order. If someone toggles, reuse the same record, but fix however you want.
Use an associative array (JS object) for each element (not a feature from relational world.) If you need an order, add an array that lists the keys in order. Both have syntax to update without touching anything other that what has changed, and will not overwrite changes to different fields.
Use an associative array where the keys are numbers. Actual deletion won't hurt retrieval.
stuff = {}
stuff[1] = {some:'details'}
stuff[2] = {some:'details2'}
Was
1) Are you making changes to the same field? Make that into an array, and push changes, and pop the latest to read the current value.
2) Are you changing different fields, but data is getting trounced? Then there is better syntax to use for the updating. you can update field by field.
$set: { 'fielda': 'valuea' }
won't lose edits on previous fields
3) change your schema
4) change the timing on the processes so they don't overlap. Or so they do so in smaller subsets, that you can manage to prevent from overlapping.
I'd like to know, just out of interest, what multiple processes are needed to make updates on the same record? I don't work with anything that looks like that.

Azure Storage Table Does not return whole partition

I found some situation on production when
CloudContext.TableData.Where( A => A.PartitionKey == "MYKEY").ToList();
where TableData is
public DataServiceQuery<T> TableData { get { return CreateQuery<T>( _TableName ); } }
does not return the whole partition (I have less than 1000 records there).
In my case it returns 367 records while in VS2010 Server Explorer or in Azure Storage Explorer I get 414 records (condition is the same).
Did anyone experience the same problem?
Also If I change the query and add RowKey into the condition - I get required record with no problem.
You have to better understand the Table Service. In the official documentation here there are listed other conditions which affect number of records returned. If you want to retrieve the whole partition you have to inspect the TableResult for Continuation Token and use provided continuation token to execute the same query over and over again, until all the results come.
You can use an approach similar to the following:
private IEnumerable<MyEntityType> GetAllEntities()
{
var result = this._tables.GetSegmentedEntities(100, null); // null is for continuation token
while (result.Results.Count > 0)
{
foreach (var ufs in result.Results)
{
yield return new MyEntityType(ufs.RowKey, ufs.WhateverOtherPropertyINeed);
}
if (result.ContinuationToken != null)
{
result = this._tables.GetSegmentedEntities(100, result.ContinuationToken);
}
else
{
break;
}
}
}
Where GetSegmentedEntities(100, result.ContinuationToken) is defined as:
public TableQuerySegment<MyEntityType> GetSegmentedEntities(int pageSize, TableContinuationToken token)
{
var partKey = "My_Desired_Partition_key_passed_via_Const_or_method_Param";
TableQuery<MyEntityType> query = new TableQuery<MyEntityType>()
.Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partKey));
query.TakeCount = pageSize;
return this.azureTableReference.ExecuteQuerySegmented<MyEntityType>(query, token);
}
You can use and modify this code for your case.
This is a known and documented behavior. The Table service API will either return 1000 entities or as much entities as possible within 5 seconds. If the query takes longer than 5 seconds to execute, it'll return a continuation token.
With the addition of rowkey you are making the query more specific and hence faster and as a result yo are getting all the entities.
See TimeOuts and Pagination on MSDN for details
If you are getting partial result sets then there will be two factors.
i) You are having more than 1000 records matching the filter
ii) Querying took more than 5 seconds.
iii) Query crosses partition boundary.
As you are having less than 1000 records the first factor wont be a issue.And as you are retrieving based on PartitionKey equality third one also wont cause any problem. You are facing this problem because of second factor.
Two handle this you need to work on continuation token. You can refer this link for more info.

Resources