Create nodes if optional match returns null - database

All the nodes I'll be using have unique constraints associated to them.
I tried to create new nodes like this (I thought about MERGE so it would not create the node if it already exists):
MERGE (n:url{url_addr:'test.com'})-[:FROM_DEVICE]->(o:device{mode:'iphone'})
And the node already exists it returns this error:
Node(21) already exists with label `url` and property `url_addr` = 'test.com'
It was obvious, since I didn't match them beforehand, so I did it in order to just pass the results properties:
MATCH (n:url{url_addr:'test.com'}), (o:device{mode:'iphone'})
MERGE (n)-[:FROM_DEVICE]->(o)
But in case any of this records do not exist new nodes will not be created.
So I thought about using OPTIONAL MATCH, since it would replace any non-existent value with null, but this means it would also replace the properties I passed with null values.
OPTIONAL MATCH (n:url{url_addr:'test.com'}) RETURN n
MERGE n-[:FROM_DEVICE]->(o:device{mode:'iphone'})
It returned this error:
Failed to create relationship ` REL87(d65d30eb-65f5-4460-b166-15996622cf1b)`, node `n` is missing. If you prefer to simply ignore rows where a relationship node is missing, set 'cypher.lenient_create_relationship = true' in neo4j.conf
So I thought about using CASE statements in order to first verify if the match would return null and then create the node to define the relationship, but things didn't work well:
OPTIONAL MATCH (n:url{url_addr:'test.com'})
WITH n as test
RETURN CASE
WHEN test IS NULL THEN CREATE (:url{url_addr:'test.com'})
END
This happens:
Invalid input '{': expected
"!="
"%"
")"
"*"
"+"
","
"-"
"."
"/"
":"
"<"
"<="
"<>"
"="
"=~"
">"
">="
"AND"
"CONTAINS"
"ENDS"
"IN"
"IS"
"OR"
"STARTS"
"XOR"
"["
"^" (line 4, column 51 (offset: 135))
" WHEN CASE IS NULL THEN CREATE (a:cookie_id{url:'test.com'})"
^
Removing the properties of the node does not help.

MERGE always matches or creates the whole pattern. So, you might want to split up the MERGE clause, so that it matches or creates each node and the relationship:
MERGE (n:url {url_addr:'test.com'}) // match or create url node
MERGE (o:device {mode:'iphone'}) // match or create device node
MERGE (n)-[:FROM_DEVICE]->(o) // match or create relationship
You might also want to have a look at conditional cypher execution with APOC procedures:
https://neo4j.com/labs/apoc/4.2/cypher-execution/conditionals/

Related

NOT IN within a cypher query

I'm attempting to find all values that match any item within a list of values within cypher. Similar to a SQL query with in and not in. I also want to find all values that are not in the list in a different query. The idea is I want to assign a property to each node that is binary and indicates whether the name of the node is within the predefined list.
I've tried the following code blocks:
MATCH (temp:APP) - [] -> (temp2:EMAIL_DOMAIN)
WHERE NOT temp2.Name IN ['GMAIL.COM', 'YAHOO.COM', 'OUTLOOK.COM', 'ICLOUD.COM', 'LIVE.COM']
RETURN temp
This block returns nothing, but should return a rather large amount of data.
MATCH (temp:APP) - [] -> (temp2:EMAIL_DOMAIN)
WHERE temp2.Name NOT IN ['GMAIL.COM', 'YAHOO.COM', 'OUTLOOK.COM', 'ICLOUD.COM', 'LIVE.COM']
RETURN temp
This code block returns an error in relation to the NOT's position. Does anyone know the correct syntax for this statement? I've looked around online and in the neo4j documentation, but there are a lot of conflicting ideas with version changes. Thanks in advance!
Neo4j is case sensitive so you need to check the data to ensure that the EMAIL_DOMAIN.Name is all upper case. If Name is mixed case, you can convert the name using toUpper(). If Name is all lower case, then you need to convert the values in your query.
MATCH (temp:APP) - [] -> (temp2:EMAIL_DOMAIN)
WHERE NOT toUpper(temp2.Name) IN ['GMAIL.COM', 'YAHOO.COM', 'OUTLOOK.COM', 'ICLOUD.COM', 'LIVE.COM']
RETURN temp

Cypher: How to merge two nodes' keys in a third node

in cypher i want to create a node that has attributes of two existing nodes. I know that to "copy" a node the query is: MATCH (old:Type) CREATE (new:Type) SET new=old but this allow me to have the keys from just one node. I need a way to join the keys of two nodes and copy that node
match (a:Ubuntu1604), (h:Host) CREATE (b:Ubuntu1604) SET b=a,b=h return b
will obviously create b equals to h. I need and "append" function
I figured out myself.
Solution is simply
match (a:Ubuntu1604), (h:Host) CREATE (b:Ubuntu1604) SET b=a, b+=h return b

AT NEW with substring access?

I have a solution that includes a LOOP which I would like to spare. So I wonder, whether you know a better way to do this.
My goal is to loop through an internal, alphabetically sorted standard table. This table has two columns: a name and a table, let's call it subtable. For every subtable I want to do some stuff (open an xml page in my xml framework).
Now, every subtable has a corresponding name. I want to group the subtables according to the first letter of this name (meaning, put the pages of these subtables on one main page -one main page for every character-). By grouping of subtables I mean, while looping through the table, I want to deal with the subtables differently according to the first letter of their name.
So far I came up with the following solution:
TYPES: BEGIN OF l_str_tables_extra,
first_letter(1) TYPE c,
name TYPE string,
subtable TYPE REF TO if_table,
END OF l_str_tables_extra.
DATA: ls_tables_extra TYPE l_str_tables_extra.
DATA: lt_tables_extra TYPE TABLE OF l_str_tables_extra.
FIELD-SYMBOLS: <ls_tables> TYPE str_table."Like LINE OF lt_tables.
FIELD-SYMBOLS: <ls_tables_extra> TYPE l_str_tables_extra.
*"--- PROCESSING LOGIC ------------------------------------------------
SORT lt_tables ASCENDING BY name.
"Add first letter column in order to use 'at new' later on
"This is the loop I would like to spare
LOOP AT lt_tables ASSIGNING <ls_tables>.
ls_tables_extra-first_letter = <ls_tables>-name+0(1). "new column
ls_tables_extra-name = <ls_tables>-name.
ls_tables_extra-subtable = <ls_tables>-subtable.
APPEND ls_tables_extra TO lt_tables_extra.
ENDLOOP.
LOOP AT lt_tables_extra ASSIGNING <ls_tables_extra>.
AT NEW first_letter.
"Do something with subtables with same first_letter.
ENDAT.
ENDLOOP.
I wish I could use
AT NEW name+0(1)
instead of
AT NEW first_letter
, but offsets and lengths are not allowed.
You see, I have to inlcude this first loop to add another column to my table which is kind of unnecessary because there is no new info gained.
In addition, I am interested in other solutions because I get into trouble with the framework later on for different reasons. A different way to do this might help me out there, too.
I am happy to hear any thoughts about this! I could not find anything related to this here on stackoverflow, but I might have used not optimal search terms ;)
Maybe the GROUP BY addition on LOOP could help you in this case:
LOOP AT i_tables
INTO DATA(wa_line)
" group lines by condition
GROUP BY (
" substring() because normal offset would be evaluated immediately
name = substring( val = wa_line-name len = 1 )
) INTO DATA(o_group).
" begin of loop over all tables starting with o_group-name(1)
" loop over group object which contains
LOOP AT GROUP o_group
ASSIGNING FIELD-SYMBOL(<fs_table>).
" <fs_table> contains your table
ENDLOOP.
" end of loop
ENDLOOP.
why not using a IF comparison?
data: lf_prev_first_letter(1) type c.
loop at lt_table assigning <ls_table>.
if <ls_table>-name(1) <> lf_prev_first_letter. "=AT NEW
"do something
lf_prev_first_letter = <ls_table>-name(1).
endif.
endloop.

Case in-sensitive search in array fields

I want to store an array to Neo4j db, Before knowing array field types, I was stored my array as an comma separated text field and used regex for find nodes that have an specific item!
Now i want to save array as an array field but i cant write a case insensitive condition with "IN" keyword!
There is my Regex for finding in cs-text field:
MATCH (user:USER)-[:MEMBER_OF]->(group:SOME_GROUP) where
group.resources =~ "(?i)(?:.*,|^)one_resource(?:,.*|$)"
RETURN group
My stored data was like this:
One_Resource,Another_Resource,...
And result was correct but i have some issues on retrieving this model and array field is better in retrieve.
Do you have any suggesting or method to solve this issue?
This is my Cypher for array field:
MATCH (node {hid:"abc"})
SET node.array_field = ["Foo","Bar","Baz","BaG"]
And finding with this:
MATCH (node) WHERE "foo" IN node.array_field RETURN node
But this is case sensitive :(
Thanks
I founded my answer in the book: Learning Cypher
Must use ANY (...IN...WHERE...) in WHERE clause:
MATCH (node)
WHERE ANY ( item IN node.array_field WHERE item =~ "(?i)foo" )
RETURN node
Now can use Regex to finding wanted node.
There are four collection predicates. They are as follows:
ANY: This predicate returns TRUE if at least one item in the collection adheres
to the expression
ALL: This predicate returns TRUE if all items in the collection adhere to the rule
NONE: This predicate returns TRUE if no item in the collection follows the rule
SINGLE: This predicate returns TRUE if exactly one item follows the rule
If we want all books tagged as NoSQL but not as Neo4j, we can use the NONE predicate
as follows:
MATCH (b:Book)
WHERE ANY ( tag IN b.tags WHERE tag = 'nosql' )
AND NONE ( tag in b.tags WHERE tag = 'neo4j' )
RETURN b.title,b.tags

How to do better text search in neo4j

I have two types of nodes Article and TAG where TAG have two properties id and name. Now I want to search all the articles according to tags.
(a : Article)-[:TAGGED]->(t : TAG)
e.g If I have tags like "i love my country" and my query string is "country" then search is successfully using the following query.
Match (a : Article)-[:TAGGED]->(t : TAG)
where t.name =~ '*.country.*'
return a;
But its vice-versa is not possible like if my tag is "country" and I search for "i love my country" then it should also display the articles related to country. It should also handle the case when user have entered more than one space between the two words. On searching I came accross lucene and solr but I don't know how to use them. And I am using PHP as my coding language.
[EDITED]
Original Answer
This should work for you:
MATCH (a: Article)-[:TAGGED]->(t:TAG)
WHERE ANY(word IN FILTER(x IN SPLIT({searchString}, " ") WHERE x <> '')
WHERE t.name CONTAINS word)
RETURN a;
{searchString} is your search string, with one or spaces separating words; e.g.:
"i love my country"
This snippet generates a collection of the non-empty words in {searchString}:
FILTER(x IN SPLIT({searchString}, " ") WHERE x <> '')
Improved Answer
This query matches on words (e.g., if the query string is "i love you", the "i" will only match "i" or "I" as a word in the tag, not just any letter "i"). It is also case insensitive.
WITH REDUCE(res = [], w IN SPLIT({searchString}, " ") |
CASE WHEN w <> '' THEN res + ("(?i).*\\b" + w + "\\b.*") ELSE res END) AS res
MATCH (a: Article)-[:TAGGED]->(t:TAG)
WHERE ANY (regexp IN res WHERE t.name =~ regexp)
RETURN a;
The REDUCE clause generates a collection of words from {searchString}, each surrounded by "(?i).*\b" and "\b.*" to become a regular expression for doing a case insensitive search with word boundaries.
NOTE: the backslashes ("\") in the regular expression actually have to be doubled-up because the backslash is an escape charater.
Neo4j uses Lucene indices internally for fulltext search.
Based on this page from the user guide, it appears that the default indexing 'type' is exact using the Lucene Keyword Analyzer which doesn't tokenize the input.
What that means, is that without changing this indexing setting you can only run queries that match the entire tag name (in the case of your example, you're running a wildcard query '*.country.*' which matches the whole tag string).
What I think you actually want, based on your stated requirements is tokenization in whitespace (type=fulltext) at the time you insert the graph data, so that the tag field actually contains one token per word: 1-i 2-love 3-my 4-country, any one of which can match a query term (without needing wildcards: eg "country" or "I love my chocolate")

Resources