1
0
Fork 0

optimizations for neighbors queries

This commit is contained in:
jsteemann 2016-06-20 18:37:46 +02:00
parent a0b67ae7ad
commit 55fce39574
12 changed files with 465 additions and 41 deletions

View File

@ -3630,7 +3630,7 @@ void arangodb::aql::optimizeTraversalsRule(Optimizer* opt,
auto outVariable = traversal->edgeOutVariable();
if (outVariable != nullptr &&
varsUsedLater.find(outVariable) == varsUsedLater.end()) {
// traversal vertex outVariable not used later
// traversal edge outVariable not used later
traversal->setEdgeOutput(nullptr);
modified = true;
}
@ -3638,7 +3638,7 @@ void arangodb::aql::optimizeTraversalsRule(Optimizer* opt,
outVariable = traversal->pathOutVariable();
if (outVariable != nullptr &&
varsUsedLater.find(outVariable) == varsUsedLater.end()) {
// traversal vertex outVariable not used later
// traversal path outVariable not used later
traversal->setPathOutput(nullptr);
modified = true;
}
@ -3655,6 +3655,52 @@ void arangodb::aql::optimizeTraversalsRule(Optimizer* opt,
n->walk(&finder);
}
}
// now check if we can use an optimized version of the neighbors search...
if (!arangodb::ServerState::instance()->isRunningInCluster()) {
for (auto const& n : tNodes) {
TraversalNode* traversal = static_cast<TraversalNode*>(n);
if (traversal->edgeOutVariable() != nullptr ||
traversal->pathOutVariable() != nullptr) {
// traversal produces edges or paths
continue;
}
if (traversal->maxDepth() > 100) {
// neighbors search is recursive... do not use recursive version if
// depth is potentially high
continue;
}
if (!traversal->expressions()->empty()) {
// traversal has filter expressions
continue;
}
if (!traversal->allDirectionsEqual()) {
// not all directions are equal
continue;
}
TraversalOptions const* options = traversal->options();
TRI_ASSERT(options != nullptr);
if (options->uniqueVertices != traverser::TraverserOptions::GLOBAL ||
options->uniqueEdges != traverser::TraverserOptions::NONE) {
// neighbors search is hard-coded to global vertex uniqueness
continue;
}
if (!options->useBreadthFirst) {
continue;
}
traversal->specializeToNeighborsSearch();
modified = true;
}
}
opt->addPlan(plan, rule, modified);
}

View File

@ -28,7 +28,10 @@
#include "Aql/ExecutionPlan.h"
#include "Aql/Functions.h"
#include "Basics/ScopeGuard.h"
#include "Basics/StringRef.h"
#include "Cluster/ClusterTraverser.h"
#include "Utils/OperationCursor.h"
#include "Utils/Transaction.h"
#include "VocBase/SingleServerTraverser.h"
#include "V8/v8-globals.h"
@ -261,6 +264,12 @@ int TraversalBlock::initializeCursor(AqlItemBlock* items, size_t pos) {
/// @brief read more paths
bool TraversalBlock::morePaths(size_t hint) {
DEBUG_BEGIN_BLOCK();
TraversalNode const* planNode = static_cast<TraversalNode const*>(getPlanNode());
if (planNode->_specializedNeighborsSearch) {
return false;
}
freeCaches();
_posInPaths = 0;
if (!_traverser->hasMore()) {
@ -268,7 +277,7 @@ bool TraversalBlock::morePaths(size_t hint) {
_engine->_stats.filtered += _traverser->getAndResetFilteredPaths();
return false;
}
if (usesVertexOutput()) {
_vertices.reserve(hint);
}
@ -315,6 +324,8 @@ bool TraversalBlock::morePaths(size_t hint) {
/// @brief skip the next paths
size_t TraversalBlock::skipPaths(size_t hint) {
DEBUG_BEGIN_BLOCK();
TRI_ASSERT(!static_cast<TraversalNode const*>(getPlanNode())->_specializedNeighborsSearch);
freeCaches();
_posInPaths = 0;
if (!_traverser->hasMore()) {
@ -331,6 +342,9 @@ void TraversalBlock::initializePaths(AqlItemBlock const* items) {
// No Initialization required.
return;
}
TraversalNode const* planNode = static_cast<TraversalNode const*>(getPlanNode());
if (!_useRegister) {
if (!_usedConstant) {
_usedConstant = true;
@ -341,22 +355,36 @@ void TraversalBlock::initializePaths(AqlItemBlock const* items) {
"Only id strings or objects with "
"_id are allowed");
} else {
_traverser->setStartVertex(_vertexId);
if (planNode->_specializedNeighborsSearch) {
// fetch neighbor nodes
neighbors(_vertexId);
} else {
_traverser->setStartVertex(_vertexId);
}
}
}
} else {
AqlValue const& in = items->getValueReference(_pos, _reg);
if (in.isObject()) {
try {
std::string idString = _trx->extractIdString(in.slice());
_traverser->setStartVertex(idString);
if (planNode->_specializedNeighborsSearch) {
// fetch neighbor nodes
neighbors(_trx->extractIdString(in.slice()));
} else {
_traverser->setStartVertex(_trx->extractIdString(in.slice()));
}
}
catch (...) {
// _id or _key not present... ignore this error and fall through
}
} else if (in.isString()) {
_vertexId = in.slice().copyString();
_traverser->setStartVertex(_vertexId);
if (planNode->_specializedNeighborsSearch) {
// fetch neighbor nodes
neighbors(_vertexId);
} else {
_traverser->setStartVertex(_vertexId);
}
} else {
_engine->getQuery()->registerWarning(
TRI_ERROR_BAD_PARAMETER, "Invalid input for traversal: Only "
@ -511,3 +539,137 @@ size_t TraversalBlock::skipSome(size_t atLeast, size_t atMost) {
return atMost;
DEBUG_END_BLOCK();
}
/// @brief optimized version of neighbors search, must properly implement this
void TraversalBlock::neighbors(std::string const& startVertex) {
std::unordered_set<VPackSlice, basics::VelocyPackHelper::VPackStringHash, basics::VelocyPackHelper::VPackStringEqual> visited;
std::vector<VPackSlice> result;
result.reserve(1000);
TransactionBuilderLeaser builder(_trx);
builder->add(VPackValue(startVertex));
std::vector<VPackSlice> startVertices;
startVertices.emplace_back(builder->slice());
visited.emplace(builder->slice());
TRI_edge_direction_e direction = TRI_EDGE_ANY;
std::string collectionName;
traverser::TraverserOptions const* options = _traverser->options();
if (options->getCollection(0, collectionName, direction)) {
runNeighbors(startVertices, visited, result, direction, 1);
}
TRI_doc_mptr_t mptr;
_vertices.clear();
_vertices.reserve(result.size());
for (auto const& it : result) {
VPackValueLength l;
char const* p = it.getString(l);
StringRef ref(p, l);
size_t pos = ref.find('/');
if (pos == std::string::npos) {
// invalid id
continue;
}
int res = _trx->documentFastPathLocal(ref.substr(0, pos).toString(), ref.substr(pos + 1).toString(), &mptr);
if (res != TRI_ERROR_NO_ERROR) {
continue;
}
_vertices.emplace_back(AqlValue(mptr.vpack()));
}
}
/// @brief worker for neighbors() function
void TraversalBlock::runNeighbors(std::vector<VPackSlice> const& startVertices,
std::unordered_set<VPackSlice, basics::VelocyPackHelper::VPackStringHash, basics::VelocyPackHelper::VPackStringEqual>& visited,
std::vector<VPackSlice>& distinct,
TRI_edge_direction_e direction,
uint64_t depth) {
std::vector<VPackSlice> nextDepth;
bool initialized = false;
TraversalNode const* node = static_cast<TraversalNode const*>(getPlanNode());
traverser::TraverserOptions const* options = _traverser->options();
TransactionBuilderLeaser builder(_trx);
size_t const n = options->collectionCount();
std::string collectionName;
Transaction::IndexHandle indexHandle;
std::vector<TRI_doc_mptr_t*> edges;
for (auto const& startVertex : startVertices) {
for (size_t i = 0; i < n; ++i) {
builder->clear();
if (!options->getCollectionAndSearchValue(i, startVertex.copyString(), collectionName, indexHandle, *builder.builder())) {
TRI_ASSERT(false);
}
std::shared_ptr<OperationCursor> cursor = _trx->indexScan(collectionName,
arangodb::Transaction::CursorType::INDEX, indexHandle,
builder->slice(), 0, UINT64_MAX, 1000, false);
if (cursor->failed()) {
continue;
}
edges.clear();
while (cursor->hasMore()) {
cursor->getMoreMptr(edges, 1000);
for (auto const& it : edges) {
VPackSlice edge(it->vpack());
VPackSlice v;
if (direction == TRI_EDGE_IN) {
v = Transaction::extractFromFromDocument(edge);
} else {
v = Transaction::extractToFromDocument(edge);
}
if (visited.find(v) == visited.end()) {
// we have not yet visited this vertex
if (depth >= node->minDepth()) {
distinct.emplace_back(v);
}
if (depth < node->maxDepth()) {
if (!initialized) {
nextDepth.reserve(64);
initialized = true;
}
nextDepth.emplace_back(v);
}
visited.emplace(v);
continue;
} else if (direction == TRI_EDGE_ANY) {
v = Transaction::extractToFromDocument(edge);
if (visited.find(v) == visited.end()) {
// we have not yet visited this vertex
if (depth >= node->minDepth()) {
distinct.emplace_back(v);
}
if (depth < node->maxDepth()) {
if (!initialized) {
nextDepth.reserve(64);
initialized = true;
}
nextDepth.emplace_back(v);
}
visited.emplace(v);
}
}
}
}
}
}
if (!nextDepth.empty()) {
runNeighbors(nextDepth, visited, distinct, direction, depth + 1);
}
}

View File

@ -26,6 +26,7 @@
#include "Aql/ExecutionBlock.h"
#include "Aql/TraversalNode.h"
#include "Basics/VelocyPackHelper.h"
#include "VocBase/Traverser.h"
namespace arangodb {
@ -146,6 +147,17 @@ class TraversalBlock : public ExecutionBlock {
/// @brief Executes the path-local filter expressions
/// Also determines the context
void executeFilterExpressions();
/// @brief optimized version of neighbors search, must properly implement this
void neighbors(std::string const& startVertex);
/// @brief worker for neighbors() function
void runNeighbors(std::vector<VPackSlice> const& startVertices,
std::unordered_set<VPackSlice, basics::VelocyPackHelper::VPackStringHash, basics::VelocyPackHelper::VPackStringEqual>& visited,
std::vector<VPackSlice>& distinct,
TRI_edge_direction_e direction,
uint64_t depth);
};
} // namespace arangodb::aql
} // namespace arangodb

View File

@ -135,7 +135,8 @@ TraversalNode::TraversalNode(ExecutionPlan* plan, size_t id,
_inVariable(nullptr),
_graphObj(nullptr),
_condition(nullptr),
_options(options) {
_options(options),
_specializedNeighborsSearch(false) {
TRI_ASSERT(_vocbase != nullptr);
TRI_ASSERT(direction != nullptr);
@ -268,7 +269,8 @@ TraversalNode::TraversalNode(
_directions(directions),
_graphObj(nullptr),
_condition(nullptr),
_options(options) {
_options(options),
_specializedNeighborsSearch(false) {
_graphJson = arangodb::basics::Json(arangodb::basics::Json::Array, edgeColls.size());
for (auto& it : edgeColls) {
@ -286,7 +288,8 @@ TraversalNode::TraversalNode(ExecutionPlan* plan,
_pathOutVariable(nullptr),
_inVariable(nullptr),
_graphObj(nullptr),
_condition(nullptr) {
_condition(nullptr),
_specializedNeighborsSearch(false) {
_minDepth =
arangodb::basics::JsonHelper::stringUInt64(base.json(), "minDepth");
_maxDepth =
@ -454,6 +457,7 @@ TraversalNode::TraversalNode(ExecutionPlan* plan,
_options = TraversalOptions(base);
}
_specializedNeighborsSearch = arangodb::basics::JsonHelper::getBooleanValue(base.json(), "specializedNeighborsSearch", false);
}
int TraversalNode::checkIsOutVariable(size_t variableId) const {
@ -469,6 +473,30 @@ int TraversalNode::checkIsOutVariable(size_t variableId) const {
return -1;
}
/// @brief check if all directions are equal
bool TraversalNode::allDirectionsEqual() const {
if (_directions.empty()) {
// no directions!
return false;
}
size_t const n = _directions.size();
TRI_edge_direction_e const expected = _directions[0];
for (size_t i = 1; i < n; ++i) {
if (_directions[i] != expected) {
return false;
}
}
return true;
}
void TraversalNode::specializeToNeighborsSearch() {
TRI_ASSERT(allDirectionsEqual());
TRI_ASSERT(!_directions.empty());
_specializedNeighborsSearch = true;
}
/// @brief toVelocyPack, for TraversalNode
void TraversalNode::toVelocyPackHelper(arangodb::velocypack::Builder& nodes,
bool verbose) const {
@ -539,6 +567,8 @@ void TraversalNode::toVelocyPackHelper(arangodb::velocypack::Builder& nodes,
}
}
}
nodes.add("specializedNeighborsSearch", VPackValue(_specializedNeighborsSearch));
nodes.add(VPackValue("traversalFlags"));
_options.toVelocyPack(nodes);
@ -584,6 +614,10 @@ ExecutionNode* TraversalNode::clone(ExecutionPlan* plan, bool withDependencies,
c->setPathOutput(pathOutVariable);
}
if (_specializedNeighborsSearch) {
c->specializeToNeighborsSearch();
}
cloneHelper(c, plan, withDependencies, withProperties);
return static_cast<ExecutionNode*>(c);

View File

@ -61,7 +61,7 @@ class SimpleTraverserExpression
/// @brief class TraversalNode
class TraversalNode : public ExecutionNode {
friend class ExecutionBlock;
friend class TraversalCollectionBlock;
friend class TraversalBlock;
friend class RedundantCalculationsReplacer;
/// @brief constructor with a vocbase and a collection name
@ -134,6 +134,14 @@ class TraversalNode : public ExecutionNode {
/// @brief getVariablesSetHere
std::vector<Variable const*> getVariablesSetHere() const override final {
std::vector<Variable const*> vars;
size_t const numVars =
(_vertexOutVariable != nullptr ? 1 : 0) +
(_edgeOutVariable != nullptr ? 1 : 0) +
(_pathOutVariable != nullptr ? 1 : 0);
vars.reserve(numVars);
if (_vertexOutVariable != nullptr) {
vars.emplace_back(_vertexOutVariable);
}
@ -211,6 +219,10 @@ class TraversalNode : public ExecutionNode {
void storeSimpleExpression(bool isEdgeAccess, size_t indexAccess,
AstNodeType comparisonType,
AstNode const* varAccess, AstNode* compareToNode);
bool allDirectionsEqual() const;
void specializeToNeighborsSearch();
/// @brief Returns a regerence to the simple traverser expressions
std::unordered_map<
@ -219,6 +231,11 @@ class TraversalNode : public ExecutionNode {
return &_expressions;
}
uint64_t minDepth() const { return _minDepth; }
uint64_t maxDepth() const { return _maxDepth; }
TraversalOptions const* options() const { return &_options; }
private:
/// @brief the database
TRI_vocbase_t* _vocbase;
@ -238,7 +255,7 @@ class TraversalNode : public ExecutionNode {
/// @brief input vertexId only used if _inVariable is unused
std::string _vertexId;
/// @brief input graphJson only used for serialisation & info
/// @brief input graphJson only used for serialization & info
arangodb::basics::Json _graphJson;
/// @brief The minimal depth included in the result
@ -270,6 +287,8 @@ class TraversalNode : public ExecutionNode {
/// @brief Options for traversals
TraversalOptions _options;
bool _specializedNeighborsSearch;
};
} // namespace arangodb::aql

View File

@ -575,6 +575,10 @@ DocumentDitch* Transaction::orderDitch(TRI_voc_cid_t cid) {
TRI_ASSERT(getStatus() == TRI_TRANSACTION_RUNNING ||
getStatus() == TRI_TRANSACTION_CREATED);
if (_ditchCache.cid == cid) {
return _ditchCache.ditch;
}
TRI_transaction_collection_t* trxCollection = TRI_GetCollectionTransaction(_trx, cid, TRI_TRANSACTION_READ);
if (trxCollection == nullptr) {
@ -592,6 +596,10 @@ DocumentDitch* Transaction::orderDitch(TRI_voc_cid_t cid) {
if (ditch == nullptr) {
THROW_ARANGO_EXCEPTION(TRI_ERROR_OUT_OF_MEMORY);
}
_ditchCache.cid = cid;
_ditchCache.ditch = ditch;
return ditch;
}

View File

@ -955,6 +955,16 @@ class Transaction {
//////////////////////////////////////////////////////////////////////////////
std::shared_ptr<TransactionContext> _transactionContext;
//////////////////////////////////////////////////////////////////////////////
/// @brief cache for last handed out DocumentDitch
//////////////////////////////////////////////////////////////////////////////
struct {
TRI_voc_cid_t cid = 0;
DocumentDitch* ditch = nullptr;
}
_ditchCache;
public:
//////////////////////////////////////////////////////////////////////////////

View File

@ -465,7 +465,7 @@ void SingleServerTraverser::EdgeGetter::getAllEdges(
if (!_traverser->edgeMatchesConditions(edge, depth)) {
if (_opts.uniqueEdges == TraverserOptions::UniquenessLevel::GLOBAL) {
// Insert a dummy to please the uniqueness
_traverser->_edges.emplace(id, nullptr);
_traverser->_edges.emplace(std::move(id), nullptr);
}
continue;
}
@ -489,3 +489,4 @@ void SingleServerTraverser::EdgeGetter::getAllEdges(
}
}
}

View File

@ -111,13 +111,6 @@ class SingleServerTraverser final : public Traverser {
SingleServerTraverser* _traverser;
//////////////////////////////////////////////////////////////////////////////
/// @brief Cache for indexes. Maps collectionName to Index
//////////////////////////////////////////////////////////////////////////////
std::unordered_map<std::string, std::pair<TRI_voc_cid_t, EdgeIndex*>>
_indexCache;
//////////////////////////////////////////////////////////////////////////////
/// @brief Traverser options
//////////////////////////////////////////////////////////////////////////////

View File

@ -104,7 +104,7 @@ size_t arangodb::traverser::TraverserOptions::collectionCount () const {
}
bool arangodb::traverser::TraverserOptions::getCollection(
size_t const index, std::string& name, TRI_edge_direction_e& dir) const {
size_t index, std::string& name, TRI_edge_direction_e& dir) const {
if (index >= _collections.size()) {
// No more collections stop now
return false;
@ -121,7 +121,7 @@ bool arangodb::traverser::TraverserOptions::getCollection(
bool arangodb::traverser::TraverserOptions::getCollectionAndSearchValue(
size_t index, std::string const& vertexId, std::string& name,
Transaction::IndexHandle& indexHandle, VPackBuilder& builder) {
Transaction::IndexHandle& indexHandle, VPackBuilder& builder) const {
if (index >= _collections.size()) {
// No more collections stop now
return false;

View File

@ -231,11 +231,11 @@ struct TraverserOptions {
size_t collectionCount() const;
bool getCollection(size_t const, std::string&, TRI_edge_direction_e&) const;
bool getCollection(size_t, std::string&, TRI_edge_direction_e&) const;
bool getCollectionAndSearchValue(size_t, std::string const&, std::string&,
arangodb::Transaction::IndexHandle&,
arangodb::velocypack::Builder&);
arangodb::velocypack::Builder&) const;
};
class Traverser {
@ -322,6 +322,8 @@ class Traverser {
_readDocuments = 0;
return tmp;
}
TraverserOptions const* options() { return &_opts; }
//////////////////////////////////////////////////////////////////////////////
/// @brief Prune the current path prefix. Do not evaluate it any further.

View File

@ -27,47 +27,184 @@
#include "Basics/Common.h"
#include "Basics/xxhash.h"
#include <velocypack/Slice.h>
#include <velocypack/Value.h>
namespace arangodb {
/// @brief a struct describing a C character array
/// not responsible for memory management!
struct StringRef {
StringRef() : data(""), length(0) {}
explicit StringRef(std::string const& str) : data(str.c_str()), length(str.size()) {}
explicit StringRef(char const* data) : data(data), length(strlen(data)) {}
StringRef(char const* data, size_t length) : data(data), length(length) {}
class StringRef {
public:
/// @brief create an empty StringRef
StringRef() : _data(""), _length(0) {}
bool operator==(StringRef const& other) const {
return (length == other.length && memcmp(data, other.data, length) == 0);
/// @brief create a StringRef from an std::string
explicit StringRef(std::string const& str) : _data(str.c_str()), _length(str.size()) {}
/// @brief create a StringRef from a null-terminated C string
explicit StringRef(char const* data) : _data(data), _length(strlen(data)) {}
/// @brief create a StringRef from a VPack slice (must be of type String)
explicit StringRef(arangodb::velocypack::Slice const& slice) : StringRef() {
arangodb::velocypack::ValueLength l;
_data = slice.getString(l);
_length = l;
}
bool operator==(std::string const& other) const {
return (length == other.size() && memcmp(data, other.c_str(), length) == 0);
/// @brief create a StringRef from a C string plus length
StringRef(char const* data, size_t length) : _data(data), _length(length) {}
/// @brief create a StringRef from another StringRef
StringRef(StringRef const& other)
: _data(other._data), _length(other._length) {}
/// @brief create a StringRef from another StringRef
StringRef& operator=(StringRef const& other) {
_data = other._data;
_length = other._length;
return *this;
}
/// @brief create a StringRef from an std::string
StringRef& operator=(std::string const& other) {
_data = other.c_str();
_length = other.size();
return *this;
}
/// @brief create a StringRef from a null-terminated C string
StringRef& operator=(char const* other) {
_data = other;
_length = strlen(other);
return *this;
}
/// @brief create a StringRef from a VPack slice of type String
StringRef& operator=(arangodb::velocypack::Slice const& slice) {
arangodb::velocypack::ValueLength l;
_data = slice.getString(l);
_length = l;
return *this;
}
char const* data;
size_t length;
size_t find(char c) const {
char const* p = static_cast<char const*>(memchr(static_cast<void const*>(_data), c, _length));
if (p == nullptr) {
return std::string::npos;
}
return (p - _data);
}
StringRef substr(size_t pos = 0, size_t count = std::string::npos) const {
if (pos >= _length) {
throw std::out_of_range("substr index out of bounds");
}
if (count == std::string::npos || (count + pos >= _length)) {
count = _length - pos;
}
return StringRef(_data + pos, count);
}
int compare(std::string const& other) const {
int res = memcmp(_data, other.c_str(), (std::min)(_length, other.size()));
if (res != 0) {
return res;
}
return (_length - other.size());
}
int compare(StringRef const& other) const {
int res = memcmp(_data, other._data, (std::min)(_length, other._length));
if (res != 0) {
return res;
}
return (_length - other._length);
}
inline std::string toString() const {
return std::string(_data, _length);
}
inline bool empty() const {
return (_length == 0);
}
char at(size_t index) const {
if (index >= _length) {
throw std::out_of_range("StringRef index out of bounds");
}
return operator[](index);
}
inline char const* begin() const {
return _data;
}
inline char const* end() const {
return _data + _length;
}
inline char front() const { return _data[0]; }
inline char back() const { return _data[_length - 1]; }
inline char operator[](size_t index) const noexcept {
return _data[index];
}
inline char const* data() const noexcept {
return _data;
}
inline size_t size() const noexcept {
return _length;
}
inline size_t length() const noexcept {
return _length;
}
private:
char const* _data;
size_t _length;
};
}
inline bool operator==(arangodb::StringRef const& lhs, arangodb::StringRef const& rhs) {
return (lhs.size() == rhs.size() && memcmp(lhs.data(), rhs.data(), lhs.size()) == 0);
}
inline bool operator!=(arangodb::StringRef const& lhs, arangodb::StringRef const& rhs) {
return !(lhs == rhs);
}
inline bool operator==(arangodb::StringRef const& lhs, std::string const& rhs) {
return (lhs.size() == rhs.size() && memcmp(lhs.data(), rhs.c_str(), lhs.size()) == 0);
}
inline bool operator!=(arangodb::StringRef const& lhs, std::string const& rhs) {
return !(lhs == rhs);
}
namespace std {
template <>
struct hash<arangodb::StringRef> {
size_t operator()(arangodb::StringRef const& value) const noexcept {
return XXH64(value.data, value.length, 0xdeadbeef);
return XXH64(value.data(), value.size(), 0xdeadbeef);
}
};
template <>
struct equal_to<arangodb::StringRef> {
bool operator()(arangodb::StringRef const& lhs,
arangodb::StringRef const& rhs) const {
if (lhs.length != rhs.length) {
return false;
}
return (memcmp(lhs.data, rhs.data, lhs.length) == 0);
arangodb::StringRef const& rhs) const noexcept {
return (lhs.size() == rhs.size() &&
(memcmp(lhs.data(), rhs.data(), lhs.size()) == 0));
}
};