Function trigramDistance added for string similarity search

This commit is contained in:
Danila Kutenin 2019-02-21 08:08:37 +03:00
parent 853537d233
commit 16b2e45586
5 changed files with 361 additions and 0 deletions

View File

@ -0,0 +1,119 @@
#include <Functions/FunctionsStringSimilarity.h>
#include <Functions/FunctionFactory.h>
#include <Functions/FunctionsHashing.h>
#include <Common/HashTable/ClearableHashMap.h>
#include <Common/HashTable/Hash.h>
#include <Common/UTF8Helpers.h>
#include <cctype>
namespace DB
{
struct TrigramDistanceImpl
{
using ResultType = Float32;
using CodePoint = UInt32;
using TrigramMap = ClearableHashMap<UInt64, UInt64, TrivialHash>;
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<Int64>(count) - static_cast<Int64>(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<Float32> & 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<const char *>(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<TrigramDistanceImpl, TrigramDistanceName>;
void registerFunctionsStringSimilarity(FunctionFactory & factory)
{
factory.registerFunction<FunctionTrigramDistance>();
}
}

View File

@ -0,0 +1,92 @@
#pragma once
#include <Columns/ColumnConst.h>
#include <Columns/ColumnString.h>
#include <Columns/ColumnVector.h>
#include <DataTypes/DataTypesNumber.h>
#include <Functions/FunctionHelpers.h>
#include <Functions/IFunction.h>
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 <typename Impl, typename Name>
class FunctionsStringSimilarity : public IFunction
{
public:
static constexpr auto name = Name::name;
static FunctionPtr create(const Context &) { return std::make_shared<FunctionsStringSimilarity>(); }
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<DataTypeNumber<typename Impl::ResultType>>();
}
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<const ColumnConst *>(&*column_haystack);
const ColumnConst * col_needle_const = typeid_cast<const ColumnConst *>(&*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<String>(), col_needle_const->getValue<String>(), res);
block.getByPosition(result).column = block.getByPosition(result).type->createColumnConst(col_haystack_const->size(), toField(res));
return;
}
auto col_res = ColumnVector<ResultType>::create();
typename ColumnVector<ResultType>::Container & vec_res = col_res->getData();
vec_res.resize(column_haystack->size());
const ColumnString * col_haystack_vector = checkAndGetColumn<ColumnString>(&*column_haystack);
if (col_haystack_vector)
Impl::vector_constant(
col_haystack_vector->getChars(), col_haystack_vector->getOffsets(), col_needle_const->getValue<String>(), 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);
}
};
}

View File

@ -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);

View File

@ -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/

View File

@ -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;