diff --git a/config/vufind/EDS.ini b/config/vufind/EDS.ini index 9080d6604edc545cbccddfbf0d3cbc3d468eaaab..13d32064dabf383bbf65667637473295e4290003 100644 --- a/config/vufind/EDS.ini +++ b/config/vufind/EDS.ini @@ -40,6 +40,13 @@ retain_filters_by_default = true ; This is the timeout in seconds when communicating with the EDS server. timeout = 120 +; This is the HTTP method to use for EDS search operations; legal options are GET +; or POST (the default). GET and POST use significantly different APIs, so it +; may be useful to switch between methods for troubleshooting purposes. Under +; normal circumstances, POST is preferred because GET has some character length +; limits that do not affect the POST-based API. +search_http_method = POST + ; The following two sections can be used to associate specific recommendations ; modules with specific search types defined in the [Basic_Searches] section ; below. For all the details on how these sections work, see the comments above diff --git a/module/VuFind/src/VuFind/Search/EDS/Params.php b/module/VuFind/src/VuFind/Search/EDS/Params.php index bf14d168262058e7c34e86c4e2c569640ed569a4..cb20f35104190320549ea600dfdd7f76ebf15f10 100644 --- a/module/VuFind/src/VuFind/Search/EDS/Params.php +++ b/module/VuFind/src/VuFind/Search/EDS/Params.php @@ -27,7 +27,6 @@ */ namespace VuFind\Search\EDS; -use VuFindSearch\Backend\EDS\SearchRequestModel as SearchRequestModel; use VuFindSearch\ParamBag; /** @@ -172,11 +171,8 @@ class Params extends \VuFind\Search\Base\Params // Loop through all filters and add appropriate values to request: foreach ($filterList as $filterArray) { foreach ($filterArray as $filt) { - $safeValue = SearchRequestModel::escapeSpecialCharacters( - $filt['value'] - ); // Standard case: - $fq = "{$filt['field']}:{$safeValue}"; + $fq = "{$filt['field']}:{$filt['value']}"; $params->add('filters', $fq); } } @@ -184,11 +180,8 @@ class Params extends \VuFind\Search\Base\Params if (!empty($hiddenFilterList)) { foreach ($hiddenFilterList as $field => $hiddenFilters) { foreach ($hiddenFilters as $value) { - $safeValue = SearchRequestModel::escapeSpecialCharacters( - $value - ); // Standard case: - $hfq = "{$field}:{$safeValue}"; + $hfq = "{$field}:{$value}"; $params->add('filters', $hfq); } } diff --git a/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php index 927df1696c2331f69f10ff42822d5b1c5262a73b..33508ebac0b5b2e3446013536fdd3c9a5f0ec671 100644 --- a/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php +++ b/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php @@ -137,7 +137,9 @@ class EdsBackendFactory implements FactoryInterface protected function createConnector() { $options = [ - 'timeout' => $this->edsConfig->General->timeout ?? 120 + 'timeout' => $this->edsConfig->General->timeout ?? 120, + 'search_http_method' => $this->edsConfig->General->search_http_method + ?? 'POST' ]; // Build HTTP client: $client = $this->serviceLocator->get(\VuFindHttp\HttpService::class) diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php index facbbafc6103a54213fe19c71a030b4b242a1240..4dd3be3a6fdfdebac2e4c0be29e15f1569fe4067 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php @@ -81,6 +81,13 @@ abstract class Base */ protected $contentType = 'application/json'; + /** + * Search HTTP method + * + * @var string + */ + protected $searchHttpMethod = 'POST'; + /** * Constructor * @@ -91,6 +98,7 @@ abstract class Base * <ul> * <li>debug - boolean to control debug mode</li> * <li>orgid - Organization making calls to the EDS API </li> + * <li>search_http_method - HTTP method for search API calls</li> * </ul> */ public function __construct($settings = []) @@ -104,6 +112,8 @@ abstract class Base case 'orgid': $this->orgId = $value; break; + case 'search_http_method': + $this->searchHttpMethod = $value; } } } @@ -203,11 +213,15 @@ abstract class Base public function search($query, $authenticationToken, $sessionToken) { // Query String Parameters - $qs = $query->convertToQueryStringParameterArray(); - $this->debugPrint('Query: ' . print_r($qs, true)); + $method = $this->searchHttpMethod; + $json = $method === 'GET' ? null : $query->convertToSearchRequestJSON(); + $qs = $method === 'GET' ? $query->convertToQueryStringParameterArray() : []; + $this->debugPrint( + 'Query: ' . ($method === 'GET' ? print_r($qs, true) : $json) + ); $url = $this->edsApiHost . '/search'; $headers = $this->setTokens($authenticationToken, $sessionToken); - return $this->call($url, $headers, $qs); + return $this->call($url, $headers, $qs, $method, $json); } /** diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php index d761481e70fc61ad5fda351856e2547008431d8c..240fde8cada6a4a80cc600df0232feef344e92a5 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php @@ -82,25 +82,20 @@ class QueryBuilder * @param Query $query Query to convert * @param string $operator Operator to apply * - * @return string + * @return array */ protected function queryToEdsQuery(Query $query, $operator = 'AND') { $expression = $query->getString(); - $expression = SearchRequestModel::escapeSpecialCharacters($expression); $fieldCode = ($query->getHandler() == 'AllFields') ? '' : $query->getHandler(); //fieldcode // Special case: default search if (empty($fieldCode) && empty($expression)) { - return $this->defaultQuery; - } - if (!empty($fieldCode)) { - $expression = $fieldCode . ':' . $expression; - } - if (!empty($operator)) { - $expression = $operator . ',' . $expression; + $expression = $this->defaultQuery; } - return $expression; + return json_encode( + ['term' => $expression, 'field' => $fieldCode, 'bool' => $operator] + ); } /// Internal API diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php index bc69974b1f702cb21bd637c598c0e6ebf47b3e6b..4f5e1647671646222b9f90a43d5a6034efaae909 100644 --- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php +++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php @@ -217,11 +217,25 @@ class SearchRequestModel { $qs = []; if (isset($this->query) && 0 < sizeof($this->query)) { - $qs['query-x'] = $this->query; + $formatQuery = function ($json) { + $query = json_decode($json, true); + $queryString = empty($query['bool']) + ? '' : ($query['bool'] . ','); + if (!empty($query['field'])) { + $queryString .= $query['field'] . ':'; + } + $queryString .= static::escapeSpecialCharacters($query['term']); + return $queryString; + }; + $qs['query-x'] = array_map($formatQuery, $this->query); } if (isset($this->facetFilters) && 0 < sizeof($this->facetFilters)) { - $qs['facetfilter'] = $this->facetFilters; + $formatFilter = function ($raw) { + list($field, $value) = explode(':', $raw, 2); + return $field . ':' . static::escapeSpecialCharacters($value); + }; + $qs['facetfilter'] = array_map($formatFilter, $this->facetFilters); } if (isset($this->limiters) && 0 < sizeof($this->limiters)) { @@ -233,7 +247,7 @@ class SearchRequestModel } if (isset($this->includeFacets)) { - $qs['includefacets'] = $this->includeFacets; + $qs['includefacets'] = $this->includeFacets; } if (isset($this->sort)) { @@ -266,6 +280,95 @@ class SearchRequestModel return $qs; } + /** + * Converts properties to a search request JSON document to send to the EdsAPI + * + * @return string + */ + public function convertToSearchRequestJSON() + { + $json = new \stdClass(); + $json->SearchCriteria = new \stdClass(); + $json->RetrievalCriteria = new \stdClass(); + $json->Actions = null; + if (isset($this->query) && 0 < sizeof($this->query)) { + $json->SearchCriteria->Queries = []; + foreach ($this->query as $queryJson) { + $query = json_decode($queryJson, true); + $queryObj = new \stdClass(); + if (!empty($query['bool'])) { + $queryObj->BooleanOperator = $query['bool']; + } + if (!empty($query['field'])) { + $queryObj->FieldCode = $query['field']; + } + $queryObj->Term = $query['term']; + $json->SearchCriteria->Queries[] = $queryObj; + } + } + + if (isset($this->facetFilters) && 0 < sizeof($this->facetFilters)) { + $json->SearchCriteria->FacetFilters = []; + foreach ($this->facetFilters as $currentFilter) { + list($id, $filter) = explode(',', $currentFilter, 2); + list($field, $value) = explode(':', $filter, 2); + $filterObj = new \stdClass(); + $filterObj->FilterId = $id; + $valueObj = new \stdClass(); + $valueObj->Id = $field; + $valueObj->Value = $value; + $filterObj->FacetValues = [$valueObj]; + $json->SearchCriteria->FacetFilters[] = $filterObj; + } + } + + if (isset($this->limiters) && 0 < sizeof($this->limiters)) { + $json->SearchCriteria->Limiters = []; + foreach ($this->limiters as $limiter) { + list($id, $values) = explode(':', $limiter, 2); + $limiterObj = new \stdClass(); + $limiterObj->Id = $id; + $limiterObj->Values = explode(',', $values); + $json->SearchCriteria->Limiters[] = $limiterObj; + } + } + + if (isset($this->actions) && 0 < sizeof($this->actions)) { + $json->Actions = $this->actions; + } + + $json->SearchCriteria->IncludeFacets = $this->includeFacets ?? 'y'; + + if (isset($this->sort)) { + $json->SearchCriteria->Sort = $this->sort; + } + + if (isset($this->searchMode)) { + $json->SearchCriteria->SearchMode = $this->searchMode; + } + + if (isset($this->expanders) && 0 < sizeof($this->expanders)) { + $json->SearchCriteria->Expanders = $this->expanders; + } + + if (isset($this->view)) { + $json->RetrievalCriteria->View = $this->view; + } + + if (isset($this->resultsPerPage)) { + $json->RetrievalCriteria->ResultsPerPage = intval($this->resultsPerPage); + } + + if (isset($this->pageNumber)) { + $json->RetrievalCriteria->PageNumber = intval($this->pageNumber); + } + + $highlightVal = isset($this->highlight) && $this->highlight ? 'y' : 'n'; + $json->RetrievalCriteria->Highlight = $highlightVal; + + return json_encode($json, JSON_PRETTY_PRINT); + } + /** * Verify whether or not a string ends with certain characters * diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php index 13f3d3e9f698ed4793ede631bd4bee04af1db478..5fc2cf24a3951837a80c35b1a0ef2e5e5b879274 100644 --- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php +++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php @@ -42,6 +42,21 @@ use VuFindSearch\Backend\EDS\QueryBuilder; */ class QueryBuilderTest extends TestCase { + /** + * Given a response, decode the JSON query objects for easier reading. + * + * @param array $response Raw response + * + * @return array + */ + protected function decodeResponse($response) + { + foreach ($response as $i => $raw) { + $response[$i] = json_decode($raw, true); + } + return $response; + } + /** * Test special case for blank queries. * @@ -51,11 +66,19 @@ class QueryBuilderTest extends TestCase { $qb = new QueryBuilder(); $params = $qb->build(new \VuFindSearch\Query\Query()); + $response = $params->getArrayCopy(); + $response['query'] = $this->decodeResponse($response['query']); $this->assertEquals( [ - 'query' => ['(FT yes) OR (FT no)'] + 'query' => [ + [ + 'term' => '(FT yes) OR (FT no)', + 'field' => null, + 'bool' => 'AND', + ], + ] ], - $params->getArrayCopy() + $response ); } @@ -68,11 +91,23 @@ class QueryBuilderTest extends TestCase { // Set up an array of expected inputs (serialized objects) and outputs // (queries): - // @codingStandardsIgnoreStart $tests = [ - ['advanced', ['AND,cheese', 'AND,SU:test']] + [ + 'advanced', + [ + [ + 'term' => 'cheese', + 'field' => null, + 'bool' => 'AND', + ], + [ + 'term' => 'test', + 'field' => 'SU', + 'bool' => 'AND', + ] + ] + ] ]; - // @codingStandardsIgnoreEnd $qb = new QueryBuilder(); foreach ($tests as $test) { @@ -83,7 +118,9 @@ class QueryBuilderTest extends TestCase ) ); $response = $qb->build($q); - $this->assertEquals($output, $response->get('query')); + $this->assertEquals( + $output, $this->decodeResponse($response->get('query')) + ); } } }