Skip to content

Commit

Permalink
TASK: 0.1 release
Browse files Browse the repository at this point in the history
  • Loading branch information
skurfuerst committed Jan 26, 2021
1 parent 3282adb commit 922aac3
Show file tree
Hide file tree
Showing 15 changed files with 572 additions and 101 deletions.
65 changes: 56 additions & 9 deletions Classes/Query/BooleanQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,86 @@

namespace Sandstorm\LightweightElasticsearch\Query;


use Neos\Eel\ProtectedContextAwareInterface;
use Neos\Flow\Annotations as Flow;

class BooleanQueryBuilder implements ProtectedContextAwareInterface
/**
* See https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html for a reference on the fields
* @Flow\Proxy(false)
*/
class BooleanQueryBuilder implements ProtectedContextAwareInterface, SearchQueryBuilderInterface
{
protected array $query = [];
private function __construct()
{
}

public function should(SearchQueryBuilderInterface $query): self
public static function create(): self
{
$this->query['should'][] = $query->buildQuery();
return $this;
return new self();
}

private array $query = [
'bool' => []
];

/**
* Add a query to the "must" part of the Bool query. This query must ALWAYS match for a document to be included in the results.
*
* @param SearchQueryBuilderInterface $query
* @return $this
*/
public function must(SearchQueryBuilderInterface $query): self
{
$this->query['must'][] = $query->buildQuery();
$this->query['bool']['must'][] = $query->buildQuery();
return $this;
}

/**
* Add a query to the "should" part of the Bool query.
*
* The "minimum_should_match" property defines the number or percentage of should clauses returned documents must match.
* If the bool query includes at least one should clause and no must or filter clauses, the default value is 1. Otherwise, the default value is 0.
*
* @param SearchQueryBuilderInterface $query
* @return $this
*/
public function should(SearchQueryBuilderInterface $query): self
{
$this->query['bool']['should'][] = $query->buildQuery();
return $this;
}

/**
* Add a query to the "must_not" part of the Bool query. This query must NEVER match for a document to be included in the results.
*
* @param SearchQueryBuilderInterface $query
* @return $this
*/
public function mustNot(SearchQueryBuilderInterface $query): self
{
$this->query['must_not'][] = $query->buildQuery();
$this->query['bool']['must_not'][] = $query->buildQuery();
return $this;
}

/**
* Add a query to the "filter" part of the Bool query. This query must ALWAYS match for a document to be included in the results; and ranking information is discarded.
*
* @param SearchQueryBuilderInterface $query
* @return $this
*/
public function filter(SearchQueryBuilderInterface $query): self
{
$this->query['filter'][] = $query->buildQuery();
$this->query['bool']['filter'][] = $query->buildQuery();
return $this;
}

public function allowsCallOfMethod($methodName)
{
return true;
}

public function buildQuery(): array
{
return $this->query;
}
}
24 changes: 11 additions & 13 deletions Classes/Query/ElasticsearchHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@
use Neos\ContentRepository\Search\Search\QueryBuilderInterface;

/**
* Eel Helper to start search queries
* Eel Helper to write search queries.
* Example:
*
* Elasticsearch.createRequest('indexnames')
* Elasticsearch.createRequest(site)
* .query(
* Elasticsearch.createBooleanQuery()
* .should(Elasticsearch.createNeosFulltextQuery(site).fulltext(...).filter(Elasticsearch.createTermQuery("key", "value")))
* .should(Elasticsearch.createNeosFulltextQuery(site).fulltext("mein Suchstring")))
* .should(...)
* .should(...)
* .must(...)
* )
* .aggregation()
* .execute()
*/
class ElasticsearchHelper implements ProtectedContextAwareInterface
Expand All @@ -39,30 +41,26 @@ class ElasticsearchHelper implements ProtectedContextAwareInterface
* @param NodeInterface $contextNode
* @return QueryBuilderInterface
*/
public function createRequest(): SearchRequestBuilder
public function createRequest(NodeInterface $contextNode = null, array $additionalIndices = []): SearchRequestBuilder
{
return new SearchRequestBuilder();
return new SearchRequestBuilder($contextNode, $additionalIndices);
}

public function createBooleanQuery(): BooleanQueryBuilder
{
return new BooleanQueryBuilder();
return BooleanQueryBuilder::create();
}

public function createNeosFulltextQuery(NodeInterface $contextNode): NeosFulltextQueryBuilder
{
return new NeosFulltextQueryBuilder($contextNode);
return NeosFulltextQueryBuilder::create($contextNode);
}

public function createTermQuery(string $fieldName, $value): TermQueryBuilder
{
return new TermQueryBuilder($fieldName, $value);
return TermQueryBuilder::create($fieldName, $value);
}

/**
* @param string $methodName
* @return boolean
*/
public function allowsCallOfMethod($methodName)
{
return true;
Expand Down
76 changes: 72 additions & 4 deletions Classes/Query/NeosFulltextQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,86 @@

namespace Sandstorm\LightweightElasticsearch\Query;


use Neos\Eel\ProtectedContextAwareInterface;
use Neos\Flow\Annotations as Flow;
use Neos\ContentRepository\Domain\Model\NodeInterface;

class NeosFulltextQueryBuilder extends BooleanQueryBuilder
/**
* Do a fulltext search in Neos nodes, by searching neos_fulltext appropriately.
*
* Also allows to further restrict the result set by calling filter().
*
* @Flow\Proxy(false)
*/
class NeosFulltextQueryBuilder implements SearchQueryBuilderInterface, ProtectedContextAwareInterface
{
public function __construct(NodeInterface $contextNode)
protected BooleanQueryBuilder $boolQuery;

public static function create(NodeInterface $contextNode): self
{
return new self($contextNode);
}

private function __construct(NodeInterface $contextNode)
{
$this->boolQuery = BooleanQueryBuilder::create()
// on indexing, the neos_parent_path is tokenized to contain ALL parent path parts,
// e.g. /foo, /foo/bar/, /foo/bar/baz; to speed up matching.. That's why we use a simple "term" filter here.
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-term-filter.html
// another term filter against the path allows the context node itself to be found
->filter(
BooleanQueryBuilder::create()
->should(TermQueryBuilder::create('neos_parent_path', $contextNode->getPath()))
->should(TermQueryBuilder::create('neos_path', $contextNode->getPath()))
)
->filter(
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-terms-filter.html
TermsQueryBuilder::create('neos_workspace', array_unique(['live', $contextNode->getContext()->getWorkspace()->getName()]))
);
}

/**
* Specify the fulltext query string to be used for searching.
*
* NOTE: this method can be called multiple times; this corresponds to an "AND" of the queries (i.e. BOTH must match to include the result).
* I am not yet sure if this is a good idea or not :-)
*
* @param string|null $query
* @return $this
*/
public function fulltext(string $query = null): self
{
$this->boolQuery->must(SimpleQueryStringBuilder::create($query ?? '')->fields([
'neos_fulltext.h1^5',
'neos_fulltext.h2^4',
'neos_fulltext.h3^3',
'neos_fulltext.h4^2',
'neos_fulltext.h5^1',
'neos_fulltext.h6',
'neos_fulltext.text',
]));
return $this;
}

/**
* Add a query to the "filter" part of the query. This query must ALWAYS match for a document to be included in the results; and ranking information is discarded.
*
* @param SearchQueryBuilderInterface $query
* @return $this
*/
public function filter(SearchQueryBuilderInterface $query): self
{
$this->boolQuery->filter($query);
return $this;
}

public function fulltext(string $query)
public function buildQuery(): array
{
return $this->boolQuery->buildQuery();
}

public function allowsCallOfMethod($methodName)
{
return true;
}
}
53 changes: 44 additions & 9 deletions Classes/Query/Result/SearchResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@

namespace Sandstorm\LightweightElasticsearch\Query\Result;

use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Eel\ProtectedContextAwareInterface;
use Neos\Flow\Annotations as Flow;

class SearchResult implements \IteratorAggregate
/**
* Wrapper for all search results
*
* @Flow\Proxy(false)
*/
class SearchResult implements \IteratorAggregate, ProtectedContextAwareInterface, \Countable
{
public static function fromElasticsearchJsonResponse(array $response): self

private array $response;
private bool $isError;
private ?NodeInterface $contextNode;

public static function fromElasticsearchJsonResponse(array $response, NodeInterface $contextNode): self
{
return new SearchResult($response, false);
return new SearchResult($response, false, $contextNode);
}

public static function error(): self
{
return new SearchResult([], true);
}

private array $response;
private bool $isError;

private function __construct(array $response, bool $isError)
private function __construct(array $response, bool $isError, NodeInterface $contextNode = null)
{
$this->response = $response;
$this->isError = $isError;
$this->contextNode = $contextNode;
}

public function isError(): bool
Expand All @@ -32,8 +43,32 @@ public function isError(): bool

public function getIterator(): \Generator
{
foreach ($this->response['hits']['hits'] as $hit) {
yield SearchResultDocument::fromElasticsearchJsonResponse($hit);
if (isset($this->response['hits']['hits'])) {
foreach ($this->response['hits']['hits'] as $hit) {
yield SearchResultDocument::fromElasticsearchJsonResponse($hit, $this->contextNode);
}
}
}

public function allowsCallOfMethod($methodName)
{
return true;
}

public function total(): int
{
if (!isset($this->response['hits']['total']['value'])) {
return 0;
}
return $this->response['hits']['total']['value'];

}

public function count()
{
if (isset($this->response['hits']['hits'])) {
return count($this->response['hits']['hits']);
}
return 0;
}
}
42 changes: 40 additions & 2 deletions Classes/Query/Result/SearchResultDocument.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,48 @@
namespace Sandstorm\LightweightElasticsearch\Query\Result;


class SearchResultDocument
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Eel\ProtectedContextAwareInterface;

class SearchResultDocument implements ProtectedContextAwareInterface
{
protected $hit;
private ?NodeInterface $contextNode;

protected function __construct(array $hit, NodeInterface $contextNode = null)
{
$this->hit = $hit;
$this->contextNode = $contextNode;
}

public static function fromElasticsearchJsonResponse(array $hit, NodeInterface $contextNode = null): self
{
return new static($hit, $contextNode);
}

public function loadNode(): ?NodeInterface
{
$nodePath = $this->hit['_source']['neos_path'];

if (is_array($nodePath)) {
$nodePath = current($nodePath);
}

return $this->contextNode->getNode($nodePath);
}

public function getFullSearchHit()
{
return $this->hit;
}

public function property(string $key)
{
return $this->hit['_source'][$key] ?? null;
}

public static function fromElasticsearchJsonResponse(array $hit): self
public function allowsCallOfMethod($methodName)
{
return true;
}
}
Loading

0 comments on commit 922aac3

Please sign in to comment.