Neo4jClient Cypher ResultSet Support

Sometimes when doing Cypher queries the result is only one column and not multiple columns, therefore it makes sense to have a method in the fluent API to let this be known, so we do not have to map the column to an object type.

So fluent support to deserialize common result sets where cypher returns a result with only 1 column with the help of Tatham Oddie is completed.

So in the Neo4jClient you can do this, when the result from Cypher is one column via REST:

var result = agencySource
                        .StartCypher("a1")
                        .AddStartPoint("a2", agency.Reference)
                        .Match("p = allShortestPaths( a1-[*..20]-a2 )")
                        .Return<PathsResult>("p")
                        .ResultSet;

So, if you need cypher results with only one column then use .ResultSet instead of .Results, thus no need for expression tree column matches to assist the deserializer with multiple column names.

Here is a sample rest response with 1 column result that is suited perfectly for ResultSet.

{
  "data" : [ [ {
    "start" : "http://localhost:20001/db/data/node/215",
    "nodes" : [ "http://localhost:20001/db/data/node/215", "http://localhost:20001/db/data/node/0", "http://localhost:20001/db/data/node/219" ],
    "length" : 2,
    "relationships" : [ "http://localhost:20001/db/data/relationship/247", "http://localhost:20001/db/data/relationship/257" ],
    "end" : "http://localhost:20001/db/data/node/219"
  } ], [ {
    "start" : "http://localhost:20001/db/data/node/215",
    "nodes" : [ "http://localhost:20001/db/data/node/215", "http://localhost:20001/db/data/node/1", "http://localhost:20001/db/data/node/219" ],
    "length" : 2,
    "relationships" : [ "http://localhost:20001/db/data/relationship/248", "http://localhost:20001/db/data/relationship/258" ],
    "end" : "http://localhost:20001/db/data/node/219"
  } ] ],
  "columns" : [ "p" ]
}

If you wondering what the hell is agencySource, it is just node references, that I got using gremlin, which can spin off cypher queries, cool is it not?

var agencies = graphClient
                .RootNode
                .Out<Agency>(Hosts.TypeKey)
                .ToList();

This just enumerate through the list of nodes to run your cypher queries off the node directly! Have these imports declarations:

using Neo4jClient.ApiModels.Cypher;
using Neo4jClient.Gremlin;
using Neo4jClient.Cypher;

Summary

Use .ResultSet for single column result sets and use .Results when dealing with multiple column results.

Gremlin vs Cypher Initial Thoughts @Neo4j

Hi,

The Neo4jClient now supports Cypher as a query language with Neo4j. However I noticed the following:

  • Simple graph traversals are much more efficient when using Gremlin
  • Queries in Gremlin are 30-50% faster for simple traversals
  • Cypher is ideal for complex traversals where back tracking is required
  • Cypher is our choice of query language for reporting
  • Gremlin is our choice of query language for simple traversals where projections are not required
  • Cypher has intrinsic table projection model, where Gremlins table projection model relies on AS steps which can be cumbersome when backtracking e.g. Back(), As() and _CopySplit, where cypher is just comma separated matches
  • Cypher is much better suited for outer joins than Gremlin, to achieve similar results in gremlin requires parallel querying with CopySplit, where as in Cypher using the Match clause with optional relationships
  • Gremlin is ideal when you need to retrieve very simple data structures
  • Table projection in gremlin can be very powerful, however outer joins can be very verbose

So in a nutshell, we like to use Cypher when we need tabular data back from Neo4j and is especially useful in outer joins.

Here are two queries that return the exact same data from Neo4j, one in Cypher and one in Gremlin.

Cypher Report Query

var resultSet = graphClient.RootNode
                .StartCypher("root")
                .Match(@"root-[:HOSTS]->(agency)
                       <-[:USER_BELONGS_TO]-(user)-[:USER_LINKED_TO_PROGRAM]
                       ->(program)
                       <-[:HAS_PROGRAM]-(centre),
                       (program)<-[:HAS_SUGGESTED_PROGRAM]-(referralDecisionsSection)
                       <-[:REFERRAL_HAS_DECISIONS_SECTION]-(referral)-[:CREATED_BY]
                       ->(createdByUser), (referral)-[:REFERRAL_HAS_WHO_SECTION]
                       ->(whoSection)-[:HAS_PARTICIPANT]->(participant)")
                .Where<Agency>(agency => agency.Key == userIdentifier.AgencyKey)
                .And()
                .Where<User>(user => user.Username == userIdentifier.Username)
                .And()
                .Where<Referral>(referral => referral.Completed == false)
                .Return((user, program, centre, createdByUser, referral, whoSection, participant) => 
                new ReferralByGroup
                {
                    UserFamilyName = createdByUser.As<User>().FamilyName,
                    UserGivenName = createdByUser.As<User>().GivenName,
                    Program = program.As<Program>().Name,
                    Centre = centre.As<Centre>().Name,
                    ReferralId = referral.As<Referral>().UniqueId,
                    ReferralDate = whoSection.As<ReferralWhoSection>().ReferralDate,
                    ParticipantName = participant.As<ReferralParticipant>().Name,
                    ParticipantDisplayOrder = participant.As<ReferralParticipant>().DisplayOrder,
                })
                .Results
                .ToArray();

Gremlin Report Query using Table Projections

            var resultSet = graphClient
                .RootNode
                .Out<Agency>(Hosts.TypeKey, a => a.Key == userIdentifier.AgencyKey)
                .In<User>(UserBelongsTo.TypeKey, u => u.Username == userIdentifier.Username)
                .Out<Program>(UserLinkedToProgram.TypeKey)
                .As("Program")
                .In<Centre>(HasProgram.TypeKey)
                .As("Centre")
                .BackV<Program>("Program")
                .In<ReferralDecisionsSection>(HasSuggestedProgram.TypeKey)
                .In<Referral>(ReferralHasDecisionsSection.TypeKey, r => r.Completed == false)
                .As("ReferralId")
                .Out<User>(CreatedBy.TypeKey)
                .As("UserGivenName")
                .As("UserFamilyName")
                .BackV<Referral>("ReferralId")
                .Out<ReferralWhoSection>(ReferralHasWhoSection.TypeKey)
                .As("ReferralDate")
                .Out<ReferralParticipant>(HasParticipant.TypeKey)
                .As("ParticipantDisplayOrder")
                .As("ParticipantName")
                .Table
                <ReferralByGroup, Program, Centre, Referral, User, User, ReferralWhoSection, ReferralParticipant,
                    ReferralParticipant>(
                        program => program.Name,
                        centre => centre.Name,
                        referral => referral.UniqueId,
                        user => user.FamilyName,
                        user => user.GivenName,
                        who => who.ReferralDate,
                        participant => participant.Name,
                        participant => participant.DisplayOrder
                )
                .ToArray();

Below is the converted parameterised script sent for cypher and gremlin respectively for those not familiar with the Neo4jClient.

Cypher

START root=node({p8})
MATCH root-[:HOSTS]->(agency)
                       <-[:USER_BELONGS_TO]-(user)-[:USER_LINKED_TO_PROGRAM]->(program)
                       <-[:HAS_PROGRAM]-(centre),
                       (program)<-[:HAS_SUGGESTED_PROGRAM]-(referralDecisionsSection)
                       <-[:REFERRAL_HAS_DECISIONS_SECTION]-(referral)-[:CREATED_BY]
                       ->(createdByUser), (referral)-[:REFERRAL_HAS_WHO_SECTION]
                       ->(whoSection)-[:HAS_PARTICIPANT]
                       ->(participant)
WHERE (agency.Key? = {p0}) AND (user.Username? = {p1}) AND (referral.Completed? = {p2})
RETURN createdByUser.FamilyName? AS UserFamilyName, createdByUser.GivenName? AS UserGivenName, program.Name? AS Program, centre.Name? AS Centre, referral.UniqueId? AS ReferralId, whoSection.ReferralDate? AS ReferralDate, participant.Name? AS ParticipantName, participant.DisplayOrder? AS ParticipantDisplayOrder

Gremlin

g.v(p0)
.out(p1).filter{ it[p2].equalsIgnoreCase(p3) }
.in(p4).filter{ it[p5].equalsIgnoreCase(p6) }
.out(p7).as(p8).in(p9).as(p10).back(p11)
.in(p12).in(p13).filter{ it[p14] == p15 }.as(p16)
.out(p17).as(p18).as(p19).back(p20)
.out(p21).as(p22).out(p23).as(p24).as(p25)
.table(new Table()){it[p26]}{it[p27]}{it[p28]}{it[p29]}{it[p30]}{it[p31]}{it[p32]}{it[p33]}
.cap

I have included below the non-paramerterised cypher and gremlin query respectively.

Cypher

START root=node(0)
MATCH root-[:HOSTS]->(agency)<-[:USER_BELONGS_TO]-(user)-[:USER_LINKED_TO_PROGRAM]
->(program)
<-[:HAS_PROGRAM]-(centre),(program)
<-[:HAS_SUGGESTED_PROGRAM]-(referralDecisionsSection)
<-[:REFERRAL_HAS_DECISIONS_SECTION]-(referral)-[:CREATED_BY]->(createdByUser), (referral)-[:REFERRAL_HAS_WHO_SECTION]
->(whoSection)-[:HAS_PARTICIPANT]
->(participant)   WHERE (agency.Key? = romikoagency) AND (user.Username? = romiko.derbynew) AND (referral.Completed? = false)   
RETURN createdByUser.FamilyName? AS UserFamilyName, createdByUser.GivenName? AS UserGivenName, program.Name? AS Program, centre.Name? AS Centre, referral.UniqueId? AS ReferralId, whoSection.ReferralDate? AS ReferralDate, participant.Name? AS ParticipantName, participant.DisplayOrder? AS ParticipantDisplayOrder

Gremlin

g.v('0').out('HOSTS').filter{ it['Key'].equalsIgnoreCase('romikoagency') }
.in('USER_BELONGS_TO').filter{ it['Username'].equalsIgnoreCase('romiko.derbynew') }
.out('USER_LINKED_TO_PROGRAM').as('Program')
.in('HAS_PROGRAM').as('Centre').back('Program')
.in('HAS_SUGGESTED_PROGRAM')
.in('REFERRAL_HAS_DECISIONS_SECTION').filter{ it['Completed'] == false }.as('ReferralId')
.out('CREATED_BY').as('UserGivenName').as('UserFamilyName').back('ReferralId')
.out('REFERRAL_HAS_WHO_SECTION').as('ReferralDate')
.out('HAS_PARTICIPANT').as('ParticipantDisplayOrder').as('ParticipantName')
.table(new Table()){it['Name']}{it['Name']}{it['UniqueId']}{it['FamilyName']}{it['GivenName']}{it['ReferralDate']}{it['Name']}{it['DisplayOrder']}.cap

#Neo4j Neo4jClient Cypher support added

Cypher support has now been added to the Neo4jClient

The Neo4jClient also has a built in custom Cypher REST result deserializer, so you do not need to worry about all the serialization/deserialization logic, courtesy to Tatham Oddie.

Below are some sample queries to get you going.

You can look at the Source Code for other code samples in the Test project.

Simple Query from API
            return graphClient.RootNode
                .StartCypher("root")
                .Match("root-[:BELONGS]->(user)")
                .Return<SimpleResultDto>("user")
                .Results
                .OrderBy(u => u.Username);
Simple Query
        [Test]
        public void WhereBooleanOperations()
        {
            // http://docs.neo4j.org/chunked/1.6/query-where.html#where-boolean-operations
            // START n=node(3, 1)
            // WHERE (n.age < 30 and n.name = "Tobias") or not(n.name = "Tobias")
            // RETURN n

            var client = Substitute.For<IGraphClient>();
            var query = new CypherFluentQuery(client)
                .Start("n", (NodeReference)3, (NodeReference)1)
                .Where<FooNode>(n => (n.Age < 30 && n.Name == "Tobias") || n.Name != "Tobias")
                .Return<object>("n")
                .Query;

            Assert.AreEqual("START n=node({p0}, {p1})\r\nWHERE (((n.Age < {p2}) AND (n.Name = {p3})) OR (n.Name != {p4}))\r\nRETURN n".Replace("'","\""), query.QueryText);
            Assert.AreEqual(3, query.QueryParameters["p0"]);
            Assert.AreEqual(1, query.QueryParameters["p1"]);
            Assert.AreEqual(30, query.QueryParameters["p2"]);
            Assert.AreEqual("Tobias", query.QueryParameters["p3"]);
            Assert.AreEqual("Tobias", query.QueryParameters["p4"]);
        }
Simple Query Column Aliases
 [Test]
        public void ReturnColumnAlias()
        {
            // http://docs.neo4j.org/chunked/1.6/query-return.html#return-column-alias
            // START a=node(1)
            // RETURN a.Age AS SomethingTotallyDifferent

            var client = Substitute.For<IGraphClient>();
            var query = new CypherFluentQuery(client)
                .Start("a", (NodeReference)1)
                .Return(a => new ReturnPropertyQueryResult
                {
                    SomethingTotallyDifferent = a.As<FooNode>().Age
                })
                .Query;

            Assert.AreEqual("START a=node({p0})\r\nRETURN a.Age AS SomethingTotallyDifferent", query.QueryText);
            Assert.AreEqual(1, query.QueryParameters["p0"]);
        }

Below is deserialization code, note this is Internal to the client and you would never explicitly call it directly, you would always use one of the Return overloads instead. I have put it here for those coders interested in deserialzing Cypher Rest results from Neo4j using expressions.

Deserialization Test Sample
        [Test]
        public void ShouldDeserializeTableStructureWithRelationships()
        {
            // Arrange
            const string queryText = @"
                START x = node({p0})
                MATCH x-[r]->n
                RETURN x AS Fooness, type(r) AS RelationshipType, n.Name? AS Name, n.UniqueId? AS UniqueId
                LIMIT 3";
            var query = new CypherQuery(
                queryText,
                new Dictionary<string, object>
                {
                    {"p0", 123}
                });

            var httpFactory = MockHttpFactory.Generate("http://foo/db/data", new Dictionary<RestRequest, HttpResponse>
            {
                {
                    new RestRequest
                    {
                        Resource = "/",
                        Method = Method.GET
                    },
                    new HttpResponse
                    {
                        StatusCode = HttpStatusCode.OK,
                        ContentType = "application/json",
                        Content =
                            @"{
                                'cypher' : 'http://foo/db/data/cypher',
                                'batch' : 'http://foo/db/data/batch',
                                'node' : 'http://foo/db/data/node',
                                'node_index' : 'http://foo/db/data/index/node',
                                'relationship_index' : 'http://foo/db/data/index/relationship',
                                'reference_node' : 'http://foo/db/data/node/0',
                                'extensions_info' : 'http://foo/db/data/ext',
                                'extensions' : {
                                'GremlinPlugin' : {
                                    'execute_script' : 'http://foo/db/data/ext/GremlinPlugin/graphdb/execute_script'
                                }
                                }
                            }".Replace('\'', '"')
                    }
                },
                {
                    new RestRequest
                    {
                        Resource = "/cypher",
                        Method = Method.POST,
                        RequestFormat = DataFormat.Json
                    }.AddBody(new CypherApiQuery(query)),
                    new HttpResponse
                    {
                        StatusCode = HttpStatusCode.OK,
                        ContentType = "application/json",
                        Content =
                            @"{
                                'data' : [ [ {
                                'start' : 'http://foo/db/data/node/0',
                                'data' : {
                                    'Bar' : 'bar',
                                    'Baz' : 'baz'
                                },
                                'property' : 'http://foo/db/data/relationship/0/properties/{key}',
                                'self' : 'http://foo/db/data/relationship/0',
                                'properties' : 'http://foo/db/data/relationship/0/properties',
                                'type' : 'HAS_REFERENCE_DATA',
                                'extensions' : {
                                },
                                'end' : 'http://foo/db/data/node/1'
                                }, 'HOSTS', 'foo', 44321 ], [ {
                                'start' : 'http://foo/db/data/node/1',
                                'data' : {
                                    'Bar' : 'bar',
                                    'Baz' : 'baz'
                                },
                                'property' : 'http://foo/db/data/relationship/1/properties/{key}',
                                'self' : 'http://foo/db/data/relationship/1',
                                'properties' : 'http://foo/db/data/relationship/1/properties',
                                'type' : 'HAS_REFERENCE_DATA',
                                'extensions' : {
                                },
                                'end' : 'http://foo/db/data/node/1'
                                }, 'LIKES', 'bar', 44311 ], [ {
                                'start' : 'http://foo/db/data/node/2',
                                'data' : {
                                    'Bar' : 'bar',
                                    'Baz' : 'baz'
                                },
                                'property' : 'http://foo/db/data/relationship/2/properties/{key}',
                                'self' : 'http://foo/db/data/relationship/2',
                                'properties' : 'http://foo/db/data/relationship/2/properties',
                                'type' : 'HAS_REFERENCE_DATA',
                                'extensions' : {
                                },
                                'end' : 'http://foo/db/data/node/1'
                                }, 'HOSTS', 'baz', 42586 ] ],
                                'columns' : [ 'Fooness', 'RelationshipType', 'Name', 'UniqueId' ]
                            }".Replace('\'', '"')
                    }
                }
            });
            var graphClient = new GraphClient(new Uri("http://foo/db/data"), httpFactory);
            graphClient.Connect();

            //Act
            var results = graphClient.ExecuteGetCypherResults<ResultWithRelationshipDto>(query);

            //Assert
            Assert.IsInstanceOf<IEnumerable<ResultWithRelationshipDto>>(results);

            var resultsArray = results.ToArray();
            Assert.AreEqual(3, resultsArray.Count());

            var firstResult = resultsArray[0];
            Assert.AreEqual(0, firstResult.Fooness.Reference.Id);
            Assert.AreEqual("bar", firstResult.Fooness.Data.Bar);
            Assert.AreEqual("baz", firstResult.Fooness.Data.Baz);
            Assert.AreEqual("HOSTS", firstResult.RelationshipType);
            Assert.AreEqual("foo", firstResult.Name);
            Assert.AreEqual(44321, firstResult.UniqueId);

            var secondResult = resultsArray[1];
            Assert.AreEqual(1, secondResult.Fooness.Reference.Id);
            Assert.AreEqual("bar", secondResult.Fooness.Data.Bar);
            Assert.AreEqual("baz", secondResult.Fooness.Data.Baz);
            Assert.AreEqual("LIKES", secondResult.RelationshipType);
            Assert.AreEqual("bar", secondResult.Name);
            Assert.AreEqual(44311, secondResult.UniqueId);

            var thirdResult = resultsArray[2];
            Assert.AreEqual(2, thirdResult.Fooness.Reference.Id);
            Assert.AreEqual("bar", thirdResult.Fooness.Data.Bar);
            Assert.AreEqual("baz", thirdResult.Fooness.Data.Baz);
            Assert.AreEqual("HOSTS", thirdResult.RelationshipType);
            Assert.AreEqual("baz", thirdResult.Name);
            Assert.AreEqual(42586, thirdResult.UniqueId);
        }
    }
Conclusion

The Neo4jClient now supports both Gremlin and Cypher query language in one logical graphClient, this should prove to be sufficient for all graph client query needs and CRUD operations, we now get the best of both worlds. You have intrinsic Neo4j CRUD + Gremlin + Cypher.

We find a balance where Gremlin is used for simple Graph Traversals and Cypher is used as our reporting tool.

#Neo4j Gremlin queries with CopySplit/table leveraging Neo4jClient

Hi,

I would like to share gremlin querying using the .Net Neo4jClient.

Consider the following graph

image

The object is to produce a table of results that shows

  • ReferralDate (ReferralDecisionSection Node)
  • ReferralId (Referral Node)
  • FamilyName (User Node)
  • GivenName (User Node)
    The trick is we want to get all referrals but we also what referrals that do not have a who section, so the ReferralDate will be NULL. We also want to get referrals that are indirectly linked to a program (via a decision) but we also want the ones that are not indirectly linked to a program
ReferralDate ReferralId FamilyName GivenName
13 Jan 2012 1 Derbynew Romiko
NULL 2 Derbynew Romiko

So, what we doing is essentially left/right joins on ReferralNode, ReferralDecisionNode and Program.

Lets see how we can do this in .Net Neo4jClient

 return graphClient
                .RootNode
                .CopySplitV<Referral>((
                    new IdentityPipe()
                        .Out(Hosts.TypeKey, a => a.Key == userIdentifier.AgencyKey)
                        .In(UserBelongsTo.TypeKey, u => u.Username == userIdentifier.Username)
                        .Out(UserLinkedToProgram.TypeKey, p => p.Name == "Foundation")
                        .In(HasSuggestedProgram.TypeKey)
                        .In(ReferralHasDecisionsSection.TypeKey, r => r.Completed == false)
                        .AggregateV("ReferralWithProgramFoundation"),
                    new IdentityPipe()
                        .Out(Hosts.TypeKey, a => a.Key == userIdentifier.AgencyKey)
                        .In(ReferralBelongsTo.TypeKey, r => r.Completed == false)
                )
                .FairMerge()
                .ExceptV("ReferralWithProgramFoundation")
                .GremlinDistinct()
                .Out(ReferralHasWhoSection.TypeKey)
                .As("ReferralDate")
                .In(ReferralHasWhoSection.TypeKey)
                .As("ReferralId")
                .Out(CreatedBy.TypeKey, u => u.Username == userIdentifier.Username)
                .As("UserGivenName")
                .As("UserFamilyName")
                .Table(
                    who => who.ReferralDate,
                    referral => referral.UniqueId,
                    user => user.FamilyName,
                    user => user.GivenName
                );

Notice the following

  • CopySplit uses a concept of an identity pipe as a continuation of the previous output
  • CopySplit will execute the two queries in parallel
  • We are getting all the referrals in the system and then we are getting all the referrals in the system that have a program “Foundation”
  • We then store the referrals that have a program (“Foundation”) in a aggregate (variable)
  • We merge the parallel query results together with a FaireMerge
  • We exclude referrals that in a a Program called “Foundation”  with an Except
  • We then deduplicate results with GremlinDistinct
  • We then use AS to mark areas we need for table projections

Note: Using the AS clause within a CopySplit pipe in conjunction with table projections will produce undesired results, I am not sure if Gremlin supports such operations, if you know, please contact me.

Visit Marko Rodriguez for in depth discussions on Gremlin.

Heroku Neo4j, App Harbor MVC4, Neo4jClient & Ruby Proxy

Overview

We going to outlay the process to deploy a 4 layers architecture using the above technologies.

architecture

Prerequisites

Sinatra and Rest-Client

Sinatra and the Rest-Client libraries are used in the Ruby Gem to proxy all Http requests to Neo4j.

Install Process

Install the Heroku client, run this from the heroku\Ruby\bin folder e.g. D:\Program Files (x86)\Heroku\ruby\bin

gem install heroku

We then need to use the Cedar stack (http://devcenter.heroku.com/articles/cedar) which supports ruby in Heroku,  so run:

heroku create –stack cedar

(Ensure git.exe is in your environment variables)

image

Once this is completed we then deploy our code to our Heroku git repository. You can see your git heroku repository in the app details for the app you created in Heroku:

image

We will use a public key that we created when first setting up Heroku, check blog post

https://romikoderbynew.com/2012/01/25/getting-started-with-heroku-toolbelt-on-windows-2/

if you changed your public key, then add it to Heroku

heroku keys:add

Ok, now we push the Gem files to our Heroku git repository. Your repsotory will be different to mine!

git push git@heroku.com:neo4jorbust.git master

image

Lets check the state of the application

E:\Projects\RubyRestProxy>heroku ps –app neo4jorbust
Process State Command
——- ———– ————————————
web.1 idle for 9h thin -p $PORT -e $RACK_ENV -R $HER..

Test HttpClient –> Ruby –> Neo4j Communication

Nice, now we got this GEM file, which is our Ruby http proxy code for gremlin queries. Lets test this baby out with fiddler and run  gremlin query to the web app, which will then call neo4j!

Lets browse the Ruby Application on the Heroku server, so we can find out what URL we need to use to send Gremlin queries to Neo4j.

image

Beautiful, all we need to do now is send an HTTP POST to the added, you can use curl, but here is fiddler.

image

And the result, music to our eyes

image

ASP.NET MVC 4 APP with Neo4jClient

This application is hosted at

http://frictionfree.org

Source Code:

https://bitbucket.org/romiko/frictionfree/src

Now we going to fire up a .Net application that will use the Neo4jClient Rest Api to communicate with neo4j in Heroku. You can find out more about Neo4jClient here http://hg.readify.net/neo4jclient/wiki/Home

Clone the git repository to get the source code for our sample Neo4jClient .Net application. This is a MVC4 application and runs on the .Net Framework 4 runtime, so please ensure you have this installed on your windows machine.

About frictionfree.org

The application is used to find other people with similar interests to you and the opportunity to get their details and contact them to perhaps share your love of flying, surfing or whatever it is you like to do!

Bitbucket and AppHarbor integration

If you have your own ASP.NET application, you can store it in bitbucket and then configure bitbucket to auto deploy to AppHarbor when pushing back the repostory.

In the admin settings for your repository just add your appharbor token.

image

Neo4jClient gremlin Query syntax

Check the source code of the MVC application in the Logic library to see how to run queries to Neo4j. Here is sample code to get administrators of the system

private void CreateAdminAccountIfNoneExist() { var anyAdmins = graphClient .RootNode .In(Administers.TypeKey) .Any(); if (!anyAdmins) { provisionNewUserService.CreateAdmin(); } } 

Getting Started with Heroku ToolBelt/Neo4j on Windows

Hi Guys,

To get started with Heroku, you need a public key. If you do not have one in the

C:\Users\<YourUsername>\.ssh folder, then you get this error

D:/Program Files (x86)/Heroku/lib/heroku/auth.rb:195:in `read’: No such file or
directory – C:/Users/Romiko/.ssh/id_rsa.pub (Errno::ENOENT)
        from D:/Program Files (x86)/Heroku/lib/heroku/auth.rb:195:in `associate_key’

looking at the source code, line 195 is ssh key generation. So what we need to do is use a utility like GitBash to do this for us:

image

Instructions

TO get up and running with the Heroku Toolbelt on windows ensure you read:

http://devcenter.heroku.com/articles/quickstart

Once you have read that and installed Heroku toolbelt

http://assets.heroku.com/heroku-toolbelt/heroku-toolbelt.exe

then install Git (Or use the Git bash if already installed)

http://msysgit.googlecode.com/files/Git-1.7.8-preview20111206.exe

Now, what you do is follow all the instructions here, you might as well ensure you have a GitHub account, its nice to put your code in a repository if developing:

http://help.github.com/win-set-up-git/

You should then have these files (you might not have known hosts), if you did not connect into git

image

The Heroku batch file will look for the id_rsa.pub file and use it to connect to the cloud servers.

Once this is done, you can then go back to the heroku.cmd execution and login

D:\Program Files (x86)\Heroku>heroku login
Enter your Heroku credentials.
Email: ROMIKOV@GMAIL.COM
Password:
Found existing public key: C:/Users/Romiko/.ssh/id_rsa.pub
Uploading ssh public key C:/Users/Romiko/.ssh/id_rsa.pub

That is it, so hope this gets you started on Heroku Smile

Neo4j

Once you ran the above command, then create an application

Create Heroku Applicaton in the Cloud

D:\Program Files (x86)\Heroku>heroku apps:create Neo4jOrBust
Creating neo4jorbust… done, stack is bamboo-mri-1.9.2
http://neo4jorbust.heroku.com/ | git@heroku.com:neo4jorbust.git

Notice the appname is lowercase always!

Install Neo4j Addon

For this to work, you must verify the account and you will need a credit card, it will NOT be billed. Don’t sue me if it does!

Then for Neo4j, just install the addon like so

D:\Program Files (x86)\Heroku>heroku addons:add neo4j –app neo4jorbust
—–> Adding neo4j to neo4jorbust… failed
!    Please verify your account to install this add-on
!    For more information, see http://devcenter.heroku.com/categories/billing
!    Confirm now at https://heroku.com/confirm

Go verify your account and then come back and run the command again

https://api.heroku.com/myapps

image

D:\Program Files (x86)\Heroku>heroku addons:add neo4j –app neo4jorbust
—–> Adding neo4j to neo4jorbust… done, (free)

image

Thats it!

You can then go check your addons on the web site at heroku under resources. Click databases and scroll to bottom.

 

image

Notice neo4jTest and it is free Smile Lets enable it

image

 

Notice, the urls for the webadmin site and rest url, you all set to work with Neo4j now Smile

Thats it, have fun!

Slow HttpWebRequest.GetResponse()

In the development of the Neo4jClient, we noticed that all DB queries to Neo4j were taking roughly 150ms longer than on my local development machine. after using Fiddler and ApacheBench, it was clear the performance issue was inside the .Net Code.

What we use in Neo4jClient (http://hg.readify.net/neo4jclient/wiki/Home) is the RestSharp open source library. Fiddler proved to be very unreliable to produce consistent results due to it caching and reuse of connections, even when disabling them, also, when requests went via fiddler, the response time was quick, so queries taking over 150ms without fiddler were taking 10-15ms within fiddler.

Profiling

The solution was to build a custom .Net tool to send Http POSTS, and using the JetBrains DotTrace utility, we were able to see an issue where, if we FIRST bypassed RestSharp and just used HttpWebRequest, the response time was quick, as soon as we used RestSharp it went slow, and even after using it, going back to HttpWebeRequest was then slow.

Somewhere, a Static Class was causing an issue that RestSharp uses.

image

Notice the 150ms overhead in the above screenshot

Here is a profile where restsharp is bypassed

image

In the above profile, the http response time is fast.

ServicePointManager

RestSharp sets the ServicePointManager, which is a static class, and what we found was that this algorithm was causing 150ms delays in our Neo4j DB calls.

ServicePointManager.UseNagleAlgorithm

I noticed in wire shark the difference between a fast stream and a slow one, it affected the number of packets which pointed to either

ServicePointManager.Expect100Continue

or

ServicePointManager.UseNagleAlgorithm

we found changing Expect100Continue not to cause any changes, however setting UseNagleAlgorithm to false, solved the issue, and now all DB calls to Neo4j are 150ms quicker!

 

(HttpWebRequest) – SLOW – After a RestSharp call (Poisoned system)

FAST TCP STREAM (HTTPWebRequest)

This is due to UseNagleAlgorithm being off

POST /db/data/ext/GremlinPlugin/graphdb/execute_script HTTP/1.1

Content-Type: application/json

Host: 10.25.234.67:20001

Content-Length: 65

{"script":"g.v(793).in(‘USER_BELONGS_TO’).drop(0).take(100)._()"}HTTP/1.1 200 OK

Content-Length: 5316

Content-Encoding: UTF-8

Content-Type: application/json

Access-Control-Allow-Origin: *

Server: Jetty(6.1.25)

POST /db/data/ext/GremlinPlugin/graphdb/execute_script HTTP/1.1

Content-Type: application/json

Host: 10.25.234.67:20001

Content-Length: 65

Expect: 100-continue

HTTP/1.1 100 Continue

{"script":"g.v(793).in(‘USER_BELONGS_TO’).drop(0).take(100)._()"}HTTP/1.1 200 OK

Content-Length: 5316

Content-Encoding: UTF-8

Content-Type: application/json

Access-Control-Allow-Origin: *

Server: Jetty(6.1.25)

Before Optimisation in Azure

image

After Optimisation in Azure

image

Conclusion

If you doing allot of small HTTP POSTS, it might be a good idea to turn off the Nagle Algorithm.

http://www.winsocketdotnetworkprogramming.com/xmlwebservicesaspnetworkprogramming11e.html

When using the Neo4j REST API(http://docs.neo4j.org/chunked/snapshot/rest-api.html), be aware of the response times, and if you notice response times greater than 150ms for all DB calls, then perhaps you have the same issue with the ServicePointManager confguration.

Nagle Algorithm

The Nagle algorithm increases network efficiency by decreasing the number of packets sent across the network. It accomplishes this by instituting a delay on the client of up to 200 milliseconds when small amounts of data are written to the network. The delay is a wait period for additional data that might be written. New data is added to the same packet. The practice of compiling data is not always ideal for Web services, however, because a POST request often contains a small amount of data in the HTTP headers without the body (because of 100 Continue, as described earlier). The Nagle algorithm can be turned off for Web services calls via the System.Net.ServicePointManager.UseNagleAlgorithm property. Yet disabling the Nagle algorithm might not provide the best performance either, you’ll need to experiment with disabling the algorithm to fine- tune your application. Whether you should disable the algorithm in this case depends on several factors, including the size of the Web services call, the latency of the network, and whether HTTP authentication is involved.

Neo4j application performance profiling management with New Relic

Hi,

We currently need a way to have performance profiling management solution in place for Neo4j running in Windows Azure cloud. The benefits of course is on going data analysis and performance statistics, not to mention assisting in debugging issues with performance. The best part of it all, is the agent that collects the data just runs as part of the JVM and the data is automatically uploaded to the website where you can view it online.

New Relic Account

The first thing you will need to do is create a new relic account at:

http://newrelic.com/

Once, this is done, you can then download the java agent which will contain two files:

newrelic.jar

newrelic.yml

The yml file contains the license key and the application name to display on the new relic performance dashboard website. When you use New Relic it has a trial option, so it is easy to test out.

Infrastructure

What we do is store these zip files in blob storage and when the worker role is bootstrapping, it will then download the zip file, and then automatically edit the neo4j config files before starting up neo4j.

image

Neo4j configuration

It is extremely simple to configure the relic agent to run and profile neo4j, all you need to do is edit the neo4j-wrapper.conf file and add this line of code.

wrapper.java.additional.2=-javaagent:..\newrelic.jar

We use a relative path, as we store the newrelic jar relative to the neo4j binaries, so all you need to do is store the newrelic.jar file in a location where neo4j can access it from when starting up.

Dashboard

Once this has been deployed to the cloud, we then have performance statistic automatically made available to use via the neo4j JVM on the New Relic web site!

image

From here, you can actually click a segment on the graph and drill into the method level calls that occurred in the JVM.

image

Notice that you can get details about the timing of method invocation timings.

image

Comparing environments

What is really cool, is comparing response times between environments, so you can see how fast UAT/Prod/Dev are compared to one another

image

Conclusion

It is a relatively easy task to get application performance statistics for neo4j running in or out of the cloud and New Relic seems to be a really useful tool with minimal overhead to get up and running, so I would highly recommend using the combination together. This coupled with VisualJM should provide enough performance data and profiling when collecting performance data and compiling reports.

Neo4j, JMX and VisualVM

It might be necessary to use JMX to profile neo4j, which we are currently doing to address performance issues with Neo4j in combination with Windows Azure.

The first thing you will need to configure is additional switches in the neo4j-wrapper.conf file, here are mine:

wrapper.java.additional.1=-d64
wrapper.java.additional.1=-server
wrapper.java.additional.1=-Xss2048k
wrapper.java.additional.2=-Dcom.sun.management.jmxremote.port=6666
wrapper.java.additional.2=-Dcom.sun.management.jmxremote.ssl=false
wrapper.java.additional.2=-Dcom.sun.management.jmxremote.authenticate=false

Notice, that I am using a port number here, and this will allow remote JMX profiling, the reason for this is that in azure, the neo4j instance runs in a different user context than the remote desktop user, so we need a way to attach to the process, with VisualVM, we can do this via remote JMX.

Just add the neo4j JMX listener as a remote host. Then, if using the azure cloud:

  • Remote desktop into the worker role hosting Neo4j
  • Install the JDK
  • Install VisualVM
  • Configure the JMX Connectionas outlined below
    Right click Local and click Add JMX Connection:

image

image

image

From here we can now do profiling of the Neo4j application.

image

Romiko

Lucene Full Text Indexing with Neo4j

Hi Guys,

I spent some time working on full text search for Neo4j. The basic goals were as follows.

    • Control the pointers of the index
    • Full Text Search
    • All operations are done via Rest
    • Can create an index when creating a node
    • Can update and index
    • Can check if an index exists
    • When bootstrapping Neo4j in the cloud run Index checks
    • Query Index using full text search lucene query language.
Download:
This is based on Neo4jClient:
Source Code at:

Introduction

So with the above objectives, I decided to go with Manual Indexing. The main reason here is that I can put an index pointing to node A based on values in node B.

Imagine the following.

You have Node A with a list:

Surname, FirstName and MiddleName. However Node A also has a relationship to Node B which has other names, perhaps Display Names, Avatar Names and AKA’s.

So with manual indexing, you can have all the above entries for names in Node A and Node B point to Node A only.

So, in a Rest call to the Neo4j server, it would look something like this in Fiddler.

image

Notice the following:

Url: http://localhost:7474/db/data/index/node/{IndexName}/{Key}/{Value}

So, if we were adding 3 names for the SAME client from 2 different nodes. You would have the same IndexName and Key then with different values in the Url. The node pointer (In the request body) will then be the address to the Node.

Neo4jClient Nuget Package

I have updated the Neo4jClient which is on Nuget, to now support:

  • Creating Exact or FullText Indexes on it’s own, so that it just exists
  • Creating Exact or FullTest indexes when creating a node, the node reference will automatically be calculated.
  • Updating an Index
  • Deleting entries from an index.
    Class diagram for the indexing solution in Neo4jClient.

image

RestSharp

The Neo4jClient package uses RestSharp, thus making all the index call operations a trivial task for us, so lets have a look at some of the code inside the client to see how to consume manual index api from .Net, and then in the next section well look how we consume this code from another application.

 public Dictionary<string, IndexMetaData> GetIndexes(IndexFor indexFor)
        {
            CheckRoot();

            string indexResource;
            switch (indexFor)
            {
                case IndexFor.Node:
                    indexResource = RootApiResponse.NodeIndex;
                    break;
                case IndexFor.Relationship:
                    indexResource = RootApiResponse.RelationshipIndex;
                    break;
                default:
                    throw new NotSupportedException(string.Format("GetIndexes does not support indexfor {0}", indexFor));
            }

            var request = new RestRequest(indexResource, Method.GET)
            {
                RequestFormat = DataFormat.Json,
                JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
            };

            var response =  client.Execute<Dictionary<string, IndexMetaData>>(request);

            if (response.StatusCode != HttpStatusCode.OK)
                throw new NotSupportedException(string.Format(
                    "Received an unexpected HTTP status when executing the request.\r\n\r\n\r\nThe response status was: {0} {1}",
                    (int)response.StatusCode,
                    response.StatusDescription));

            return response.Data;
        }

        public bool CheckIndexExists(string indexName, IndexFor indexFor)
        {
            CheckRoot();

            string indexResource;
            switch (indexFor)
            {
                case IndexFor.Node:
                    indexResource = RootApiResponse.NodeIndex;
                    break;
                case IndexFor.Relationship:
                    indexResource = RootApiResponse.RelationshipIndex;
                    break;
                default:
                    throw new NotSupportedException(string.Format("IndexExists does not support indexfor {0}", indexFor));
            }

            var request = new RestRequest(string.Format("{0}/{1}",indexResource, indexName), Method.GET)
            {
                RequestFormat = DataFormat.Json,
                JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
            };

            var response = client.Execute<Dictionary<string, IndexMetaData>>(request);

            return response.StatusCode == HttpStatusCode.OK;
        }

        void CheckRoot()
        {
            if (RootApiResponse == null)
                throw new InvalidOperationException(
                    "The graph client is not connected to the server. Call the Connect method first.");
        }

        public void CreateIndex(string indexName, IndexConfiguration config, IndexFor indexFor)
        {
            CheckRoot();

            string nodeResource;
            switch (indexFor)
            {
                case IndexFor.Node:
                    nodeResource = RootApiResponse.NodeIndex;
                    break;
                case IndexFor.Relationship:
                    nodeResource = RootApiResponse.RelationshipIndex;
                    break;
                default:
                    throw new NotSupportedException(string.Format("CreateIndex does not support indexfor {0}", indexFor));
            }

            var createIndexApiRequest = new
                {
                    name = indexName.ToLower(),
                    config
                };

            var request = new RestRequest(nodeResource, Method.POST)
                {
                    RequestFormat = DataFormat.Json,
                    JsonSerializer = new CustomJsonSerializer {NullHandling = JsonSerializerNullValueHandling}
                };
            request.AddBody(createIndexApiRequest);

            var response = client.Execute(request);

            if (response.StatusCode != HttpStatusCode.Created)
                throw new NotSupportedException(string.Format(
                    "Received an unexpected HTTP status when executing the request..\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                    indexName,
                    (int) response.StatusCode,
                    response.StatusDescription));
        }

        public void ReIndex(NodeReference node, IEnumerable<IndexEntry> indexEntries)
        {
            CheckRoot();

            var nodeAddress = string.Join("/", new[] {RootApiResponse.Node, node.Id.ToString()});

            var updates = indexEntries
                .SelectMany(
                    i => i.KeyValues,
                    (i, kv) => new {IndexName = i.Name, kv.Key, kv.Value});

            foreach (var update in updates)
            {
                if (update.Value == null)
                    break;

                string indexValue;
                if(update.Value is DateTimeOffset)
                {
                    indexValue = ((DateTimeOffset) update.Value).UtcTicks.ToString();
                }
                else if (update.Value is DateTime)
                {
                    indexValue = ((DateTime)update.Value).Ticks.ToString();
                }
                else
                {
                    indexValue = update.Value.ToString();
                }

                AddNodeToIndex(update.IndexName, update.Key, indexValue, nodeAddress);
            }
        }

        public void DeleteIndex(string indexName, IndexFor indexFor)
        {
            CheckRoot();

            string indexResource;
            switch (indexFor)
            {
                case IndexFor.Node:
                    indexResource = RootApiResponse.NodeIndex;
                    break;
                case IndexFor.Relationship:
                    indexResource = RootApiResponse.RelationshipIndex;
                    break;
                default:
                    throw new NotSupportedException(string.Format("DeleteIndex does not support indexfor {0}", indexFor));
            }

            var request = new RestRequest(string.Format("{0}/{1}", indexResource, indexName), Method.DELETE)
            {
                RequestFormat = DataFormat.Json,
                JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
            };

            var response = client.Execute(request);

            if (response.StatusCode != HttpStatusCode.NoContent)
                throw new NotSupportedException(string.Format(
                    "Received an unexpected HTTP status when executing the request.\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                    indexName,
                    (int)response.StatusCode,
                    response.StatusDescription));
        }

        void AddNodeToIndex(string indexName, string indexKey, string indexValue, string nodeAddress)
        {
            var nodeIndexAddress = string.Join("/", new[] { RootApiResponse.NodeIndex, indexName, indexKey, indexValue });
            var request = new RestRequest(nodeIndexAddress, Method.POST)
            {
                RequestFormat = DataFormat.Json,
                JsonSerializer = new CustomJsonSerializer { NullHandling = JsonSerializerNullValueHandling }
            };
            request.AddBody(string.Join("", client.BaseUrl, nodeAddress));

            var response = client.Execute(request);

            if (response.StatusCode != HttpStatusCode.Created)
                throw new NotSupportedException(string.Format(
                    "Received an unexpected HTTP status when executing the request.\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                    indexName,
                    (int)response.StatusCode,
                    response.StatusDescription));
        }

        public IEnumerable<Node<TNode>> QueryIndex<TNode>(string indexName, IndexFor indexFor, string query)
        {
            CheckRoot();

            string indexResource;

            switch (indexFor)
            {
                case IndexFor.Node:
                    indexResource = RootApiResponse.NodeIndex;
                    break;
                case IndexFor.Relationship:
                    indexResource = RootApiResponse.RelationshipIndex;
                    break;
                default:
                    throw new NotSupportedException(string.Format("QueryIndex does not support indexfor {0}", indexFor));
            }

            var request = new RestRequest(indexResource + "/" + indexName, Method.GET)
                {
                    RequestFormat = DataFormat.Json,
                    JsonSerializer = new CustomJsonSerializer {NullHandling = JsonSerializerNullValueHandling}
                };

            request.AddParameter("query", query);

            var response = client.Execute<List<NodeApiResponse<TNode>>>(request);

            if (response.StatusCode != HttpStatusCode.OK)
                throw new NotSupportedException(string.Format(
                    "Received an unexpected HTTP status when executing the request.\r\n\r\nThe index name was: {0}\r\n\r\nThe response status was: {1} {2}",
                    indexName,
                    (int) response.StatusCode,
                    response.StatusDescription));

            return response.Data == null
           ? Enumerable.Empty<Node<TNode>>()
           : response.Data.Select(r => r.ToNode(this));
        }
		

Using the Neo4jClient from within an application

Create an Index and check if it exists

This is useful when bootstrapping Neo4j, to see if there are any indexes that SHOULD be there and are not, so that you can enumerate all the nodes for that index and add entries.

public void CreateIndexesForAgencyClients()
        {
            var agencies = graphClient
                .RootNode
                .Out<Agency>(Hosts.TypeKey)
                .ToList();

            foreach (var agency in agencies)
            {
                var indexName = IndexNames.Clients(agency.Data);
                var indexConfiguration = new IndexConfiguration
                    {
                        Provider = IndexProvider.lucene,
                        Type = IndexType.fulltext
                    };

                if (!graphClient.CheckIndexExists(indexName, IndexFor.Node))
                {
                    Trace.TraceInformation("CreateIndexIfNotExists {0} for Agency Key {0}", indexName, agency.Data.Key);
                    graphClient.CreateIndex(indexName, indexConfiguration, IndexFor.Node);
                    PopulateAgencyClientIndex(agency.Data);
                }
            }
        }

Create an Index Node Entry when creating a node

 var indexEntries = GetIndexEntries(agency.Data, client, clientViewModel.AlsoKnownAses);

var clientNodeReference = graphClient.Create(
                client,
                new[] {new ClientBelongsTo(agencyNode.Reference)}, indexEntries);

public IEnumerable<IndexEntry> GetIndexEntries(Agency agency, Client client, IEnumerable<AlsoKnownAs> alsoKnownAses)
        {
            var indexKeyValues = new List<KeyValuePair<string, object>>
            {
                new KeyValuePair<string, object>(AgencyClientIndexKeys.Gender.ToString(), client.Gender)
            };

            if (client.DateOfBirth.HasValue)
            {
                var dateOfBirthUtcTicks = client.DateOfBirth.Value.UtcTicks;
                indexKeyValues.Add(new KeyValuePair<string, object>(AgencyClientIndexKeys.DateOfBirth.ToString(), dateOfBirthUtcTicks));
            }

            var names = new List<string>
            {
                client.GivenName,
                client.FamilyName,
                client.PreferredName,
            };

            if (alsoKnownAses != null)
            {
                names.AddRange(alsoKnownAses.Where(a => !string.IsNullOrEmpty(a.Name)).Select(aka => aka.Name));
            }

            indexKeyValues.AddRange(names.Select(name => new KeyValuePair<string, object>(AgencyClientIndexKeys.Name.ToString(), name)));

            return new[]
            {
                new IndexEntry
                {
                    Name = IndexNames.Clients(agency),
                    KeyValues = indexKeyValues.Where(v => v.Value != null)
                }
            };
        }
		

Reindex a node

Notice there was a call to PopulateAgencyClientIndexin in the code, this is done in our bootstrap to ensure indexes are always there as expected, and if for some reason they are not, then they created and populated by using reindex feature.

void PopulateAgencyClientIndex(Agency agency)
        {
            var clients = graphClient
                .RootNode
                .Out<Agency>(Hosts.TypeKey, a => a.Key == agency.Key)
                .In<Client>(ClientBelongsTo.TypeKey);

            foreach (var client in clients)
            {
                var clientService = clientServiceCallback();
                var akas = client.Out<AlsoKnownAs>(IsAlsoKnownAs.TypeKey).Select(a => a.Data);
                var indexEntries = clientService.GetIndexEntries(agency, client.Data, akas);
                graphClient.ReIndex(client.Reference, indexEntries);
            }
        }
		

Querying a full text search index using Lucene

Below is sample code to query full text search. Basically your index entries for a person with

Name: Bob, Surname:Van de Builder, Aka1: Bobby, Aka2: Bobs, PrefferedName: Bob The Builder

The index entries will need to look like the

Key:Value
Name: Bob
Name:Van
Name:de
Name: Builder
Name: Bobby
Name: Bobs

Remember, Lucene has a white space analyser, so any names with spaces MUST become a new index entry, so what we do is split out names based on whitespaces and this becomes our collection of IndexEntries. The above is related to full text search context.

Note: If using EXACT Index match, then composite entries are needed for multiple words, since you no longer using lucene full text search capabilities. e.g.

Name: Bob The Builder

This is good to know, because things like postal code searches or Gender where exact matches are required do not need full text indexes.

Lets check out an example of querying an index.

        [Test]
        public void VerifyWhenANewClientIsCreateThatPartialNameCanBeFuzzySearchedInTheFullTextSearchIndex()
        {
            using (var agency = Data.NewTestAgency())
            using (var client = Data.NewTestClient(agency, c =>
            {
                c.Gender = Gender.Male;
                c.GivenName = "Joseph";
                c.MiddleNames = "Mark";
                c.FamilyName = "Kitson";
                c.PreferredName = "Joey";

                c.AlsoKnownAses = new List<AlsoKnownAs>
                    {
                       new AlsoKnownAs {Name = "J-Man"},
                       new AlsoKnownAs {Name = "J-Town"}
                    };
            }
                ))
            {
                var indexName = IndexNames.Clients(agency.Agency.Data);
                const string partialName = "+Name:Joe~+Name:Kitson~";
                var result = GraphClient.QueryIndex<Client>(indexName, IndexFor.Node, partialName);
                Assert.AreEqual(client.Client.Data.UniqueId, result.First().Data.UniqueId);
            }
        }
		

Dates

Notice that in some of the code, you may have noticed that when I store date entries in the index, I store them as Ticks, so this will be as long numbers, this is awesome, as it gives raw power to searching dates via longs Smile

 [Test]
        public void VerifyWhenANewClientIsCreateThatTheDateOfBirthCanBeRangeSearchedInTheFullTextSearchIndex()
        {
            // Arrange
            const long dateOfBirthTicks = 634493518171556320;
            using (var agency = Data.NewTestAgency())
            using (var client = Data.NewTestClient(agency, c =>
            {
                c.Gender = Gender.Male;
                c.GivenName = "Joseph";
                c.MiddleNames = "Mark";
                c.FamilyName = "Kitson";
                c.PreferredName = "Joey";
                c.DateOfBirth = new DateTimeOffset(dateOfBirthTicks, new TimeSpan());
                c.CurrentAge = null;
                c.AlsoKnownAses = new List<AlsoKnownAs>
                    {
                       new AlsoKnownAs {Name = "J-Man"},
                       new AlsoKnownAs {Name = "J-Town"}
                    };
            }
                ))
            {
                // Act
                var indexName = IndexNames.Clients(agency.Agency.Data);
                var partialName = string.Format("DateOfBirth:[{0} TO {1}]", dateOfBirthTicks - 5, dateOfBirthTicks + 5);
                var result = GraphClient.QueryIndex<Client>(indexName, IndexFor.Node, partialName);
                // Assert
                Assert.AreEqual(client.Client.Data.UniqueId, result.First().Data.UniqueId);
            }
        }
		

Summary

Well, I hope you found this post useful. Neo4jClientis on nuget, so have a bash using it and would love to know your feedback.

Download

NuGetPackage:
Source Code at:

Cheers