diff --git a/dbms/src/Functions/FunctionsStringSimilarity.cpp b/dbms/src/Functions/FunctionsStringSimilarity.cpp new file mode 100644 index 00000000000..571c8afc377 --- /dev/null +++ b/dbms/src/Functions/FunctionsStringSimilarity.cpp @@ -0,0 +1,119 @@ +#include + +#include +#include +#include +#include +#include + +#include + +namespace DB +{ + +struct TrigramDistanceImpl +{ + using ResultType = Float32; + using CodePoint = UInt32; + + using TrigramMap = ClearableHashMap; + + static inline CodePoint readCodePoint(const char *& pos, const char * end) noexcept + { + size_t length = UTF8::seqLength(*pos); + + if (pos + length > end) + length = end - pos; + + if (length > sizeof(CodePoint)) + length = sizeof(CodePoint); + + CodePoint res = 0; + memcpy(&res, pos, length); + pos += length; + return res; + } + + static inline size_t calculateStats(const char * data, const size_t size, TrigramMap & ans) + { + ans.clear(); + size_t len = 0; + size_t trigramCnt = 0; + const char * start = data; + const char * end = data + size; + CodePoint cp1 = 0; + CodePoint cp2 = 0; + CodePoint cp3 = 0; + while (start != end) + { + cp1 = cp2; + cp2 = cp3; + cp3 = readCodePoint(start, end); + ++len; + if (len < 3) + continue; + ++trigramCnt; + ++ans[intHashCRC32(intHashCRC32(cp1) ^ cp2) ^ cp3]; + } + return trigramCnt; + } + + static inline UInt64 calculateMetric(const TrigramMap & lhs, const TrigramMap & rhs) + { + UInt64 res = 0; + + for (const auto & [trigram, count] : lhs) + { + if (auto it = rhs.find(trigram); it != rhs.end()) + res += std::abs(static_cast(count) - static_cast(it->second)); + else + res += count; + } + + for (const auto & [trigram, count] : rhs) + if (!lhs.has(trigram)) + res += count; + + return res; + } + + static void constant_constant(const std::string & data, const std::string & needle, Float32 & res) + { + TrigramMap haystack_stats; + TrigramMap needle_stats; + size_t first_size = calculateStats(data.data(), data.size(), haystack_stats); + size_t second_size = calculateStats(needle.data(), needle.size(), needle_stats); + res = calculateMetric(needle_stats, haystack_stats) * 1.0 / std::max(first_size + second_size, size_t(1)); + } + + static void vector_constant(const ColumnString::Chars & data, const ColumnString::Offsets & offsets, const std::string & needle, PaddedPODArray & res) + { + TrigramMap needle_stats; + TrigramMap haystack_stats; + const size_t needle_stats_size = calculateStats(needle.data(), needle.size(), needle_stats); + size_t prev_offset = 0; + for (size_t i = 0; i < offsets.size(); ++i) + { + const auto * haystack = &data[prev_offset]; + const size_t haystack_size = offsets[i] - prev_offset - 1; + size_t haystack_stats_size = calculateStats(reinterpret_cast(haystack), haystack_size, haystack_stats); + res[i] = calculateMetric(haystack_stats, needle_stats) * 1.0 / std::max(haystack_stats_size + needle_stats_size, size_t(1)); + prev_offset = offsets[i]; + } + } +}; + + +struct TrigramDistanceName +{ + static constexpr auto name = "trigramDistance"; +}; + +using FunctionTrigramDistance = FunctionsStringSimilarity; + +void registerFunctionsStringSimilarity(FunctionFactory & factory) +{ + factory.registerFunction(); +} + +} diff --git a/dbms/src/Functions/FunctionsStringSimilarity.h b/dbms/src/Functions/FunctionsStringSimilarity.h new file mode 100644 index 00000000000..c8d81205fe0 --- /dev/null +++ b/dbms/src/Functions/FunctionsStringSimilarity.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace DB +{ + +/** Calculate similarity metrics: + * + * trigramSimilarity(haystack, needle) --- calculate trigram distance between haystack and needle + * levensteinDistance(haystack, needle) --- calculate Levenstein distance between haystack and needle + */ + +namespace ErrorCodes +{ + extern const int ILLEGAL_TYPE_OF_ARGUMENT; + extern const int ILLEGAL_COLUMN; + extern const int NUMBER_OF_ARGUMENTS_DOESNT_MATCH; +} + +template +class FunctionsStringSimilarity : public IFunction +{ +public: + static constexpr auto name = Name::name; + + static FunctionPtr create(const Context &) { return std::make_shared(); } + + String getName() const override { return name; } + + size_t getNumberOfArguments() const override { return 2; } + + DataTypePtr getReturnTypeImpl(const DataTypes & arguments) const override + { + if (!isString(arguments[0])) + throw Exception( + "Illegal type " + arguments[0]->getName() + " of argument of function " + getName(), ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT); + + if (!isString(arguments[1])) + throw Exception( + "Illegal type " + arguments[1]->getName() + " of argument of function " + getName(), ErrorCodes::ILLEGAL_TYPE_OF_ARGUMENT); + + return std::make_shared>(); + } + + void executeImpl(Block & block, const ColumnNumbers & arguments, size_t result, size_t /*input_rows_count*/) override + { + using ResultType = typename Impl::ResultType; + + const ColumnPtr & column_haystack = block.getByPosition(arguments[0]).column; + const ColumnPtr & column_needle = block.getByPosition(arguments[1]).column; + + const ColumnConst * col_haystack_const = typeid_cast(&*column_haystack); + const ColumnConst * col_needle_const = typeid_cast(&*column_needle); + + if (!col_needle_const) + throw Exception("Second argument of function " + getName() + " must be constant string.", ErrorCodes::ILLEGAL_COLUMN); + + if (col_haystack_const) + { + ResultType res{}; + Impl::constant_constant(col_haystack_const->getValue(), col_needle_const->getValue(), res); + block.getByPosition(result).column = block.getByPosition(result).type->createColumnConst(col_haystack_const->size(), toField(res)); + return; + } + + auto col_res = ColumnVector::create(); + + typename ColumnVector::Container & vec_res = col_res->getData(); + vec_res.resize(column_haystack->size()); + + const ColumnString * col_haystack_vector = checkAndGetColumn(&*column_haystack); + + if (col_haystack_vector) + Impl::vector_constant( + col_haystack_vector->getChars(), col_haystack_vector->getOffsets(), col_needle_const->getValue(), vec_res); + else + throw Exception( + "Illegal columns " + block.getByPosition(arguments[0]).column->getName() + " and " + + block.getByPosition(arguments[1]).column->getName() + " of arguments of function " + getName(), + ErrorCodes::ILLEGAL_COLUMN); + + block.getByPosition(result).column = std::move(col_res); + } +}; + +} diff --git a/dbms/src/Functions/registerFunctions.cpp b/dbms/src/Functions/registerFunctions.cpp index 41164e5e65e..fa4c8f1d273 100644 --- a/dbms/src/Functions/registerFunctions.cpp +++ b/dbms/src/Functions/registerFunctions.cpp @@ -33,6 +33,7 @@ void registerFunctionsRound(FunctionFactory &); void registerFunctionsString(FunctionFactory &); void registerFunctionsStringArray(FunctionFactory &); void registerFunctionsStringSearch(FunctionFactory &); +void registerFunctionsStringSimilarity(FunctionFactory &); void registerFunctionsURL(FunctionFactory &); void registerFunctionsVisitParam(FunctionFactory &); void registerFunctionsMath(FunctionFactory &); @@ -72,6 +73,7 @@ void registerFunctions() registerFunctionsString(factory); registerFunctionsStringArray(factory); registerFunctionsStringSearch(factory); + registerFunctionsStringSimilarity(factory); registerFunctionsURL(factory); registerFunctionsVisitParam(factory); registerFunctionsMath(factory); diff --git a/dbms/tests/queries/0_stateless/00909_trigram_distance.reference b/dbms/tests/queries/0_stateless/00909_trigram_distance.reference new file mode 100644 index 00000000000..14dba2a2dcf --- /dev/null +++ b/dbms/tests/queries/0_stateless/00909_trigram_distance.reference @@ -0,0 +1,119 @@ +0 +0 +0 +0 +0 +1000 +1000 +1000 +1000 +1000 +1000 +1000 +1000 +1000 +1000 +0 +0 +0 +0 +0 +77 +77 +77 +77 +77 +636 +636 +636 +636 +636 +1000 +1000 +1000 +1000 +1000 +0 +1000 +1000 +0 +77 +636 +1000 +привет как дела?... Херсон +пап привет как дела - Яндекс.Видео +привет как дела клип - Яндекс.Видео +привет братан как дела - Яндекс.Видео +привет +http://metric.ru/ +http://autometric.ru/ +http://metrica.yandex.com/ +http://metris.ru/ +http://metrika.ru/ + +привет как дела?... Херсон +пап привет как дела - Яндекс.Видео +привет +привет как дела клип - Яндекс.Видео +привет братан как дела - Яндекс.Видео +http://metric.ru/ +http://autometric.ru/ +http://metrica.yandex.com/ +http://metris.ru/ +http://metrika.ru/ + +http://metrika.ru/ +http://metric.ru/ +http://metris.ru/ +http://autometric.ru/ +http://metrica.yandex.com/ +привет как дела?... Херсон +привет как дела клип - Яндекс.Видео +привет +пап привет как дела - Яндекс.Видео +привет братан как дела - Яндекс.Видео + +http://metric.ru/ +http://metrica.yandex.com/ +http://autometric.ru/ +http://metris.ru/ +http://metrika.ru/ +привет как дела?... Херсон +привет как дела клип - Яндекс.Видео +привет +пап привет как дела - Яндекс.Видео +привет братан как дела - Яндекс.Видео + +http://metrika.ru/ +http://metric.ru/ +http://metris.ru/ +http://autometric.ru/ +http://metrica.yandex.com/ +привет как дела?... Херсон +привет как дела клип - Яндекс.Видео +привет +пап привет как дела - Яндекс.Видео +привет братан как дела - Яндекс.Видео + +http://metric.ru/ +http://autometric.ru/ +http://metris.ru/ +http://metrika.ru/ +http://metrica.yandex.com/ +привет как дела?... Херсон +привет как дела клип - Яндекс.Видео +привет +пап привет как дела - Яндекс.Видео +привет братан как дела - Яндекс.Видео + +http://metrica.yandex.com/ +привет как дела?... Херсон +привет как дела клип - Яндекс.Видео +привет +пап привет как дела - Яндекс.Видео +привет братан как дела - Яндекс.Видео +http://metric.ru/ +http://autometric.ru/ +http://metris.ru/ +http://metrika.ru/ + diff --git a/dbms/tests/queries/0_stateless/00909_trigram_distance.sql b/dbms/tests/queries/0_stateless/00909_trigram_distance.sql new file mode 100644 index 00000000000..ca6a18d2513 --- /dev/null +++ b/dbms/tests/queries/0_stateless/00909_trigram_distance.sql @@ -0,0 +1,29 @@ +select round(1000 * trigramDistance(materialize(''), '')) from system.numbers limit 5; +select round(1000 * trigramDistance(materialize('абв'), '')) from system.numbers limit 5; +select round(1000 * trigramDistance(materialize(''), 'абв')) from system.numbers limit 5; +select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'абвгдеёжз')) from system.numbers limit 5; +select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'абвгдеёж')) from system.numbers limit 5; +select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'гдеёзд')) from system.numbers limit 5; +select round(1000 * trigramDistance(materialize('абвгдеёжз'), 'ёёёёёёёё')) from system.numbers limit 5; + +select round(1000 * trigramDistance('', '')); +select round(1000 * trigramDistance('абв', '')); +select round(1000 * trigramDistance('', 'абв')); +select round(1000 * trigramDistance('абвгдеёжз', 'абвгдеёжз')); +select round(1000 * trigramDistance('абвгдеёжз', 'абвгдеёж')); +select round(1000 * trigramDistance('абвгдеёжз', 'гдеёзд')); +select round(1000 * trigramDistance('абвгдеёжз', 'ёёёёёёёё')); + +drop table if exists test.test_distance; +create table test.test_distance (Title String) engine = Memory; +insert into test.test_distance values ('привет как дела?... Херсон'), ('привет как дела клип - Яндекс.Видео'), ('привет'), ('пап привет как дела - Яндекс.Видео'), ('привет братан как дела - Яндекс.Видео'), ('http://metric.ru/'), ('http://autometric.ru/'), ('http://metrica.yandex.com/'), ('http://metris.ru/'), ('http://metrika.ru/'), (''); + +SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'привет как дела'); +SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'как привет дела'); +SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metrika'); +SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metrica'); +SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metriks'); +SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'metrics'); +SELECT Title FROM test.test_distance ORDER BY trigramDistance(Title, 'yandex'); + +drop table if exists test.test_distance;