diff --git a/src/main/scala/com/gu/contentapi/porter/graphql/Content.scala b/src/main/scala/com/gu/contentapi/porter/graphql/Content.scala index eea0d9c..44c76fb 100644 --- a/src/main/scala/com/gu/contentapi/porter/graphql/Content.scala +++ b/src/main/scala/com/gu/contentapi/porter/graphql/Content.scala @@ -147,7 +147,7 @@ object Content { ReplaceField("tags", Field("tags", OptionType(ListType(Tags.Tag)), arguments = TagQueryParameters.NonPaginatedTagQueryParameters, - resolve=ctx=> ctx.ctx.repo.tagsForList(ctx.value.tags, ctx arg TagQueryParameters.Section, ctx arg TagQueryParameters.TagType)) + resolve=ctx=> ctx.ctx.repo.tagsForList(ctx.value.tags, ctx arg TagQueryParameters.Section, ctx arg TagQueryParameters.TagType, ctx arg TagQueryParameters.Category, ctx arg TagQueryParameters.Reference)) ), ExcludeFields("atomIds", "isGone", "isExpired", "sectionId"), AddFields( diff --git a/src/main/scala/com/gu/contentapi/porter/graphql/ContentQueryParameters.scala b/src/main/scala/com/gu/contentapi/porter/graphql/ContentQueryParameters.scala index 45d8d82..bd0d5d9 100644 --- a/src/main/scala/com/gu/contentapi/porter/graphql/ContentQueryParameters.scala +++ b/src/main/scala/com/gu/contentapi/porter/graphql/ContentQueryParameters.scala @@ -11,7 +11,7 @@ object ContentQueryParameters { val ContentIdArg = Argument("id", OptionInputType(StringType), description = "get one article by ID") val QueryString = Argument("q", OptionInputType(StringType), description = "an Elastic Search query string to search for content") val QueryFields = Argument("queryFields", OptionInputType(ListInputType(StringType)), description = "fields to perform a query against. Defaults to webTitle and path.") - val TagArg = Argument("tags", OptionInputType(ListInputType(StringType)), description = "look up articles associated with all of these tag IDs") + val TagArg = Argument("tags", OptionInputType(ListInputType(StringType)), description = "look up articles associated with all of these tag IDs. If you don't have exact tag IDs you should instead make a root query on Tags and use the `matchingContent` or `matchingAnyTag` selectors") val ExcludeTagArg = Argument("excludeTags", OptionInputType(ListInputType(StringType)), description = "don't include any articles with these tag IDs") val SectionArg = Argument("sectionId", OptionInputType(ListInputType(StringType)), description = "look up articles in any of these sections") val ExcludeSectionArg = Argument("excludeSections", OptionInputType(ListInputType(StringType)), description = "don't include any articles with these tag IDs") diff --git a/src/main/scala/deprecated/anotherschema/Edge.scala b/src/main/scala/com/gu/contentapi/porter/graphql/Edge.scala similarity index 95% rename from src/main/scala/deprecated/anotherschema/Edge.scala rename to src/main/scala/com/gu/contentapi/porter/graphql/Edge.scala index 48ec3ec..b9f6ca5 100644 --- a/src/main/scala/deprecated/anotherschema/Edge.scala +++ b/src/main/scala/com/gu/contentapi/porter/graphql/Edge.scala @@ -9,11 +9,11 @@ import java.nio.charset.StandardCharsets import java.util.Base64 import scala.util.Try import io.circe.syntax._ -@deprecated("you should be using com.gu.contentapi.porter.graphql") + case class Edge[T:io.circe.Decoder](totalCount:Long, endCursor:Option[String], hasNextPage:Boolean, nodes:Seq[T]) { def map[V:io.circe.Decoder](mapper:(T)=>V) = Edge[V](totalCount, endCursor, hasNextPage, nodes.map(mapper)) } -@deprecated("you should be using com.gu.contentapi.porter.graphql") + object Edge { private val logger = LoggerFactory.getLogger(getClass) private val encoder = Base64.getEncoder diff --git a/src/main/scala/com/gu/contentapi/porter/graphql/PaginationParameters.scala b/src/main/scala/com/gu/contentapi/porter/graphql/PaginationParameters.scala index 0ba8614..10f9b62 100644 --- a/src/main/scala/com/gu/contentapi/porter/graphql/PaginationParameters.scala +++ b/src/main/scala/com/gu/contentapi/porter/graphql/PaginationParameters.scala @@ -17,8 +17,9 @@ object PaginationParameters { object OrderDateSchema { val definition = EnumType( "OrderDate", - Some("Which date field to use for ordering the content"), + Some("Which date field to use for ordering the content, or whether to search on document score"), List( + EnumValue("score", Some("Ignore when the content was made or published and sort by relevance to the query parameters"), "score"), EnumValue("published", Some("When the content was published to web"), "webPublicationDate"), EnumValue("firstPublished", Some("When the first version of this content was published"), "fields.firstPublicationDate"), EnumValue("lastModified", Some("The last time the content was modified prior to publication"), "fields.lastModified"), diff --git a/src/main/scala/com/gu/contentapi/porter/graphql/RootQuery.scala b/src/main/scala/com/gu/contentapi/porter/graphql/RootQuery.scala index d8287ab..8f41016 100644 --- a/src/main/scala/com/gu/contentapi/porter/graphql/RootQuery.scala +++ b/src/main/scala/com/gu/contentapi/porter/graphql/RootQuery.scala @@ -1,6 +1,7 @@ package com.gu.contentapi.porter.graphql import com.gu.contentapi.porter.model.{Content, Tag} +import com.sksamuel.elastic4s.requests.searches.sort.SortOrder import sangria.schema._ import datastore.GQLQueryContext import deprecated.anotherschema.Edge @@ -10,6 +11,8 @@ import scala.concurrent.ExecutionContext.Implicits.global import io.circe.generic.auto._ import org.slf4j.LoggerFactory +import scala.concurrent.Future + object RootQuery { private val logger = LoggerFactory.getLogger(getClass) @@ -24,14 +27,41 @@ object RootQuery { ) ) - val TagEdge: ObjectType[Unit, Edge[Tag]] = ObjectType( + val TagEdge: ObjectType[GQLQueryContext, Edge[Tag]] = ObjectType( "TagEdge", "A list of tags with pagination features", - () => fields[Unit, Edge[Tag]]( + () => fields[GQLQueryContext, Edge[Tag]]( Field("totalCount", LongType, Some("Total number of results that match your query"), resolve = _.value.totalCount), Field("endCursor", OptionType(StringType), Some("The last record cursor in the set"), resolve = _.value.endCursor), Field("hasNextPage", BooleanType, Some("Whether there are any more records to retrieve"), resolve = _.value.hasNextPage), - Field("nodes", ListType(com.gu.contentapi.porter.graphql.Tags.Tag), Some("The actual tags returned"), resolve = _.value.nodes) + Field("nodes", ListType(com.gu.contentapi.porter.graphql.Tags.Tag), Some("The actual tags returned"), resolve = _.value.nodes), + Field("matchingAnyTag", ArticleEdge, Some("Content which matches any of the tags returned"), + arguments= ContentQueryParameters.AllContentQueryParameters, + resolve = { ctx=> + if(ctx.value.nodes.isEmpty) { + Future(Edge[Content]( + 0L, + None, + false, + Seq() + )) + } else { + ctx.ctx.repo.marshalledDocs(ctx arg ContentQueryParameters.QueryString, + queryFields = ctx arg ContentQueryParameters.QueryFields, + atomId = None, + forChannel = ctx arg ContentQueryParameters.ChannelArg, + userTier = ctx.ctx.userTier, + tagIds = Some(ctx.value.nodes.map(_.id)), + excludeTags = ctx arg ContentQueryParameters.ExcludeTagArg, + sectionIds = ctx arg ContentQueryParameters.SectionArg, + excludeSections = ctx arg ContentQueryParameters.ExcludeSectionArg, + orderDate = ctx arg PaginationParameters.OrderDate, + orderBy = ctx arg PaginationParameters.OrderBy, + limit = ctx arg PaginationParameters.Limit, + cursor = ctx arg PaginationParameters.Cursor, + ) + } + }) ) ) @@ -56,6 +86,8 @@ object RootQuery { val Query = ObjectType[GQLQueryContext, Unit]( "Query", fields[GQLQueryContext, Unit]( Field("article", ArticleEdge, + description = Some("An Article is the main unit of our publication. You can search articles directly here, or query" + + " tags or sections to see what articles live within it."), arguments = ContentQueryParameters.AllContentQueryParameters, resolve = ctx => ctx arg ContentQueryParameters.ContentIdArg match { @@ -71,11 +103,23 @@ object RootQuery { } ), Field("tag", TagEdge, + description = Some("The Guardian uses tags to group similar pieces of content together across multiple different viewpoints. " + + "Tags are a closed set, which can be searched here, and there are different types of tags which represent different viewpoints"), arguments = TagQueryParameters.AllTagQueryParameters, resolve = ctx => - ctx.ctx.repo.marshalledTags(ctx arg TagQueryParameters.tagId, ctx arg TagQueryParameters.Section, ctx arg TagQueryParameters.TagType, ctx arg PaginationParameters.OrderBy, ctx arg PaginationParameters.Limit, ctx arg PaginationParameters.Cursor) + ctx.ctx.repo.marshalledTags(ctx arg TagQueryParameters.QueryString, + ctx arg TagQueryParameters.Fuzziness, + ctx arg TagQueryParameters.tagId, + ctx arg TagQueryParameters.Section, + ctx arg TagQueryParameters.TagType, + ctx arg TagQueryParameters.Category, + ctx arg TagQueryParameters.Reference, + ctx arg PaginationParameters.OrderBy, + ctx arg PaginationParameters.Limit, ctx arg PaginationParameters.Cursor) ), Field("atom", AtomEdge, + description = Some("An Atom is a piece of content which can be linked to multiple articles but may have a production lifecycle independent" + + " of these articles. Examples are cartoons, videos, quizzes, call-to-action blocks, etc."), arguments = AtomQueryParameters.AllParameters, resolve = ctx=> ctx.ctx.repo.atoms(ctx arg AtomQueryParameters.AtomIds, ctx arg AtomQueryParameters.QueryString, ctx arg AtomQueryParameters.QueryFields, diff --git a/src/main/scala/com/gu/contentapi/porter/graphql/TagQueryParameters.scala b/src/main/scala/com/gu/contentapi/porter/graphql/TagQueryParameters.scala index ed60340..80df28f 100644 --- a/src/main/scala/com/gu/contentapi/porter/graphql/TagQueryParameters.scala +++ b/src/main/scala/com/gu/contentapi/porter/graphql/TagQueryParameters.scala @@ -96,10 +96,31 @@ object TagQueryParameters { ) ) + val FuzzinessOptions = EnumType( + "FuzzinessOptions", + Some("Valid options for making a fuzzy-match query"), + List( + EnumValue("AUTO", + value="AUTO", + description=Some("Generates an edit distance based on the length of the term. If the term is >5 chars, then 2 edits allowed; if <3 chars than no edits allowed") + ), + EnumValue("OFF", + value="OFF", + description=Some("Disable fuzzy-matching") + ) + ) + ) + val tagId = Argument("tagId", OptionInputType(StringType), description = "Retrieve this specific tag") val Section = Argument("section", OptionInputType(StringType), description = "Only return tags from this section") val TagType = Argument("type", OptionInputType(TagTypes), description = "Type of the tag to return") - val AllTagQueryParameters = tagId :: Section :: TagType :: Cursor :: OrderBy :: Limit :: Nil + val QueryString = Argument("q", OptionInputType(StringType), description = "Search for tags that match this public-facing name") + val Fuzziness = Argument("fuzzy", OptionInputType(FuzzinessOptions), description = "Perform a fuzzy-matching query (default). Set to `OFF` to disable fuzzy-matching.") + val Category = Argument("category", OptionInputType(StringType), description = "A category to match against tags") + val Reference = Argument("reference", OptionInputType(StringType), description = "A reference to match against tags") + val AllTagQueryParameters = QueryString :: tagId :: Section :: TagType :: Fuzziness :: Category :: + Reference :: Cursor :: OrderBy :: Limit :: Nil - val NonPaginatedTagQueryParameters = Section :: TagType :: Nil + val NonPaginatedTagQueryParameters = QueryString :: tagId :: Section :: TagType :: Fuzziness :: Category :: + Reference :: Nil } diff --git a/src/main/scala/com/gu/contentapi/porter/graphql/Tags.scala b/src/main/scala/com/gu/contentapi/porter/graphql/Tags.scala index a96b110..02cc440 100644 --- a/src/main/scala/com/gu/contentapi/porter/graphql/Tags.scala +++ b/src/main/scala/com/gu/contentapi/porter/graphql/Tags.scala @@ -7,6 +7,7 @@ import java.time.format.DateTimeFormatter import io.circe.generic.auto._ import io.circe.syntax._ import com.gu.contentapi.porter.model +import datastore.GQLQueryContext object Tags { import Content.Reference @@ -15,10 +16,30 @@ object Tags { implicit val PodcastCategory = deriveObjectType[Unit, model.PodcastCategory]() implicit val TagPodcast = deriveObjectType[Unit, model.TagPodcast]() - val Tag = deriveObjectType[Unit, model.Tag]( + val Tag = deriveObjectType[GQLQueryContext, model.Tag]( ReplaceField("type", Field("type", OptionType(TagQueryParameters.TagTypes), resolve=_.value.`type`)), - ReplaceField("alternateIds", Field("alternateIds", ListType(StringType), arguments = AlternateIdParameters.AllAlternateIdParameters, resolve= AlternateIdParameters.TagResolver[Unit])), + ReplaceField("alternateIds", Field("alternateIds", ListType(StringType), arguments = AlternateIdParameters.AllAlternateIdParameters, resolve= AlternateIdParameters.TagResolver[GQLQueryContext])), ReplaceField("tagCategories", Field("tagCategories", ListType(StringType), resolve = _.value.tagCategories.map(_.toSeq).getOrElse(Seq()))), - ReplaceField("entityIds", Field("entityIds", ListType(StringType), resolve = _.value.tagCategories.map(_.toSeq).getOrElse(Seq()))) + ReplaceField("entityIds", Field("entityIds", ListType(StringType), resolve = _.value.tagCategories.map(_.toSeq).getOrElse(Seq()))), + AddFields( + Field("matchingContent", RootQuery.ArticleEdge, description=Some("Articles and other content which have this tag"), + arguments= ContentQueryParameters.AllContentQueryParameters.filterNot(_ == ContentQueryParameters.TagArg), + resolve = { ctx=> + ctx.ctx.repo.marshalledDocs(ctx arg ContentQueryParameters.QueryString, + queryFields=ctx arg ContentQueryParameters.QueryFields, + atomId = None, + forChannel = ctx arg ContentQueryParameters.ChannelArg, + userTier = ctx.ctx.userTier, + tagIds = Some(Seq(ctx.value.id)), + excludeTags = ctx arg ContentQueryParameters.ExcludeTagArg, + sectionIds = ctx arg ContentQueryParameters.SectionArg, + excludeSections = ctx arg ContentQueryParameters.ExcludeSectionArg, + orderDate = ctx arg PaginationParameters.OrderDate, + orderBy = ctx arg PaginationParameters.OrderBy, + limit = ctx arg PaginationParameters.Limit, + cursor = ctx arg PaginationParameters.Cursor, + ) + }) + ) ) } diff --git a/src/main/scala/datastore/DocumentRepo.scala b/src/main/scala/datastore/DocumentRepo.scala index 19a74ee..01325b7 100644 --- a/src/main/scala/datastore/DocumentRepo.scala +++ b/src/main/scala/datastore/DocumentRepo.scala @@ -20,10 +20,18 @@ trait DocumentRepo { orderDate:Option[String], orderBy:Option[SortOrder], limit: Option[Int], cursor: Option[String]): Future[Edge[Content]] - def marshalledTags(maybeTagId:Option[String], maybeSection: Option[String], tagType:Option[String], - orderBy: Option[SortOrder], limit: Option[Int], cursor: Option[String]): Future[Edge[Tag]] - - def tagsForList(tagIdList:Seq[String], maybeSection: Option[String], tagType:Option[String]):Future[Seq[Tag]] + def marshalledTags(maybeQuery:Option[String], + maybeFuzziness:Option[String], + maybeTagId:Option[String], + maybeSection: Option[String], + tagType:Option[String], + maybeCategory:Option[String], + maybeReferences:Option[String], + orderBy: Option[SortOrder], + limit: Option[Int], + cursor: Option[String]): Future[Edge[Tag]] + + def tagsForList(tagIdList:Seq[String], maybeSection: Option[String], tagType:Option[String], maybeCategory:Option[String], maybeReferences:Option[String]):Future[Seq[Tag]] def atomsForList(atomIds: Seq[String], atomType: Option[String]):Future[Seq[Json]] diff --git a/src/main/scala/datastore/ElasticsearchRepo.scala b/src/main/scala/datastore/ElasticsearchRepo.scala index eecc8a4..c7bd8fb 100644 --- a/src/main/scala/datastore/ElasticsearchRepo.scala +++ b/src/main/scala/datastore/ElasticsearchRepo.scala @@ -8,7 +8,7 @@ import com.sksamuel.elastic4s.ElasticDsl._ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import com.sksamuel.elastic4s.requests.searches.SearchResponse -import com.sksamuel.elastic4s.requests.searches.queries.{ExistsQuery, NestedQuery, Query, RangeQuery} +import com.sksamuel.elastic4s.requests.searches.queries.{DisMaxQuery, ExistsQuery, Fuzzy, FuzzyQuery, NestedQuery, Query, RangeQuery} import com.sksamuel.elastic4s.requests.searches.queries.compound.BoolQuery import com.sksamuel.elastic4s.requests.searches.queries.matches.{FieldWithOptionalBoost, MatchAllQuery, MatchQuery, MultiMatchQuery} import com.sksamuel.elastic4s.requests.searches.sort.{FieldSort, ScoreSort, Sort, SortOrder} @@ -73,8 +73,9 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 private def defaultingSortParam(orderDate:Option[String], orderBy:Option[SortOrder]): Sort = { orderDate match { + case Some("score")=>ScoreSort(orderBy.getOrElse(SortOrder.DESC)) case Some(field)=>FieldSort(field, order = orderBy.getOrElse(SortOrder.DESC)) - case None=>ScoreSort(orderBy.getOrElse(SortOrder.DESC)) + case None=>FieldSort("webPublicationDate", order=orderBy.getOrElse(SortOrder.DESC)) } } @@ -163,7 +164,7 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 Some(limitToChannelQuery(selectedChannel)), queryString.map(MultiMatchQuery(_, fields = fieldsToQuery)), atomId.map(MatchQuery("atomIds.id", _)), - tagIds.map(tags=>BoolQuery(must=tags.map(MatchQuery("tags", _)))) , + tagIds.map(tags=>BoolQuery(should=tags.map(MatchQuery("tags", _)))) , excludeTags.map(tags=>BoolQuery(not=Seq(BoolQuery(should=tags.map(MatchQuery("tags", _)))))), sectionIds.map(s=>BoolQuery(should=s.map(MatchQuery("sectionId", _)))), excludeSections.map(s=>BoolQuery(not=Seq(BoolQuery(should=s.map(MatchQuery("sectionId", _)))))) @@ -183,8 +184,21 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 } } - private def tagQueryParams(maybeTagId:Option[String], maybeSection:Option[String], tagType:Option[String]):Seq[Query] = { + private def tagQueryParams(maybeTagId:Option[String], maybeSection:Option[String], + tagType:Option[String], maybeCategory:Option[String], + maybeReferences: Option[String], queryString:Option[String], fuzziness:Option[String]):Seq[Query] = { Seq( + queryString.map(qs=>{ + if(fuzziness.getOrElse("AUTO") != "OFF") { + //Why DisMax here? Because we want to include exact-matches as well, if they are relevant. E.g. FuzzyQuery on "politics" returns no results! + DisMaxQuery(Seq( + FuzzyQuery("webTitle", qs, fuzziness), + MatchQuery("webTitle", qs) + )) + } else { + MatchQuery("webTitle", qs) + } + }), maybeTagId.map(MatchQuery("id", _)), maybeSection.map(MatchQuery("sectionId", _)), tagType.map({ @@ -192,18 +206,26 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 ExistsQuery("podcast") case tp: String => MatchQuery("type", tp) - }) + }), + maybeSection.map(s=>termQuery("section",s)), + maybeCategory.map(cat=>termQuery("tagCategories", cat)), + maybeReferences.map(ref=>termQuery("references", ref)) //this is an object field - check how terming works!! ).collect({ case Some(param) => param }) } - private def buildTagQuery(maybeTagId:Option[String], maybeSection:Option[String], tagType:Option[String]) = { - val base = search("tag") - val params = tagQueryParams(maybeTagId, maybeSection, tagType) + private def buildTagQuery(maybeTagId:Option[String], + maybeSection:Option[String], + tagType:Option[String], maybeQuery:Option[String], + maybeFuzziness:Option[String], + maybeCategory:Option[String], maybeReferences:Option[String]) = { + val baseSearch = search("tag") + + val params = tagQueryParams(maybeTagId, maybeSection, tagType, maybeCategory, maybeReferences, maybeQuery, maybeFuzziness) if(params.isEmpty) { - base + baseSearch } else { - base.query(BoolQuery(must=params)) + baseSearch.query(BoolQuery(must=params)) } } @@ -217,7 +239,16 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 //FIXME: tagsForList / marshalledTags could be DRY'd out a bit - override def marshalledTags(maybeTagId:Option[String], maybeSection: Option[String], tagType:Option[String], orderBy: Option[SortOrder], limit: Option[Int], cursor: Option[String]): Future[Edge[Tag]] = { + override def marshalledTags(maybeQuery:Option[String], + maybeFuzziness:Option[String], + maybeTagId:Option[String], + maybeSection: Option[String], + tagType:Option[String], + maybeCategory:Option[String], + maybeReferences:Option[String], + orderBy: Option[SortOrder], + limit: Option[Int], + cursor: Option[String]): Future[Edge[Tag]] = { val pageSize = limit.getOrElse(defaultPageSize) val sortParam = if(maybeSection.isDefined || tagType.isDefined) { @@ -229,7 +260,7 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 Edge.decodeCursor(cursor) match { case Right(maybeCursor)=> client.execute { - buildTagQuery(maybeTagId, maybeSection, tagType) + buildTagQuery(maybeTagId, maybeSection, tagType, maybeQuery, maybeFuzziness, maybeCategory, maybeReferences) .sortBy(sortParam) .limit(pageSize) .searchAfter(maybeCursor) @@ -239,25 +270,10 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 } } - override def tagsForList(tagIdList:Seq[String], maybeSection: Option[String], tagType:Option[String]):Future[Seq[Tag]] = { - val tagIdMatches = tagIdList.map(MatchQuery("id", _)) - - client.execute { - val restrictions = tagQueryParams(None, maybeSection, tagType) - - if(restrictions.nonEmpty) { - search("tag").query( - BoolQuery( - must=restrictions :+ BoolQuery(should=tagIdMatches) - ) - ) - } else { - search("tag").query(BoolQuery(should=tagIdMatches)) - } - - } flatMap { response=> + private def marshalTags(response:Future[Response[SearchResponse]]):Future[Seq[Tag]] = { + response flatMap { response=> if(response.isError) { - logger.error(s"Could not make query for tags ${tagIdList}: ${response.error}") + logger.error(s"Could not make query for tags: ${response.error}") Future.failed(response.error.asException) } else { Future(response.result.hits.hits.map(hit=> @@ -272,6 +288,25 @@ class ElasticsearchRepo(endpoint:ElasticNodeEndpoint, val defaultPageSize:Int=20 } } + override def tagsForList(tagIdList:Seq[String], maybeSection: Option[String], tagType:Option[String], maybeCategory:Option[String], maybeReferences:Option[String]):Future[Seq[Tag]] = { + val tagIdMatches = tagIdList.map(MatchQuery("id", _)) + + val response = client.execute { + val restrictions = tagQueryParams(None, maybeSection, tagType, maybeCategory, maybeReferences, None, None) + + if(restrictions.nonEmpty) { + search("tag").query( + BoolQuery( + must=restrictions :+ BoolQuery(should=tagIdMatches) + ) + ) + } else { + search("tag").query(BoolQuery(should=tagIdMatches)) + } + } + marshalTags(response) + } + private def findAtoms(atomIds: Option[Seq[String]], queryString: Option[String], queryFields: Option[Seq[String]], atomType: Option[String], revisionBefore: Option[Long], revisionAfter: Option[Long],