GraphQL, filtering results from full text search in neo4j

Neo4j and neo4j-graphql-js is a really nice combo for creating a GraphQL-based backend and I particularly find the nested filtering parameter powerful. I was hoping to use this together with full-text indexing of nodes to build an app where you do keyword searches combined with various filtering selection. Unfortunately, this does not seem to be possible because filters are not supported yet for @cypher directive fields which is needed to utilize the full text-index.

Currently I use the following type:

type Query {
works (query: String):[Work] @cypher(
statement: "CALL db.index.fulltext.queryNodes('work', $query) yield node RETURN node"
)
}

and want to do queries like:

{
works(query: "Baskervilles"
filter:{
contributors_some:{name_contains: "Doyle"}
}){
title
contributors{
name
}
}
}

Any advice for workaround to solve this? or something that will be supported in the near future?
(Current idea for workaround is to rather rely on neo4j substring search using CONTAINS but this is somewhat awkward and not very efficient.)

1 Like

A lot of times the best thing to do is add the longer cypher query, so like as you said, the substring contains, variant. You can use the full power and functionality of cypher so if you can get it done with that it's the easiest route. Most of the built in filter methods, etc are for things like dates and numbers.

Stumbled upon the same limitation as @taalberg recently. I'm running the latest everything neo4j-graphql-js and Neo4j 4.0.0 but this functionality is still not there. Too bad.

This blog post seems to imply it's possible:

@vanbenj,

I don't see any filters being used in the article you're referring to. It's just traditional attribute selection when results are back. What we want to achieve is to use the keyword "filter" such as:

Podcast(podcastId: $podcastId, first: 1, filter: {title_ends_with: "something"}) {
    podcastId
    title
...

Does anyone know if this feature will ever see the light of day?

2 Likes

I'm trying to figure out the same thing. Intuitively, as a developer, you'd expect the return's _TypeFilter to be available as a param...but its not.

Under custom cypher based query functions, only pagination but not filtration is available out of the box:. IMHO, this makes zero sense, since we're building queries, filtration must be included pre-rolled for you, not just pagination.

We're hacking away trying to figure this out too

The post you shared refers to "filteration" 0 times...how therefore, does it imply its possible? :laughing:

If you have a code snippet, please do share :pray:

1 Like

I found a workaround for this. It'a a little fragile, but it works.

Create a custom query in the schema. Include filter input and a string input for your search string.

type Query {
  fuzzyItemByName(searchString: String, filter: _ItemFilter): [Item]
}

Create a custom resolver for the query

import { neo4jgraphql, cypherQuery } from 'neo4j-graphql-js';
import { extractQueryResult } from 'neo4j-graphql-js/dist/utils';

const Query = {
  async fuzzyItemByName(object, args, ctx, resolveInfo) {
    if (!args.searchString) {
      return neo4jgraphql(object, args, ctx, resolveInfo);
    }

    const fullTextQuery = `CALL db.index.fulltext.queryNodes('itemNameIndex', $searchString + '~') YIELD node AS item`;

    let [query, queryParams] = cypherQuery(args, ctx, resolveInfo);

    query = query.replace(
      'MATCH (`item`:`Item` {searchString:$searchString})',
      fullTextQuery
    );

    let session = ctx.driver.session();

    let result;

    try {
      result = await session.readTransaction((tx) => {
        return tx.run(query, queryParams);
      });
    } finally {
      session.close();
    }
    return extractQueryResult(result, resolveInfo.returnType);
  },
};

What I'm doing in the resolver is checking to see if the search string is in the query.

If it isn't then pass the query through to neo4jgraphql

But, if it is, then I do some find and replace of MATCH ... with CALL .... Yes this part is fragile, but hopefully it's helpful.

Then start a session and execute the query.

Use extractQueryResult to get the return payload in the correct format.

To use, run a query like this

  fuzzyItemByName(
    searchString: "t-shart"
    filter: { price_lt: 3000 }
  ) {
    name
    price
  }

:v:

This is an interesting idea but it appears to no longer work with neo4j-graphql-js 4.1.0.
It says cypherQuery is not exported from neo4j-graphql-js.
Where is it now?

Hi malik, please double check you are using the correct package. I see the latest version on npm is 2.14.4 and it looks like cypherQuery is exported.

And, makes sure you use the curly braces on the import statement.

I came up with an ad hoc solution that works well in my project where use cases either are full text-searches combined with filtering, or full GraphQL queries based on exact match. If there is one field used for full text queries, this field can be used in an if-else statement and it works well to combine a call statement with the generated query-parts for filtering.

The following code is my ad hoc adaptation in neo4j-graphql-js/translate.js/nodeQuery(). Mainly just adding if-else and using a CALL-statement instead of MATCH for the full text query. Some duplication of code, but this is just to make it easier to merge in changes from the master branch.

  if ('textquery' in params) {
    let textquery = params.textquery;
    let query = `CALL db.index.fulltext.queryNodes('${variableName}', '${textquery}') yield node as ${safeVariableName}
      ${predicate}${
      optimization.earlyOrderBy
        ? `WITH ${safeVariableName}${orderByClause}`
        : ''
    }RETURN ${mapProjection} AS ${safeVariableName}${
      optimization.earlyOrderBy ? '' : orderByClause
    }${outerSkipLimit}`;
    return [query, { ...params, ...fragmentTypeParams }];
  } else {
    let query = `MATCH (${safeVariableName}:${safeLabelName}${
      argString ? ` ${argString}` : ''
    }) ${predicate}${
      optimization.earlyOrderBy
        ? `WITH ${safeVariableName}${orderByClause}`
        : ''
    }RETURN ${mapProjection} AS ${safeVariableName}${
      optimization.earlyOrderBy ? '' : orderByClause
    }${outerSkipLimit}`;
    return [query, { ...params, ...fragmentTypeParams }];
  }
};